charness 3.6.1 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +297 -0
- package/DEVELOPMENT.md +35 -5
- package/MIGRATION_RENAME_TO_CHARNESS.md +3 -2
- package/README.md +84 -782
- package/SETUP.md +48 -10
- package/agent-plugin/.claude-plugin/plugin.json +7 -0
- package/agent-plugin/README.md +35 -0
- package/agent-plugin/skills/charness-delegate-claude/SKILL.md +85 -0
- package/agent-plugin/skills/charness-delegate-claude/summarize-events.py +90 -0
- package/agent-plugin/skills/charness-delegate-opencode/SKILL.md +68 -0
- package/agent-plugin/skills/charness-manager/SKILL.md +152 -0
- package/dist/adapters/adapterNames.d.ts +6 -0
- package/dist/adapters/adapterNames.d.ts.map +1 -1
- package/dist/adapters/adapterNames.js +8 -2
- package/dist/adapters/adapterNames.js.map +1 -1
- package/dist/adapters/claudeCliAdapter.d.ts +8 -0
- package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeCliAdapter.js +42 -5
- package/dist/adapters/claudeCliAdapter.js.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.d.ts +10 -9
- package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.js +53 -55
- package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
- package/dist/adapters/createAdapter.d.ts +8 -0
- package/dist/adapters/createAdapter.d.ts.map +1 -1
- package/dist/adapters/createAdapter.js +14 -2
- package/dist/adapters/createAdapter.js.map +1 -1
- package/dist/adapters/openCodeAdapter.d.ts +43 -6
- package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/openCodeAdapter.js +97 -7
- package/dist/adapters/openCodeAdapter.js.map +1 -1
- package/dist/agentLogin/agentLogin.d.ts +1 -1
- package/dist/agentLogin/agentLogin.d.ts.map +1 -1
- package/dist/apiRetryKick.d.ts +4 -3
- package/dist/apiRetryKick.d.ts.map +1 -1
- package/dist/apiRetryKick.js +16 -9
- package/dist/apiRetryKick.js.map +1 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +1360 -387
- package/dist/bot.js.map +1 -1
- package/dist/cli/claudeProfiles.d.ts +11 -6
- package/dist/cli/claudeProfiles.d.ts.map +1 -1
- package/dist/cli/claudeProfiles.js +50 -22
- package/dist/cli/claudeProfiles.js.map +1 -1
- package/dist/cli/hot.d.ts +5 -0
- package/dist/cli/hot.d.ts.map +1 -1
- package/dist/cli/hot.js +71 -25
- package/dist/cli/hot.js.map +1 -1
- package/dist/cli/tscWatchGuard.d.ts +95 -0
- package/dist/cli/tscWatchGuard.d.ts.map +1 -0
- package/dist/cli/tscWatchGuard.js +295 -0
- package/dist/cli/tscWatchGuard.js.map +1 -0
- package/dist/cli.js +55 -1
- package/dist/cli.js.map +1 -1
- package/dist/configDirFile.d.ts +7 -3
- package/dist/configDirFile.d.ts.map +1 -1
- package/dist/configDirFile.js +12 -5
- package/dist/configDirFile.js.map +1 -1
- package/dist/connectors/jira/answerSink.d.ts +10 -0
- package/dist/connectors/jira/answerSink.d.ts.map +1 -1
- package/dist/connectors/jira/answerSink.js +13 -0
- package/dist/connectors/jira/answerSink.js.map +1 -1
- package/dist/connectors/jira/config.d.ts +2 -0
- package/dist/connectors/jira/config.d.ts.map +1 -1
- package/dist/connectors/jira/config.js +9 -1
- package/dist/connectors/jira/config.js.map +1 -1
- package/dist/connectors/jira/connector.d.ts +11 -1
- package/dist/connectors/jira/connector.d.ts.map +1 -1
- package/dist/connectors/jira/connector.js +7 -0
- package/dist/connectors/jira/connector.js.map +1 -1
- package/dist/connectors/jira/inbound.d.ts +27 -4
- package/dist/connectors/jira/inbound.d.ts.map +1 -1
- package/dist/connectors/jira/inbound.js +69 -8
- package/dist/connectors/jira/inbound.js.map +1 -1
- package/dist/connectors/jira/issueBlocks.d.ts +5 -0
- package/dist/connectors/jira/issueBlocks.d.ts.map +1 -1
- package/dist/connectors/jira/issueBlocks.js +11 -0
- package/dist/connectors/jira/issueBlocks.js.map +1 -1
- package/dist/connectors/telegram/bindPicker.d.ts +5 -0
- package/dist/connectors/telegram/bindPicker.d.ts.map +1 -1
- package/dist/connectors/telegram/bindPicker.js +18 -1
- package/dist/connectors/telegram/bindPicker.js.map +1 -1
- package/dist/connectors/telegram/bindPickerClaims.d.ts +46 -0
- package/dist/connectors/telegram/bindPickerClaims.d.ts.map +1 -0
- package/dist/connectors/telegram/bindPickerClaims.js +83 -0
- package/dist/connectors/telegram/bindPickerClaims.js.map +1 -0
- package/dist/connectors/telegram/bindSweep.d.ts +32 -0
- package/dist/connectors/telegram/bindSweep.d.ts.map +1 -0
- package/dist/connectors/telegram/bindSweep.js +47 -0
- package/dist/connectors/telegram/bindSweep.js.map +1 -0
- package/dist/connectors/telegram/bindingWelcome.d.ts +23 -0
- package/dist/connectors/telegram/bindingWelcome.d.ts.map +1 -0
- package/dist/connectors/telegram/bindingWelcome.js +26 -0
- package/dist/connectors/telegram/bindingWelcome.js.map +1 -0
- package/dist/connectors/telegram/botApiConnection.d.ts +74 -0
- package/dist/connectors/telegram/botApiConnection.d.ts.map +1 -0
- package/dist/connectors/telegram/botApiConnection.js +201 -0
- package/dist/connectors/telegram/botApiConnection.js.map +1 -0
- package/dist/connectors/telegram/commands/botCore.d.ts +13 -3
- package/dist/connectors/telegram/commands/botCore.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/botSkills.d.ts +21 -0
- package/dist/connectors/telegram/commands/botSkills.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/botSkills.js +133 -0
- package/dist/connectors/telegram/commands/botSkills.js.map +1 -0
- package/dist/connectors/telegram/commands/callbackMessage.d.ts +5 -0
- package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/callbackMessage.js +9 -0
- package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
- package/dist/connectors/telegram/commands/compaction.d.ts +65 -3
- package/dist/connectors/telegram/commands/compaction.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/compaction.js +512 -94
- package/dist/connectors/telegram/commands/compaction.js.map +1 -1
- package/dist/connectors/telegram/commands/displayModes.d.ts +1 -1
- package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/displayModes.js +6 -18
- package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
- package/dist/connectors/telegram/commands/fallbackChain.js +5 -5
- package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
- package/dist/connectors/telegram/commands/reminders.d.ts +1 -1
- package/dist/connectors/telegram/commands/reminders.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/reminders.js +4 -15
- package/dist/connectors/telegram/commands/reminders.js.map +1 -1
- package/dist/connectors/telegram/commands/runners.d.ts +52 -0
- package/dist/connectors/telegram/commands/runners.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/runners.js +486 -0
- package/dist/connectors/telegram/commands/runners.js.map +1 -0
- package/dist/connectors/telegram/commands/selfUpdate.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/selfUpdate.js +1 -11
- package/dist/connectors/telegram/commands/selfUpdate.js.map +1 -1
- package/dist/connectors/telegram/commands/taskAgents.d.ts +22 -0
- package/dist/connectors/telegram/commands/taskAgents.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/taskAgents.js +104 -0
- package/dist/connectors/telegram/commands/taskAgents.js.map +1 -0
- package/dist/connectors/telegram/commands/todo.d.ts +82 -0
- package/dist/connectors/telegram/commands/todo.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/todo.js +384 -0
- package/dist/connectors/telegram/commands/todo.js.map +1 -0
- package/dist/connectors/telegram/commands/topicCommand.d.ts +6 -2
- package/dist/connectors/telegram/commands/topicCommand.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/topicCommand.js +6 -9
- package/dist/connectors/telegram/commands/topicCommand.js.map +1 -1
- package/dist/connectors/telegram/openBindMessages.d.ts +45 -0
- package/dist/connectors/telegram/openBindMessages.d.ts.map +1 -0
- package/dist/connectors/telegram/openBindMessages.js +67 -0
- package/dist/connectors/telegram/openBindMessages.js.map +1 -0
- package/dist/connectors/telegram/outputFlushPlan.d.ts +49 -0
- package/dist/connectors/telegram/outputFlushPlan.d.ts.map +1 -1
- package/dist/connectors/telegram/outputFlushPlan.js +46 -0
- package/dist/connectors/telegram/outputFlushPlan.js.map +1 -1
- package/dist/connectors/telegram/pendingTopicName.d.ts +26 -0
- package/dist/connectors/telegram/pendingTopicName.d.ts.map +1 -0
- package/dist/connectors/telegram/pendingTopicName.js +27 -0
- package/dist/connectors/telegram/pendingTopicName.js.map +1 -0
- package/dist/connectors/telegram/runnerPairingMessages.d.ts +27 -0
- package/dist/connectors/telegram/runnerPairingMessages.d.ts.map +1 -0
- package/dist/connectors/telegram/runnerPairingMessages.js +105 -0
- package/dist/connectors/telegram/runnerPairingMessages.js.map +1 -0
- package/dist/connectors/test/connector.d.ts.map +1 -1
- package/dist/connectors/test/connector.js +6 -1
- package/dist/connectors/test/connector.js.map +1 -1
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/de.js +148 -7
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +148 -7
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +148 -7
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +148 -7
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/hi.d.ts.map +1 -1
- package/dist/i18n/hi.js +148 -7
- package/dist/i18n/hi.js.map +1 -1
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ja.js +148 -7
- package/dist/i18n/ja.js.map +1 -1
- package/dist/i18n/ka.d.ts.map +1 -1
- package/dist/i18n/ka.js +148 -7
- package/dist/i18n/ka.js.map +1 -1
- package/dist/i18n/pt.d.ts.map +1 -1
- package/dist/i18n/pt.js +148 -7
- package/dist/i18n/pt.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +148 -7
- package/dist/i18n/ru.js.map +1 -1
- package/dist/i18n/uk.d.ts.map +1 -1
- package/dist/i18n/uk.js +148 -7
- package/dist/i18n/uk.js.map +1 -1
- package/dist/i18n/uz.d.ts.map +1 -1
- package/dist/i18n/uz.js +148 -7
- package/dist/i18n/uz.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +148 -7
- package/dist/i18n/zh.js.map +1 -1
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +1 -1
- package/dist/installManager.d.ts +70 -1
- package/dist/installManager.d.ts.map +1 -1
- package/dist/installManager.js +125 -13
- package/dist/installManager.js.map +1 -1
- package/dist/instanceSettings.d.ts +50 -1
- package/dist/instanceSettings.d.ts.map +1 -1
- package/dist/instanceSettings.js +119 -26
- package/dist/instanceSettings.js.map +1 -1
- package/dist/openCodeQuestionFlow.d.ts +30 -0
- package/dist/openCodeQuestionFlow.d.ts.map +1 -1
- package/dist/openCodeQuestionFlow.js +43 -0
- package/dist/openCodeQuestionFlow.js.map +1 -1
- package/dist/outputTrace.d.ts +6 -0
- package/dist/outputTrace.d.ts.map +1 -1
- package/dist/outputTrace.js +24 -1
- package/dist/outputTrace.js.map +1 -1
- package/dist/postToSession.d.ts +20 -5
- package/dist/postToSession.d.ts.map +1 -1
- package/dist/postToSession.js +38 -7
- package/dist/postToSession.js.map +1 -1
- package/dist/remoteRun/cliArgs.d.ts +75 -0
- package/dist/remoteRun/cliArgs.d.ts.map +1 -0
- package/dist/remoteRun/cliArgs.js +206 -0
- package/dist/remoteRun/cliArgs.js.map +1 -0
- package/dist/remoteRun/commands.d.ts +77 -0
- package/dist/remoteRun/commands.d.ts.map +1 -0
- package/dist/remoteRun/commands.js +327 -0
- package/dist/remoteRun/commands.js.map +1 -0
- package/dist/remoteRun/depsPlan.d.ts +16 -0
- package/dist/remoteRun/depsPlan.d.ts.map +1 -0
- package/dist/remoteRun/depsPlan.js +82 -0
- package/dist/remoteRun/depsPlan.js.map +1 -0
- package/dist/remoteRun/exitCodes.d.ts +34 -0
- package/dist/remoteRun/exitCodes.d.ts.map +1 -0
- package/dist/remoteRun/exitCodes.js +38 -0
- package/dist/remoteRun/exitCodes.js.map +1 -0
- package/dist/remoteRun/foreground.d.ts +29 -0
- package/dist/remoteRun/foreground.d.ts.map +1 -0
- package/dist/remoteRun/foreground.js +159 -0
- package/dist/remoteRun/foreground.js.map +1 -0
- package/dist/remoteRun/interrupts.d.ts +27 -0
- package/dist/remoteRun/interrupts.d.ts.map +1 -0
- package/dist/remoteRun/interrupts.js +78 -0
- package/dist/remoteRun/interrupts.js.map +1 -0
- package/dist/remoteRun/jobFiles.d.ts +31 -0
- package/dist/remoteRun/jobFiles.d.ts.map +1 -0
- package/dist/remoteRun/jobFiles.js +84 -0
- package/dist/remoteRun/jobFiles.js.map +1 -0
- package/dist/remoteRun/jobFormat.d.ts +16 -0
- package/dist/remoteRun/jobFormat.d.ts.map +1 -0
- package/dist/remoteRun/jobFormat.js +130 -0
- package/dist/remoteRun/jobFormat.js.map +1 -0
- package/dist/remoteRun/jobId.d.ts +15 -0
- package/dist/remoteRun/jobId.d.ts.map +1 -0
- package/dist/remoteRun/jobId.js +85 -0
- package/dist/remoteRun/jobId.js.map +1 -0
- package/dist/remoteRun/jobMeta.d.ts +18 -0
- package/dist/remoteRun/jobMeta.d.ts.map +1 -0
- package/dist/remoteRun/jobMeta.js +33 -0
- package/dist/remoteRun/jobMeta.js.map +1 -0
- package/dist/remoteRun/jobQuery.d.ts +12 -0
- package/dist/remoteRun/jobQuery.d.ts.map +1 -0
- package/dist/remoteRun/jobQuery.js +22 -0
- package/dist/remoteRun/jobQuery.js.map +1 -0
- package/dist/remoteRun/jobRecords.d.ts +22 -0
- package/dist/remoteRun/jobRecords.d.ts.map +1 -0
- package/dist/remoteRun/jobRecords.js +145 -0
- package/dist/remoteRun/jobRecords.js.map +1 -0
- package/dist/remoteRun/jobState.d.ts +62 -0
- package/dist/remoteRun/jobState.d.ts.map +1 -0
- package/dist/remoteRun/jobState.js +132 -0
- package/dist/remoteRun/jobState.js.map +1 -0
- package/dist/remoteRun/leaseTiming.d.ts +18 -0
- package/dist/remoteRun/leaseTiming.d.ts.map +1 -0
- package/dist/remoteRun/leaseTiming.js +43 -0
- package/dist/remoteRun/leaseTiming.js.map +1 -0
- package/dist/remoteRun/output.d.ts +37 -0
- package/dist/remoteRun/output.d.ts.map +1 -0
- package/dist/remoteRun/output.js +127 -0
- package/dist/remoteRun/output.js.map +1 -0
- package/dist/remoteRun/pairedRunners.d.ts +51 -0
- package/dist/remoteRun/pairedRunners.d.ts.map +1 -0
- package/dist/remoteRun/pairedRunners.js +165 -0
- package/dist/remoteRun/pairedRunners.js.map +1 -0
- package/dist/remoteRun/pathPatterns.d.ts +33 -0
- package/dist/remoteRun/pathPatterns.d.ts.map +1 -0
- package/dist/remoteRun/pathPatterns.js +115 -0
- package/dist/remoteRun/pathPatterns.js.map +1 -0
- package/dist/remoteRun/probe.d.ts +78 -0
- package/dist/remoteRun/probe.d.ts.map +1 -0
- package/dist/remoteRun/probe.js +157 -0
- package/dist/remoteRun/probe.js.map +1 -0
- package/dist/remoteRun/project.d.ts +26 -0
- package/dist/remoteRun/project.d.ts.map +1 -0
- package/dist/remoteRun/project.js +159 -0
- package/dist/remoteRun/project.js.map +1 -0
- package/dist/remoteRun/projectConfig.d.ts +38 -0
- package/dist/remoteRun/projectConfig.d.ts.map +1 -0
- package/dist/remoteRun/projectConfig.js +87 -0
- package/dist/remoteRun/projectConfig.js.map +1 -0
- package/dist/remoteRun/remoteRunCli.d.ts +22 -0
- package/dist/remoteRun/remoteRunCli.d.ts.map +1 -0
- package/dist/remoteRun/remoteRunCli.js +119 -0
- package/dist/remoteRun/remoteRunCli.js.map +1 -0
- package/dist/remoteRun/remoteRunError.d.ts +14 -0
- package/dist/remoteRun/remoteRunError.d.ts.map +1 -0
- package/dist/remoteRun/remoteRunError.js +28 -0
- package/dist/remoteRun/remoteRunError.js.map +1 -0
- package/dist/remoteRun/rsync.d.ts +17 -0
- package/dist/remoteRun/rsync.d.ts.map +1 -0
- package/dist/remoteRun/rsync.js +39 -0
- package/dist/remoteRun/rsync.js.map +1 -0
- package/dist/remoteRun/runnerClient.d.ts +52 -0
- package/dist/remoteRun/runnerClient.d.ts.map +1 -0
- package/dist/remoteRun/runnerClient.js +143 -0
- package/dist/remoteRun/runnerClient.js.map +1 -0
- package/dist/remoteRun/runnerSummary.d.ts +24 -0
- package/dist/remoteRun/runnerSummary.d.ts.map +1 -0
- package/dist/remoteRun/runnerSummary.js +86 -0
- package/dist/remoteRun/runnerSummary.js.map +1 -0
- package/dist/remoteRun/runnerWait.d.ts +68 -0
- package/dist/remoteRun/runnerWait.d.ts.map +1 -0
- package/dist/remoteRun/runnerWait.js +137 -0
- package/dist/remoteRun/runnerWait.js.map +1 -0
- package/dist/remoteRun/runnerWaitTiming.d.ts +16 -0
- package/dist/remoteRun/runnerWaitTiming.d.ts.map +1 -0
- package/dist/remoteRun/runnerWaitTiming.js +37 -0
- package/dist/remoteRun/runnerWaitTiming.js.map +1 -0
- package/dist/remoteRun/runners.d.ts +57 -0
- package/dist/remoteRun/runners.d.ts.map +1 -0
- package/dist/remoteRun/runners.js +209 -0
- package/dist/remoteRun/runners.js.map +1 -0
- package/dist/remoteRun/runnersCatalog.d.ts +14 -0
- package/dist/remoteRun/runnersCatalog.d.ts.map +1 -0
- package/dist/remoteRun/runnersCatalog.js +68 -0
- package/dist/remoteRun/runnersCatalog.js.map +1 -0
- package/dist/remoteRun/runnersConfig.d.ts +53 -0
- package/dist/remoteRun/runnersConfig.d.ts.map +1 -0
- package/dist/remoteRun/runnersConfig.js +109 -0
- package/dist/remoteRun/runnersConfig.js.map +1 -0
- package/dist/remoteRun/safeCopy.d.ts +31 -0
- package/dist/remoteRun/safeCopy.d.ts.map +1 -0
- package/dist/remoteRun/safeCopy.js +137 -0
- package/dist/remoteRun/safeCopy.js.map +1 -0
- package/dist/remoteRun/sendPlan.d.ts +67 -0
- package/dist/remoteRun/sendPlan.d.ts.map +1 -0
- package/dist/remoteRun/sendPlan.js +139 -0
- package/dist/remoteRun/sendPlan.js.map +1 -0
- package/dist/remoteRun/ssh.d.ts +82 -0
- package/dist/remoteRun/ssh.d.ts.map +1 -0
- package/dist/remoteRun/ssh.js +236 -0
- package/dist/remoteRun/ssh.js.map +1 -0
- package/dist/remoteRun/submit.d.ts +29 -0
- package/dist/remoteRun/submit.d.ts.map +1 -0
- package/dist/remoteRun/submit.js +142 -0
- package/dist/remoteRun/submit.js.map +1 -0
- package/dist/requests/answerRequest.d.ts +5 -0
- package/dist/requests/answerRequest.d.ts.map +1 -1
- package/dist/requests/answerRequest.js +16 -1
- package/dist/requests/answerRequest.js.map +1 -1
- package/dist/requests/requestLedger.d.ts +13 -0
- package/dist/requests/requestLedger.d.ts.map +1 -1
- package/dist/requests/requestLedger.js +9 -0
- package/dist/requests/requestLedger.js.map +1 -1
- package/dist/requests/topicRequestSettle.d.ts +30 -0
- package/dist/requests/topicRequestSettle.d.ts.map +1 -0
- package/dist/requests/topicRequestSettle.js +23 -0
- package/dist/requests/topicRequestSettle.js.map +1 -0
- package/dist/requests/wakeUpEngine.d.ts +4 -0
- package/dist/requests/wakeUpEngine.d.ts.map +1 -1
- package/dist/requests/wakeUpEngine.js +5 -1
- package/dist/requests/wakeUpEngine.js.map +1 -1
- package/dist/runnerUtility/adminCheck.d.ts +11 -0
- package/dist/runnerUtility/adminCheck.d.ts.map +1 -0
- package/dist/runnerUtility/adminCheck.js +68 -0
- package/dist/runnerUtility/adminCheck.js.map +1 -0
- package/dist/runnerUtility/appInstall.d.ts +25 -0
- package/dist/runnerUtility/appInstall.d.ts.map +1 -0
- package/dist/runnerUtility/appInstall.js +147 -0
- package/dist/runnerUtility/appInstall.js.map +1 -0
- package/dist/runnerUtility/appUpdate.d.ts +39 -0
- package/dist/runnerUtility/appUpdate.d.ts.map +1 -0
- package/dist/runnerUtility/appUpdate.js +90 -0
- package/dist/runnerUtility/appUpdate.js.map +1 -0
- package/dist/runnerUtility/execEnv.d.ts +21 -0
- package/dist/runnerUtility/execEnv.d.ts.map +1 -0
- package/dist/runnerUtility/execEnv.js +109 -0
- package/dist/runnerUtility/execEnv.js.map +1 -0
- package/dist/runnerUtility/execSession.d.ts +13 -0
- package/dist/runnerUtility/execSession.d.ts.map +1 -0
- package/dist/runnerUtility/execSession.js +58 -0
- package/dist/runnerUtility/execSession.js.map +1 -0
- package/dist/runnerUtility/hubClient.d.ts +30 -0
- package/dist/runnerUtility/hubClient.d.ts.map +1 -0
- package/dist/runnerUtility/hubClient.js +127 -0
- package/dist/runnerUtility/hubClient.js.map +1 -0
- package/dist/runnerUtility/injectStream.d.ts +9 -0
- package/dist/runnerUtility/injectStream.d.ts.map +1 -0
- package/dist/runnerUtility/injectStream.js +12 -0
- package/dist/runnerUtility/injectStream.js.map +1 -0
- package/dist/runnerUtility/iosCheck.d.ts +62 -0
- package/dist/runnerUtility/iosCheck.d.ts.map +1 -0
- package/dist/runnerUtility/iosCheck.js +200 -0
- package/dist/runnerUtility/iosCheck.js.map +1 -0
- package/dist/runnerUtility/launcher.d.ts +5 -0
- package/dist/runnerUtility/launcher.d.ts.map +1 -0
- package/dist/runnerUtility/launcher.js +85 -0
- package/dist/runnerUtility/launcher.js.map +1 -0
- package/dist/runnerUtility/linkLoop.d.ts +47 -0
- package/dist/runnerUtility/linkLoop.d.ts.map +1 -0
- package/dist/runnerUtility/linkLoop.js +281 -0
- package/dist/runnerUtility/linkLoop.js.map +1 -0
- package/dist/runnerUtility/localLinkStop.d.ts +23 -0
- package/dist/runnerUtility/localLinkStop.d.ts.map +1 -0
- package/dist/runnerUtility/localLinkStop.js +79 -0
- package/dist/runnerUtility/localLinkStop.js.map +1 -0
- package/dist/runnerUtility/machineReport.d.ts +39 -0
- package/dist/runnerUtility/machineReport.d.ts.map +1 -0
- package/dist/runnerUtility/machineReport.js +161 -0
- package/dist/runnerUtility/machineReport.js.map +1 -0
- package/dist/runnerUtility/packageId.d.ts +5 -0
- package/dist/runnerUtility/packageId.d.ts.map +1 -0
- package/dist/runnerUtility/packageId.js +64 -0
- package/dist/runnerUtility/packageId.js.map +1 -0
- package/dist/runnerUtility/pairingRecovery.d.ts +31 -0
- package/dist/runnerUtility/pairingRecovery.d.ts.map +1 -0
- package/dist/runnerUtility/pairingRecovery.js +24 -0
- package/dist/runnerUtility/pairingRecovery.js.map +1 -0
- package/dist/runnerUtility/pueueDaemon.d.ts +36 -0
- package/dist/runnerUtility/pueueDaemon.d.ts.map +1 -0
- package/dist/runnerUtility/pueueDaemon.js +165 -0
- package/dist/runnerUtility/pueueDaemon.js.map +1 -0
- package/dist/runnerUtility/runnerCli.d.ts +18 -0
- package/dist/runnerUtility/runnerCli.d.ts.map +1 -0
- package/dist/runnerUtility/runnerCli.js +184 -0
- package/dist/runnerUtility/runnerCli.js.map +1 -0
- package/dist/runnerUtility/runnerCommands.d.ts +67 -0
- package/dist/runnerUtility/runnerCommands.d.ts.map +1 -0
- package/dist/runnerUtility/runnerCommands.js +418 -0
- package/dist/runnerUtility/runnerCommands.js.map +1 -0
- package/dist/runnerUtility/runnerConfig.d.ts +63 -0
- package/dist/runnerUtility/runnerConfig.d.ts.map +1 -0
- package/dist/runnerUtility/runnerConfig.js +153 -0
- package/dist/runnerUtility/runnerConfig.js.map +1 -0
- package/dist/runnerUtility/runnerFlags.d.ts +9 -0
- package/dist/runnerUtility/runnerFlags.d.ts.map +1 -0
- package/dist/runnerUtility/runnerFlags.js +53 -0
- package/dist/runnerUtility/runnerFlags.js.map +1 -0
- package/dist/runnerUtility/runnerHome.d.ts +30 -0
- package/dist/runnerUtility/runnerHome.d.ts.map +1 -0
- package/dist/runnerUtility/runnerHome.js +82 -0
- package/dist/runnerUtility/runnerHome.js.map +1 -0
- package/dist/runnerUtility/runnerKeys.d.ts +11 -0
- package/dist/runnerUtility/runnerKeys.d.ts.map +1 -0
- package/dist/runnerUtility/runnerKeys.js +80 -0
- package/dist/runnerUtility/runnerKeys.js.map +1 -0
- package/dist/runnerUtility/runnerServers.d.ts +40 -0
- package/dist/runnerUtility/runnerServers.d.ts.map +1 -0
- package/dist/runnerUtility/runnerServers.js +79 -0
- package/dist/runnerUtility/runnerServers.js.map +1 -0
- package/dist/runnerUtility/serviceManager.d.ts +54 -0
- package/dist/runnerUtility/serviceManager.d.ts.map +1 -0
- package/dist/runnerUtility/serviceManager.js +413 -0
- package/dist/runnerUtility/serviceManager.js.map +1 -0
- package/dist/runnerUtility/serviceUnits.d.ts +36 -0
- package/dist/runnerUtility/serviceUnits.d.ts.map +1 -0
- package/dist/runnerUtility/serviceUnits.js +169 -0
- package/dist/runnerUtility/serviceUnits.js.map +1 -0
- package/dist/runnerUtility/toolInstall.d.ts +95 -0
- package/dist/runnerUtility/toolInstall.d.ts.map +1 -0
- package/dist/runnerUtility/toolInstall.js +232 -0
- package/dist/runnerUtility/toolInstall.js.map +1 -0
- package/dist/runnerUtility/versionPollTiming.d.ts +16 -0
- package/dist/runnerUtility/versionPollTiming.d.ts.map +1 -0
- package/dist/runnerUtility/versionPollTiming.js +36 -0
- package/dist/runnerUtility/versionPollTiming.js.map +1 -0
- package/dist/runners/authorizedKeys.d.ts +52 -0
- package/dist/runners/authorizedKeys.d.ts.map +1 -0
- package/dist/runners/authorizedKeys.js +153 -0
- package/dist/runners/authorizedKeys.js.map +1 -0
- package/dist/runners/bootstrapLine.d.ts +4 -0
- package/dist/runners/bootstrapLine.d.ts.map +1 -0
- package/dist/runners/bootstrapLine.js +69 -0
- package/dist/runners/bootstrapLine.js.map +1 -0
- package/dist/runners/gate.d.ts +41 -0
- package/dist/runners/gate.d.ts.map +1 -0
- package/dist/runners/gate.js +216 -0
- package/dist/runners/gate.js.map +1 -0
- package/dist/runners/gateLink.d.ts +26 -0
- package/dist/runners/gateLink.d.ts.map +1 -0
- package/dist/runners/gateLink.js +240 -0
- package/dist/runners/gateLink.js.map +1 -0
- package/dist/runners/gateProtocol.d.ts +98 -0
- package/dist/runners/gateProtocol.d.ts.map +1 -0
- package/dist/runners/gateProtocol.js +149 -0
- package/dist/runners/gateProtocol.js.map +1 -0
- package/dist/runners/gateWrapper.d.ts +6 -0
- package/dist/runners/gateWrapper.d.ts.map +1 -0
- package/dist/runners/gateWrapper.js +70 -0
- package/dist/runners/gateWrapper.js.map +1 -0
- package/dist/runners/hubAddress.d.ts +33 -0
- package/dist/runners/hubAddress.d.ts.map +1 -0
- package/dist/runners/hubAddress.js +88 -0
- package/dist/runners/hubAddress.js.map +1 -0
- package/dist/runners/hubAddressCandidates.d.ts +16 -0
- package/dist/runners/hubAddressCandidates.d.ts.map +1 -0
- package/dist/runners/hubAddressCandidates.js +96 -0
- package/dist/runners/hubAddressCandidates.js.map +1 -0
- package/dist/runners/hubIds.d.ts +16 -0
- package/dist/runners/hubIds.d.ts.map +1 -0
- package/dist/runners/hubIds.js +25 -0
- package/dist/runners/hubIds.js.map +1 -0
- package/dist/runners/hubKey.d.ts +6 -0
- package/dist/runners/hubKey.d.ts.map +1 -0
- package/dist/runners/hubKey.js +78 -0
- package/dist/runners/hubKey.js.map +1 -0
- package/dist/runners/hubLock.d.ts +7 -0
- package/dist/runners/hubLock.d.ts.map +1 -0
- package/dist/runners/hubLock.js +230 -0
- package/dist/runners/hubLock.js.map +1 -0
- package/dist/runners/hubPackage.d.ts +23 -0
- package/dist/runners/hubPackage.d.ts.map +1 -0
- package/dist/runners/hubPackage.js +184 -0
- package/dist/runners/hubPackage.js.map +1 -0
- package/dist/runners/hubPairing.d.ts +63 -0
- package/dist/runners/hubPairing.d.ts.map +1 -0
- package/dist/runners/hubPairing.js +261 -0
- package/dist/runners/hubPairing.js.map +1 -0
- package/dist/runners/hubPaths.d.ts +38 -0
- package/dist/runners/hubPaths.d.ts.map +1 -0
- package/dist/runners/hubPaths.js +94 -0
- package/dist/runners/hubPaths.js.map +1 -0
- package/dist/runners/hubRegistry.d.ts +112 -0
- package/dist/runners/hubRegistry.d.ts.map +1 -0
- package/dist/runners/hubRegistry.js +210 -0
- package/dist/runners/hubRegistry.js.map +1 -0
- package/dist/runners/linkControl.d.ts +27 -0
- package/dist/runners/linkControl.d.ts.map +1 -0
- package/dist/runners/linkControl.js +124 -0
- package/dist/runners/linkControl.js.map +1 -0
- package/dist/runners/pairCode.d.ts +30 -0
- package/dist/runners/pairCode.d.ts.map +1 -0
- package/dist/runners/pairCode.js +46 -0
- package/dist/runners/pairCode.js.map +1 -0
- package/dist/runners/pairingTiming.d.ts +18 -0
- package/dist/runners/pairingTiming.d.ts.map +1 -0
- package/dist/runners/pairingTiming.js +42 -0
- package/dist/runners/pairingTiming.js.map +1 -0
- package/dist/runners/runnerProxy.d.ts +9 -0
- package/dist/runners/runnerProxy.d.ts.map +1 -0
- package/dist/runners/runnerProxy.js +67 -0
- package/dist/runners/runnerProxy.js.map +1 -0
- package/dist/runners/runnerRemoval.d.ts +21 -0
- package/dist/runners/runnerRemoval.d.ts.map +1 -0
- package/dist/runners/runnerRemoval.js +82 -0
- package/dist/runners/runnerRemoval.js.map +1 -0
- package/dist/runners/runnersCli.d.ts +16 -0
- package/dist/runners/runnersCli.d.ts.map +1 -0
- package/dist/runners/runnersCli.js +214 -0
- package/dist/runners/runnersCli.js.map +1 -0
- package/dist/runners/runnersList.d.ts +17 -0
- package/dist/runners/runnersList.d.ts.map +1 -0
- package/dist/runners/runnersList.js +49 -0
- package/dist/runners/runnersList.js.map +1 -0
- package/dist/runners/runnersOverview.d.ts +20 -0
- package/dist/runners/runnersOverview.d.ts.map +1 -0
- package/dist/runners/runnersOverview.js +55 -0
- package/dist/runners/runnersOverview.js.map +1 -0
- package/dist/runners/sshKeys.d.ts +26 -0
- package/dist/runners/sshKeys.d.ts.map +1 -0
- package/dist/runners/sshKeys.js +60 -0
- package/dist/runners/sshKeys.js.map +1 -0
- package/dist/runners/tarArchive.d.ts +20 -0
- package/dist/runners/tarArchive.d.ts.map +1 -0
- package/dist/runners/tarArchive.js +70 -0
- package/dist/runners/tarArchive.js.map +1 -0
- package/dist/scheduler/delivery.d.ts +2 -0
- package/dist/scheduler/delivery.d.ts.map +1 -1
- package/dist/scheduler/delivery.js +8 -4
- package/dist/scheduler/delivery.js.map +1 -1
- package/dist/scheduler/mcpSurface.d.ts +65 -10
- package/dist/scheduler/mcpSurface.d.ts.map +1 -1
- package/dist/scheduler/mcpSurface.js +319 -89
- package/dist/scheduler/mcpSurface.js.map +1 -1
- package/dist/scheduler/recurrence.d.ts +5 -0
- package/dist/scheduler/recurrence.d.ts.map +1 -1
- package/dist/scheduler/recurrence.js +12 -2
- package/dist/scheduler/recurrence.js.map +1 -1
- package/dist/scheduler/strictToolInput.d.ts +45 -0
- package/dist/scheduler/strictToolInput.d.ts.map +1 -0
- package/dist/scheduler/strictToolInput.js +155 -0
- package/dist/scheduler/strictToolInput.js.map +1 -0
- package/dist/scheduler/types.d.ts +5 -0
- package/dist/scheduler/types.d.ts.map +1 -1
- package/dist/scheduler/workerWatch.d.ts +23 -0
- package/dist/scheduler/workerWatch.d.ts.map +1 -0
- package/dist/scheduler/workerWatch.js +26 -0
- package/dist/scheduler/workerWatch.js.map +1 -0
- package/dist/selfUpdate/emittedSource.d.ts +11 -0
- package/dist/selfUpdate/emittedSource.d.ts.map +1 -0
- package/dist/selfUpdate/emittedSource.js +24 -0
- package/dist/selfUpdate/emittedSource.js.map +1 -0
- package/dist/selfUpdate/gitSource.d.ts +0 -6
- package/dist/selfUpdate/gitSource.d.ts.map +1 -1
- package/dist/selfUpdate/gitSource.js +2 -18
- package/dist/selfUpdate/gitSource.js.map +1 -1
- package/dist/selfUpdate/selfCheck.js +2 -2
- package/dist/selfUpdate/selfCheck.js.map +1 -1
- package/dist/selfUpdate/updatePending.d.ts +1 -1
- package/dist/sendErrorClassifier.d.ts +10 -0
- package/dist/sendErrorClassifier.d.ts.map +1 -1
- package/dist/sendErrorClassifier.js +22 -0
- package/dist/sendErrorClassifier.js.map +1 -1
- package/dist/state.d.ts +78 -7
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +125 -8
- package/dist/state.js.map +1 -1
- package/dist/stateFile.d.ts +9 -0
- package/dist/stateFile.d.ts.map +1 -0
- package/dist/stateFile.js +57 -0
- package/dist/stateFile.js.map +1 -0
- package/dist/stt/sttInstall.d.ts +7 -3
- package/dist/stt/sttInstall.d.ts.map +1 -1
- package/dist/stt/sttInstall.js +8 -4
- package/dist/stt/sttInstall.js.map +1 -1
- package/dist/stt/sttSettings.d.ts +2 -1
- package/dist/stt/sttSettings.d.ts.map +1 -1
- package/dist/stt/sttSettings.js +3 -2
- package/dist/stt/sttSettings.js.map +1 -1
- package/dist/stt/transcribeChain.js +2 -0
- package/dist/stt/transcribeChain.js.map +1 -1
- package/dist/testTimings.d.ts +16 -3
- package/dist/testTimings.d.ts.map +1 -1
- package/dist/testTimings.js +35 -2
- package/dist/testTimings.js.map +1 -1
- package/dist/todos/board.d.ts +83 -0
- package/dist/todos/board.d.ts.map +1 -0
- package/dist/todos/board.js +227 -0
- package/dist/todos/board.js.map +1 -0
- package/dist/todos/format.d.ts +33 -0
- package/dist/todos/format.d.ts.map +1 -0
- package/dist/todos/format.js +90 -0
- package/dist/todos/format.js.map +1 -0
- package/dist/todos/merge.d.ts +46 -0
- package/dist/todos/merge.d.ts.map +1 -0
- package/dist/todos/merge.js +137 -0
- package/dist/todos/merge.js.map +1 -0
- package/dist/todos/midTurn.d.ts +75 -0
- package/dist/todos/midTurn.d.ts.map +1 -0
- package/dist/todos/midTurn.js +214 -0
- package/dist/todos/midTurn.js.map +1 -0
- package/dist/todos/nag.d.ts +85 -0
- package/dist/todos/nag.d.ts.map +1 -0
- package/dist/todos/nag.js +103 -0
- package/dist/todos/nag.js.map +1 -0
- package/dist/todos/order.d.ts +25 -0
- package/dist/todos/order.d.ts.map +1 -0
- package/dist/todos/order.js +61 -0
- package/dist/todos/order.js.map +1 -0
- package/dist/todos/projects.d.ts +102 -0
- package/dist/todos/projects.d.ts.map +1 -0
- package/dist/todos/projects.js +342 -0
- package/dist/todos/projects.js.map +1 -0
- package/dist/todos/registry.d.ts +64 -0
- package/dist/todos/registry.d.ts.map +1 -0
- package/dist/todos/registry.js +116 -0
- package/dist/todos/registry.js.map +1 -0
- package/dist/todos/reminderEngine.d.ts +78 -0
- package/dist/todos/reminderEngine.d.ts.map +1 -0
- package/dist/todos/reminderEngine.js +162 -0
- package/dist/todos/reminderEngine.js.map +1 -0
- package/dist/todos/store.d.ts +34 -0
- package/dist/todos/store.d.ts.map +1 -0
- package/dist/todos/store.js +208 -0
- package/dist/todos/store.js.map +1 -0
- package/dist/todos/tools.d.ts +26 -0
- package/dist/todos/tools.d.ts.map +1 -0
- package/dist/todos/tools.js +25 -0
- package/dist/todos/tools.js.map +1 -0
- package/dist/todos/types.d.ts +85 -0
- package/dist/todos/types.d.ts.map +1 -0
- package/dist/todos/types.js +30 -0
- package/dist/todos/types.js.map +1 -0
- package/dist/trackers/taskAgentLimit.d.ts +58 -0
- package/dist/trackers/taskAgentLimit.d.ts.map +1 -0
- package/dist/trackers/taskAgentLimit.js +62 -0
- package/dist/trackers/taskAgentLimit.js.map +1 -0
- package/dist/trackers/taskAgentSlots.d.ts +109 -0
- package/dist/trackers/taskAgentSlots.d.ts.map +1 -0
- package/dist/trackers/taskAgentSlots.js +227 -0
- package/dist/trackers/taskAgentSlots.js.map +1 -0
- package/dist/trackers/types.d.ts +14 -0
- package/dist/trackers/types.d.ts.map +1 -0
- package/dist/trackers/types.js +3 -0
- package/dist/trackers/types.js.map +1 -0
- package/dist/types.d.ts +80 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/adoptReplayGate.d.ts +16 -0
- package/dist/utils/adoptReplayGate.d.ts.map +1 -0
- package/dist/utils/adoptReplayGate.js +35 -0
- package/dist/utils/adoptReplayGate.js.map +1 -0
- package/dist/utils/agentEnvironment.d.ts +6 -2
- package/dist/utils/agentEnvironment.d.ts.map +1 -1
- package/dist/utils/agentEnvironment.js +13 -2
- package/dist/utils/agentEnvironment.js.map +1 -1
- package/dist/utils/agentPlugin.d.ts +113 -0
- package/dist/utils/agentPlugin.d.ts.map +1 -0
- package/dist/utils/agentPlugin.js +192 -0
- package/dist/utils/agentPlugin.js.map +1 -0
- package/dist/utils/autoContinueOnLimit.d.ts +7 -4
- package/dist/utils/autoContinueOnLimit.d.ts.map +1 -1
- package/dist/utils/autoContinueOnLimit.js +8 -5
- package/dist/utils/autoContinueOnLimit.js.map +1 -1
- package/dist/utils/botSkills.d.ts +62 -0
- package/dist/utils/botSkills.d.ts.map +1 -0
- package/dist/utils/botSkills.js +53 -0
- package/dist/utils/botSkills.js.map +1 -0
- package/dist/utils/botSkillsSetting.d.ts +7 -0
- package/dist/utils/botSkillsSetting.d.ts.map +1 -0
- package/dist/utils/botSkillsSetting.js +9 -0
- package/dist/utils/botSkillsSetting.js.map +1 -0
- package/dist/utils/claudeCodeVersion.d.ts +7 -0
- package/dist/utils/claudeCodeVersion.d.ts.map +1 -1
- package/dist/utils/claudeCodeVersion.js +14 -1
- package/dist/utils/claudeCodeVersion.js.map +1 -1
- package/dist/utils/claudeCompactHook.d.ts +33 -15
- package/dist/utils/claudeCompactHook.d.ts.map +1 -1
- package/dist/utils/claudeCompactHook.js +36 -17
- package/dist/utils/claudeCompactHook.js.map +1 -1
- package/dist/utils/claudeCompactSummary.d.ts +15 -0
- package/dist/utils/claudeCompactSummary.d.ts.map +1 -0
- package/dist/utils/claudeCompactSummary.js +96 -0
- package/dist/utils/claudeCompactSummary.js.map +1 -0
- package/dist/utils/claudeLimitSwitch.d.ts +11 -9
- package/dist/utils/claudeLimitSwitch.d.ts.map +1 -1
- package/dist/utils/claudeLimitSwitch.js +18 -16
- package/dist/utils/claudeLimitSwitch.js.map +1 -1
- package/dist/utils/claudeProfiles.d.ts +5 -0
- package/dist/utils/claudeProfiles.d.ts.map +1 -1
- package/dist/utils/claudeProfiles.js +14 -0
- package/dist/utils/claudeProfiles.js.map +1 -1
- package/dist/utils/claudeSessionTitle.d.ts +2 -1
- package/dist/utils/claudeSessionTitle.d.ts.map +1 -1
- package/dist/utils/claudeSessionTitle.js +14 -14
- package/dist/utils/claudeSessionTitle.js.map +1 -1
- package/dist/utils/claudeUsage.d.ts +95 -15
- package/dist/utils/claudeUsage.d.ts.map +1 -1
- package/dist/utils/claudeUsage.js +241 -41
- package/dist/utils/claudeUsage.js.map +1 -1
- package/dist/utils/claudeUsageRefresher.d.ts +10 -0
- package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
- package/dist/utils/claudeUsageRefresher.js +11 -0
- package/dist/utils/claudeUsageRefresher.js.map +1 -1
- package/dist/utils/claudeUserEntry.d.ts +50 -0
- package/dist/utils/claudeUserEntry.d.ts.map +1 -0
- package/dist/utils/claudeUserEntry.js +120 -0
- package/dist/utils/claudeUserEntry.js.map +1 -0
- package/dist/utils/clearThreadOutputQueues.d.ts +4 -3
- package/dist/utils/clearThreadOutputQueues.d.ts.map +1 -1
- package/dist/utils/clearThreadOutputQueues.js +1 -1
- package/dist/utils/clearThreadOutputQueues.js.map +1 -1
- package/dist/utils/compactOnIdle.d.ts +33 -13
- package/dist/utils/compactOnIdle.d.ts.map +1 -1
- package/dist/utils/compactOnIdle.js +35 -15
- package/dist/utils/compactOnIdle.js.map +1 -1
- package/dist/utils/fallbackBridge.d.ts +5 -0
- package/dist/utils/fallbackBridge.d.ts.map +1 -1
- package/dist/utils/fallbackBridge.js +19 -3
- package/dist/utils/fallbackBridge.js.map +1 -1
- package/dist/utils/fallbackChain.d.ts +9 -3
- package/dist/utils/fallbackChain.d.ts.map +1 -1
- package/dist/utils/fallbackChain.js +21 -3
- package/dist/utils/fallbackChain.js.map +1 -1
- package/dist/utils/fileSendPlan.d.ts +6 -0
- package/dist/utils/fileSendPlan.d.ts.map +1 -1
- package/dist/utils/fileSendPlan.js +11 -0
- package/dist/utils/fileSendPlan.js.map +1 -1
- package/dist/utils/fileSendService.d.ts +7 -1
- package/dist/utils/fileSendService.d.ts.map +1 -1
- package/dist/utils/fileSendService.js +6 -2
- package/dist/utils/fileSendService.js.map +1 -1
- package/dist/utils/groupFinalizePlan.d.ts +8 -10
- package/dist/utils/groupFinalizePlan.d.ts.map +1 -1
- package/dist/utils/groupFinalizePlan.js +8 -12
- package/dist/utils/groupFinalizePlan.js.map +1 -1
- package/dist/utils/jsonStreamHost.d.ts +13 -0
- package/dist/utils/jsonStreamHost.d.ts.map +1 -1
- package/dist/utils/jsonStreamHost.js +13 -0
- package/dist/utils/jsonStreamHost.js.map +1 -1
- package/dist/utils/messageSendService.d.ts +5 -3
- package/dist/utils/messageSendService.d.ts.map +1 -1
- package/dist/utils/messageSendService.js +12 -5
- package/dist/utils/messageSendService.js.map +1 -1
- package/dist/utils/openCodeCompactPlugin.d.ts +39 -14
- package/dist/utils/openCodeCompactPlugin.d.ts.map +1 -1
- package/dist/utils/openCodeCompactPlugin.js +106 -38
- package/dist/utils/openCodeCompactPlugin.js.map +1 -1
- package/dist/utils/promptDelivery.d.ts +29 -12
- package/dist/utils/promptDelivery.d.ts.map +1 -1
- package/dist/utils/promptDelivery.js +13 -5
- package/dist/utils/promptDelivery.js.map +1 -1
- package/dist/utils/runnersScreenPlan.d.ts +130 -0
- package/dist/utils/runnersScreenPlan.d.ts.map +1 -0
- package/dist/utils/runnersScreenPlan.js +257 -0
- package/dist/utils/runnersScreenPlan.js.map +1 -0
- package/dist/utils/sentKindsSummary.d.ts +10 -0
- package/dist/utils/sentKindsSummary.d.ts.map +1 -0
- package/dist/utils/sentKindsSummary.js +18 -0
- package/dist/utils/sentKindsSummary.js.map +1 -0
- package/dist/utils/telegramError.d.ts +6 -0
- package/dist/utils/telegramError.d.ts.map +1 -1
- package/dist/utils/telegramError.js +12 -0
- package/dist/utils/telegramError.js.map +1 -1
- package/dist/utils/topicView.d.ts +1 -1
- package/dist/utils/topicView.js +1 -1
- package/dist/utils/transcribeAudio.d.ts +10 -0
- package/dist/utils/transcribeAudio.d.ts.map +1 -1
- package/dist/utils/transcribeAudio.js +22 -3
- package/dist/utils/transcribeAudio.js.map +1 -1
- package/dist/utils/transcriptErrorTime.d.ts +8 -0
- package/dist/utils/transcriptErrorTime.d.ts.map +1 -0
- package/dist/utils/transcriptErrorTime.js +56 -0
- package/dist/utils/transcriptErrorTime.js.map +1 -0
- package/dist/utils/wedgeRecovery.d.ts +2 -0
- package/dist/utils/wedgeRecovery.d.ts.map +1 -1
- package/dist/utils/wedgeRecovery.js +1 -1
- package/dist/utils/wedgeRecovery.js.map +1 -1
- package/package.json +15 -4
- package/remoteRun/runner.sh +1317 -0
- package/stt/gigaam_sidecar.py +555 -50
package/dist/bot.js
CHANGED
|
@@ -46,7 +46,6 @@ const filters_1 = require("telegraf/filters");
|
|
|
46
46
|
const fs = __importStar(require("fs"));
|
|
47
47
|
const os = __importStar(require("os"));
|
|
48
48
|
const path = __importStar(require("path"));
|
|
49
|
-
const https = __importStar(require("https"));
|
|
50
49
|
const child_process_1 = require("child_process");
|
|
51
50
|
const util_1 = require("util");
|
|
52
51
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
@@ -76,6 +75,7 @@ const startupReadiness_1 = require("./utils/startupReadiness");
|
|
|
76
75
|
const doctorLine_1 = require("./utils/doctorLine");
|
|
77
76
|
const nativeWindows_1 = require("./utils/nativeWindows");
|
|
78
77
|
const state_1 = require("./state");
|
|
78
|
+
const diagLog_1 = require("./diagLog");
|
|
79
79
|
const instanceLock_1 = require("./instanceLock");
|
|
80
80
|
const shutdown_1 = require("./shutdown");
|
|
81
81
|
const updateDispatcher_1 = require("./connectors/telegram/updateDispatcher");
|
|
@@ -87,7 +87,14 @@ const timezonePicker_1 = require("./connectors/telegram/timezonePicker");
|
|
|
87
87
|
const timezoneRecompute_1 = require("./scheduler/timezoneRecompute");
|
|
88
88
|
const validation_1 = require("./validation");
|
|
89
89
|
const bindPicker_1 = require("./connectors/telegram/bindPicker");
|
|
90
|
+
const openBindMessages_1 = require("./connectors/telegram/openBindMessages");
|
|
91
|
+
const bindSweep_1 = require("./connectors/telegram/bindSweep");
|
|
90
92
|
const topicCommand_1 = require("./connectors/telegram/commands/topicCommand");
|
|
93
|
+
const callbackMessage_1 = require("./connectors/telegram/commands/callbackMessage");
|
|
94
|
+
const callbackToast_1 = require("./connectors/telegram/commands/callbackToast");
|
|
95
|
+
const bindingWelcome_1 = require("./connectors/telegram/bindingWelcome");
|
|
96
|
+
const bindPickerClaims_1 = require("./connectors/telegram/bindPickerClaims");
|
|
97
|
+
const pendingTopicName_1 = require("./connectors/telegram/pendingTopicName");
|
|
91
98
|
const forumTopicCreated_1 = require("./connectors/telegram/forumTopicCreated");
|
|
92
99
|
const providerDisconnectPlan_1 = require("./utils/providerDisconnectPlan");
|
|
93
100
|
const folderName_1 = require("./folderName");
|
|
@@ -101,10 +108,13 @@ const outbound_3 = require("./connectors/jira/outbound");
|
|
|
101
108
|
const adapterEventRouting_1 = require("./adapters/adapterEventRouting");
|
|
102
109
|
const connectorSet_1 = require("./platform/connectorSet");
|
|
103
110
|
const telegramCallGuard_1 = require("./connectors/telegram/telegramCallGuard");
|
|
111
|
+
const botApiConnection_1 = require("./connectors/telegram/botApiConnection");
|
|
104
112
|
const unservedStateGuard_1 = require("./platform/unservedStateGuard");
|
|
105
113
|
const contextPreamble_1 = require("./connectors/jira/contextPreamble");
|
|
106
114
|
const postToSession_1 = require("./postToSession");
|
|
107
115
|
const limitHeldPrompts_1 = require("./utils/limitHeldPrompts");
|
|
116
|
+
const adoptReplayGate_1 = require("./utils/adoptReplayGate");
|
|
117
|
+
const topicRequestSettle_1 = require("./requests/topicRequestSettle");
|
|
108
118
|
const promptDelivery_1 = require("./utils/promptDelivery");
|
|
109
119
|
const apiRetryKick_1 = require("./apiRetryKick");
|
|
110
120
|
const limitHeldPromptQueue_1 = require("./utils/limitHeldPromptQueue");
|
|
@@ -164,6 +174,9 @@ const claudeLivenessAction_1 = require("./utils/claudeLivenessAction");
|
|
|
164
174
|
const isoTimestamp_1 = require("./utils/isoTimestamp");
|
|
165
175
|
const effortLevels_1 = require("./effortLevels");
|
|
166
176
|
const instanceSettings_1 = require("./instanceSettings");
|
|
177
|
+
const taskAgentLimit_1 = require("./trackers/taskAgentLimit");
|
|
178
|
+
const taskAgentSlots_1 = require("./trackers/taskAgentSlots");
|
|
179
|
+
const botSkills_1 = require("./utils/botSkills");
|
|
167
180
|
const constants_1 = require("./selfUpdate/constants");
|
|
168
181
|
const launchEnvironment_1 = require("./selfUpdate/launchEnvironment");
|
|
169
182
|
const restart_1 = require("./selfUpdate/restart");
|
|
@@ -183,11 +196,20 @@ const runLedger_1 = require("./scheduler/runLedger");
|
|
|
183
196
|
const delivery_1 = require("./scheduler/delivery");
|
|
184
197
|
const checkRun_1 = require("./scheduler/checkRun");
|
|
185
198
|
const engine_1 = require("./scheduler/engine");
|
|
199
|
+
const recurrence_1 = require("./scheduler/recurrence");
|
|
186
200
|
const mcpSurface_1 = require("./scheduler/mcpSurface");
|
|
187
201
|
const injection_1 = require("./scheduler/injection");
|
|
188
202
|
const mcpBoot_1 = require("./scheduler/mcpBoot");
|
|
189
203
|
const requestLedger_1 = require("./requests/requestLedger");
|
|
190
204
|
const answerRequest_1 = require("./requests/answerRequest");
|
|
205
|
+
const store_1 = require("./todos/store");
|
|
206
|
+
const registry_1 = require("./todos/registry");
|
|
207
|
+
const nag_1 = require("./todos/nag");
|
|
208
|
+
const reminderEngine_1 = require("./todos/reminderEngine");
|
|
209
|
+
const projects_1 = require("./todos/projects");
|
|
210
|
+
const board_1 = require("./todos/board");
|
|
211
|
+
const tools_1 = require("./todos/tools");
|
|
212
|
+
const midTurn_1 = require("./todos/midTurn");
|
|
191
213
|
const answerTail_1 = require("./requests/answerTail");
|
|
192
214
|
const answerSink_1 = require("./connectors/telegram/answerSink");
|
|
193
215
|
const answerSink_2 = require("./platform/answerSink");
|
|
@@ -204,6 +226,7 @@ const apiErrorRetry_1 = require("./apiErrorRetry");
|
|
|
204
226
|
const wedgeRecovery_1 = require("./utils/wedgeRecovery");
|
|
205
227
|
const claudeAuthLogin_1 = require("./utils/claudeAuthLogin");
|
|
206
228
|
const limitEpisodeRecovery_1 = require("./utils/limitEpisodeRecovery");
|
|
229
|
+
const transcriptErrorTime_1 = require("./utils/transcriptErrorTime");
|
|
207
230
|
const autoContinueOnLimit_1 = require("./utils/autoContinueOnLimit");
|
|
208
231
|
const openCodeAuthLogin_1 = require("./utils/openCodeAuthLogin");
|
|
209
232
|
const agentLogin_1 = require("./agentLogin/agentLogin");
|
|
@@ -211,9 +234,19 @@ const questionKeyboards_1 = require("./connectors/telegram/questionKeyboards");
|
|
|
211
234
|
const displayModes_1 = require("./connectors/telegram/commands/displayModes");
|
|
212
235
|
const reminders_1 = require("./connectors/telegram/commands/reminders");
|
|
213
236
|
const modelProviders_1 = require("./connectors/telegram/commands/modelProviders");
|
|
214
|
-
const
|
|
237
|
+
const callbackToast_2 = require("./connectors/telegram/commands/callbackToast");
|
|
215
238
|
const compaction_1 = require("./connectors/telegram/commands/compaction");
|
|
216
239
|
const autoContinueLimits_1 = require("./connectors/telegram/commands/autoContinueLimits");
|
|
240
|
+
const botSkills_2 = require("./connectors/telegram/commands/botSkills");
|
|
241
|
+
const taskAgents_1 = require("./connectors/telegram/commands/taskAgents");
|
|
242
|
+
const runners_1 = require("./connectors/telegram/commands/runners");
|
|
243
|
+
const gateWrapper_1 = require("./runners/gateWrapper");
|
|
244
|
+
const hubAddress_1 = require("./runners/hubAddress");
|
|
245
|
+
const hubAddressCandidates_1 = require("./runners/hubAddressCandidates");
|
|
246
|
+
const hubPaths_1 = require("./runners/hubPaths");
|
|
247
|
+
const hubPairing_1 = require("./runners/hubPairing");
|
|
248
|
+
const runnersOverview_1 = require("./runners/runnersOverview");
|
|
249
|
+
const ssh_1 = require("./remoteRun/ssh");
|
|
217
250
|
const claudeAccounts_1 = require("./connectors/telegram/commands/claudeAccounts");
|
|
218
251
|
const claudeProfiles_1 = require("./utils/claudeProfiles");
|
|
219
252
|
const claudeUsage_1 = require("./utils/claudeUsage");
|
|
@@ -229,6 +262,7 @@ const fallbackChain_1 = require("./utils/fallbackChain");
|
|
|
229
262
|
const fallbackBridge_1 = require("./utils/fallbackBridge");
|
|
230
263
|
const fallbackChain_2 = require("./connectors/telegram/commands/fallbackChain");
|
|
231
264
|
const limitsBoard_1 = require("./connectors/telegram/commands/limitsBoard");
|
|
265
|
+
const todo_1 = require("./connectors/telegram/commands/todo");
|
|
232
266
|
const backendSwitch_1 = require("./connectors/telegram/commands/backendSwitch");
|
|
233
267
|
const operatorSwitchPlan_1 = require("./utils/operatorSwitchPlan");
|
|
234
268
|
const backendSwitchPlan_1 = require("./utils/backendSwitchPlan");
|
|
@@ -237,6 +271,7 @@ const stt_1 = require("./connectors/telegram/commands/stt");
|
|
|
237
271
|
const claudeApiSpend_1 = require("./utils/claudeApiSpend");
|
|
238
272
|
const claudeProfileUsageLines_1 = require("./utils/claudeProfileUsageLines");
|
|
239
273
|
const resolveBinary_1 = require("./utils/resolveBinary");
|
|
274
|
+
const runnerSummary_1 = require("./remoteRun/runnerSummary");
|
|
240
275
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
241
276
|
// ENV parsing & fatal validation
|
|
242
277
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -354,6 +389,8 @@ if (ENV.testTimings.telegramSendIntervalMs !== undefined)
|
|
|
354
389
|
(0, rateLimiter_1.configureGlobalSendInterval)(ENV.testTimings.telegramSendIntervalMs);
|
|
355
390
|
if (ENV.testTimings.backgroundHoldMaxMs !== undefined)
|
|
356
391
|
(0, createAdapter_1.registerPendingWorkHoldMs)(ENV.testTimings.backgroundHoldMaxMs);
|
|
392
|
+
if (ENV.testTimings.scheduleMinFireIntervalMs !== undefined)
|
|
393
|
+
(0, recurrence_1.registerMinFireIntervalMs)(ENV.testTimings.scheduleMinFireIntervalMs);
|
|
357
394
|
/** The short retry that probes an unconfirmed usage limit (a test instance may shorten it). */
|
|
358
395
|
const limitProbeDelayMs = ENV.testTimings.limitProbeDelayMs ?? apiErrorRetry_1.unconfirmedLimitProbeDelayMs;
|
|
359
396
|
// The instance default effort (`/effort` in General) is `settings.json`'s `defaultEffort`, re-read when the file changes.
|
|
@@ -446,24 +483,22 @@ function getOutputTransport() {
|
|
|
446
483
|
}
|
|
447
484
|
return outputTransport;
|
|
448
485
|
}
|
|
449
|
-
const telegramAgent =
|
|
450
|
-
keepAlive: true,
|
|
451
|
-
keepAliveMsecs: 10000,
|
|
452
|
-
family: 4,
|
|
453
|
-
});
|
|
486
|
+
const telegramAgent = (0, botApiConnection_1.createBotApiHttpsAgent)();
|
|
454
487
|
// Without the telegram connector there is no token (an empty one), and the call
|
|
455
488
|
// guard below refuses every Bot API call before it is sent (Jira plan J3, D9).
|
|
456
489
|
// `TELEGRAM_API_ROOT` points the client at another Bot API host — the
|
|
457
490
|
// process-level test's fake on loopback; unset is the real `api.telegram.org`.
|
|
458
|
-
// Telegraf drops the https agent itself for an `http://` root.
|
|
459
491
|
const telegramApiRoot = process.env.TELEGRAM_API_ROOT?.trim();
|
|
492
|
+
// Telegraf drops the https agent itself for an `http://` root and node-fetch then rides the global agent, whose
|
|
493
|
+
// pooling is not the bot's. That root gets the same pooling over `http`.
|
|
494
|
+
const telegramApiAgent = telegramApiRoot?.startsWith('http://') ? (0, botApiConnection_1.createBotApiHttpAgent)() : telegramAgent;
|
|
460
495
|
const bot = new telegraf_1.Telegraf(ENV.botToken, {
|
|
461
|
-
telegram: { agent:
|
|
496
|
+
telegram: { agent: telegramApiAgent, ...(telegramApiRoot ? { apiRoot: telegramApiRoot } : {}) },
|
|
462
497
|
});
|
|
463
498
|
// Decouple the long-polling intake/ACK from per-handler latency: wrap
|
|
464
499
|
// `bot.handleUpdate` so each update is enqueued into its thread's serial queue
|
|
465
500
|
// and the loop's `Promise.all` resolves at once (next `getUpdates` = ACK + new
|
|
466
|
-
// batch fires now). The real middleware chain —
|
|
501
|
+
// batch fires now). The real middleware chain — every `bot.use` middleware,
|
|
467
502
|
// every command/`on`/action, `bot.catch` — runs unchanged inside the captured
|
|
468
503
|
// original, just off-loop and per-thread-serial. MUST be installed before
|
|
469
504
|
// `bot.launch`. The graceful-shutdown path drains it (see `updateDrainTimeoutMs`).
|
|
@@ -481,6 +516,10 @@ const updateDrainTimeoutMs = 2000;
|
|
|
481
516
|
// window is not lost on shutdown.
|
|
482
517
|
(0, outputTrace_1.installCallApiTrace)(bot.telegram);
|
|
483
518
|
process.on('exit', outputTrace_1.flushTraceBufferSyncOnExit);
|
|
519
|
+
// The bot's pooling (the agent Telegraf dropped for an `http://` root) and the retry of a request that met a pooled
|
|
520
|
+
// connection the server had closed. AFTER the trace wrap, so the trace records both real HTTP attempts, and below the
|
|
521
|
+
// send queue, pacer and 429 retry. Telegraf builds a NEW client per update: see the first `bot.use` below.
|
|
522
|
+
(0, botApiConnection_1.wireBotApiClient)(bot.telegram, telegramApiAgent);
|
|
484
523
|
// Default every text message/edit to NO link preview: a bare URL in agent
|
|
485
524
|
// output otherwise expanded into a large preview card, one per message, in the
|
|
486
525
|
// muted topic (operator complaint 2026-09-07). Installed OUTSIDE the trace wrap
|
|
@@ -601,7 +640,14 @@ async function checkIsForumAdmin(chatId, userId) {
|
|
|
601
640
|
return false;
|
|
602
641
|
}
|
|
603
642
|
}
|
|
604
|
-
//
|
|
643
|
+
// Telegraf builds a NEW `Telegram` client for every update (`ctx.telegram`, behind every `ctx.reply`, `ctx.answerCbQuery`
|
|
644
|
+
// and `ctx.editMessageText`) from `bot.telegram.options`, so the wiring of `bot.telegram` does not reach it and an
|
|
645
|
+
// `http://` root drops the agent again. Wire it before any handler can use it.
|
|
646
|
+
bot.use((ctx, next) => {
|
|
647
|
+
(0, botApiConnection_1.wireBotApiClient)(ctx.telegram, telegramApiAgent);
|
|
648
|
+
return next();
|
|
649
|
+
});
|
|
650
|
+
// Output-trace `recv` hook — must come before every middleware that can gate or drop an update, so every update is
|
|
605
651
|
// recorded before any gating (access control, group routing) can drop it.
|
|
606
652
|
// Security: while the thread awaits a `/connect` key paste (same
|
|
607
653
|
// `pendingProviderConnects` state the text handler consumes later in this
|
|
@@ -788,7 +834,7 @@ let schedulerEngine = null;
|
|
|
788
834
|
* The request wake-up engine (request/answer core S4), assigned once by
|
|
789
835
|
* `startBot` after the request ledger loads. Module-level like
|
|
790
836
|
* {@link schedulerEngine} because the cancel paths (`/esc`, `/c`, `/quit`,
|
|
791
|
-
* `/
|
|
837
|
+
* `/quitall`, `/new`, `/resume`, leaving the folder, switching the topic to
|
|
792
838
|
* another agent or a shell) are module-level command handlers.
|
|
793
839
|
*/
|
|
794
840
|
let requestWakeUpEngine = null;
|
|
@@ -820,15 +866,19 @@ async function openTopicRequest(key, text, intake) {
|
|
|
820
866
|
return { header: (0, topicRequest_1.buildTopicRequestHeader)(request.id, intake.source, view, request.supersededRequestIds), requestId: request.id };
|
|
821
867
|
}
|
|
822
868
|
/**
|
|
823
|
-
* @description
|
|
824
|
-
*
|
|
825
|
-
*
|
|
869
|
+
* @description Where a topic request goes once its prompt came back from the prompt path (`requests/topicRequestSettle.ts`): the
|
|
870
|
+
* wake-up engine watches the turn of a prompt that reached a session (or waits behind its start), and a prompt that found no
|
|
871
|
+
* session closes the request it opened. Both ports are read at call time: the engine and the ledger are assigned at boot.
|
|
826
872
|
*/
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
873
|
+
const { trackTurn: trackTopicRequestTurn, settleAfterDelivery: settleTopicRequestAfterDelivery } = (0, topicRequestSettle_1.createTopicRequestSettler)({
|
|
874
|
+
trackTurn: async (key, requestId) => {
|
|
875
|
+
await requestWakeUpEngine?.trackForwardedTurn(key, requestId, { isRequestPrompt: true });
|
|
876
|
+
},
|
|
877
|
+
cancelRequest: async (requestId) => {
|
|
878
|
+
await topicRequestLedger?.closeRequest(requestId, 'cancelled');
|
|
879
|
+
},
|
|
880
|
+
warn: (message) => console.warn(message),
|
|
881
|
+
});
|
|
832
882
|
/** What the bot's own limit-wait answer needs (S5); set at boot next to the engine. */
|
|
833
883
|
let requestLimitWaitAnswerDeps = null;
|
|
834
884
|
/**
|
|
@@ -850,6 +900,212 @@ function checkIsAgentEventShownInTopic(key, event) {
|
|
|
850
900
|
function getDataDir() {
|
|
851
901
|
return path.dirname(state.stateFilePath);
|
|
852
902
|
}
|
|
903
|
+
/** The shared todo registry (`todos/`), built on first use: its store lives in `DATA_DIR`, known once the state is. */
|
|
904
|
+
let todoRegistry = null;
|
|
905
|
+
function getTodoRegistry() {
|
|
906
|
+
todoRegistry ??= (0, registry_1.createTodoRegistry)({ store: new store_1.TodoStore(getDataDir()), getNowMs: Date.now });
|
|
907
|
+
return todoRegistry;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* The «Bot skills» switch (`/bot_skills`, `utils/botSkills.ts`): the topic's own choice, else General's value, else `settings.json`'s
|
|
911
|
+
* `botSkills` (re-read when the file changes), else ON. `state` is read when asked, so the reader is registered once it is loaded.
|
|
912
|
+
*/
|
|
913
|
+
const botSkillsSources = {
|
|
914
|
+
getGeneralValue: () => state.getBotSkillsGlobalValue(),
|
|
915
|
+
getTopicOverride: (key) => state.getBotSkillsOverride(key),
|
|
916
|
+
getSettingsValue: (0, instanceSettings_1.createBotSkillsSettingsReader)(() => (0, instanceSettings_1.getInstanceSettingsPath)(), (line) => console.error(line)),
|
|
917
|
+
};
|
|
918
|
+
const botSkillsReader = (0, botSkills_1.createBotSkillsReader)(botSkillsSources);
|
|
919
|
+
/**
|
|
920
|
+
* The cap on tracker task agents (`trackers/taskAgentLimit.ts`): General's value, else `settings.json`'s
|
|
921
|
+
* `taskAgents.maxConcurrent` (re-read when the file changes), else the built-in default. `state` is read when asked.
|
|
922
|
+
*/
|
|
923
|
+
const taskAgentLimitSources = {
|
|
924
|
+
getGeneralValue: () => state.getTaskAgentsMaxConcurrentGlobalValue(),
|
|
925
|
+
getSettingsValue: (0, instanceSettings_1.createTaskAgentsMaxConcurrentReader)(() => (0, instanceSettings_1.getInstanceSettingsPath)(), (line) => console.error(line)),
|
|
926
|
+
};
|
|
927
|
+
/** The slots of the tracker task agents and their wait list; built at boot, once the request ledger is loaded. */
|
|
928
|
+
let taskAgentSlots = null;
|
|
929
|
+
/** `settings.json`'s `todo` (on / off, the reminder's interval), re-read when the file changes. */
|
|
930
|
+
const readTodoSettings = (0, instanceSettings_1.createTodoSettingsReader)(() => (0, instanceSettings_1.getInstanceSettingsPath)(), (line) => console.error(line));
|
|
931
|
+
/** The staleness reminder's interval now; `null` = off (the feature or its reminder). */
|
|
932
|
+
function getTodoReminderIntervalMsNow() {
|
|
933
|
+
return (0, nag_1.getTodoReminderIntervalMs)(readTodoSettings(), ENV.testTimings.todoReminderIntervalMs);
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* The reminder line a prompt forwarded now carries (D11 form a), or `null`; taking it counts as delivered. The lists
|
|
937
|
+
* are a courtesy to the agent: a failure here is logged and the prompt goes on without the line.
|
|
938
|
+
*/
|
|
939
|
+
function takeTodoPromptReminder(key) {
|
|
940
|
+
try {
|
|
941
|
+
return getTodoRegistry().takePromptReminder(key, getTodoReminderIntervalMsNow(), Date.now());
|
|
942
|
+
}
|
|
943
|
+
catch (e) {
|
|
944
|
+
console.error(`[todos] ${(0, sessionKey_1.keyToString)(key)}: the reminder line could not be taken:`, e instanceof Error ? e.message : e);
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
/** The staleness reminder's poll (`todos/reminderEngine.ts`), built at boot and started once the sessions are restored. */
|
|
949
|
+
let todoReminderEngine = null;
|
|
950
|
+
let todoRepositories = null;
|
|
951
|
+
/** Which folders are one git repository (`todos/projects.ts`), cached; the root commits persist in `DATA_DIR/todos`. */
|
|
952
|
+
function getTodoRepositories() {
|
|
953
|
+
todoRepositories ??= (0, projects_1.createRepositoryIdentityReader)({
|
|
954
|
+
runGit: projects_1.runGitInFolder,
|
|
955
|
+
rootCommitStore: (0, projects_1.createRepositoryRootCommitFile)((0, store_1.getTodosDir)(getDataDir())),
|
|
956
|
+
});
|
|
957
|
+
return todoRepositories;
|
|
958
|
+
}
|
|
959
|
+
/** What the boards need of the Jira connector (D55, D57); set at boot when it is loaded. */
|
|
960
|
+
let todoJiraConversations = null;
|
|
961
|
+
/**
|
|
962
|
+
* A bound conversation as the project grouping sees it before git is asked: complete (a Jira issue, a folder that does
|
|
963
|
+
* not resolve), or still needing its folder's repository.
|
|
964
|
+
*/
|
|
965
|
+
function describeTodoProjectMember({ key, data }) {
|
|
966
|
+
const conversationKey = (0, sessionKey_1.keyToString)(key);
|
|
967
|
+
if (key.platform === 'jira') {
|
|
968
|
+
return { member: { conversationKey, workDir: null, identity: null, jiraRepositoryNames: todoJiraConversations?.getConversationRepositoryNames(key) ?? null } };
|
|
969
|
+
}
|
|
970
|
+
const decision = (0, validation_1.resolveBoundWorkDir)(ENV.workRoot, data);
|
|
971
|
+
return decision.kind === 'proceed' ? { conversationKey, workDir: decision.workDir } : { member: { conversationKey, workDir: null, identity: null } };
|
|
972
|
+
}
|
|
973
|
+
/** The instance's project boards (D15, D16, D57), waiting for git where a folder's read is due. */
|
|
974
|
+
async function listTodoProjects() {
|
|
975
|
+
const repositories = getTodoRepositories();
|
|
976
|
+
const members = await Promise.all(state.listBindings().map(async (binding) => {
|
|
977
|
+
const described = describeTodoProjectMember(binding);
|
|
978
|
+
if ('member' in described)
|
|
979
|
+
return described.member;
|
|
980
|
+
const identity = await repositories.read(described.workDir);
|
|
981
|
+
return identity === undefined ? undefined : { ...described, identity };
|
|
982
|
+
}));
|
|
983
|
+
return (0, projects_1.groupTodoProjects)(members.filter((member) => member !== undefined), state.getTodoProjectOverrides());
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* The project boards as known NOW, never waiting for git and never yielding (a prompt, the mid-turn poll): `null` while a
|
|
987
|
+
* bound folder was never read — its read starts now, with every other one due.
|
|
988
|
+
*/
|
|
989
|
+
function peekTodoProjects() {
|
|
990
|
+
const repositories = getTodoRepositories();
|
|
991
|
+
const members = state.listBindings().map((binding) => {
|
|
992
|
+
const described = describeTodoProjectMember(binding);
|
|
993
|
+
if ('member' in described)
|
|
994
|
+
return described.member;
|
|
995
|
+
const identity = repositories.peek(described.workDir);
|
|
996
|
+
return identity === undefined ? undefined : { ...described, identity };
|
|
997
|
+
});
|
|
998
|
+
return members.every((member) => member !== undefined) ? (0, projects_1.groupTodoProjects)(members, state.getTodoProjectOverrides()) : null;
|
|
999
|
+
}
|
|
1000
|
+
/** The bound folders' due repository reads start in the background (boot, a bind), so the next prompts find the grouping known. */
|
|
1001
|
+
function warmTodoProjects() {
|
|
1002
|
+
if (!readTodoSettings().isEnabled)
|
|
1003
|
+
return;
|
|
1004
|
+
try {
|
|
1005
|
+
peekTodoProjects();
|
|
1006
|
+
}
|
|
1007
|
+
catch (e) {
|
|
1008
|
+
console.error('[todos] the bound folders\' repositories could not be read:', e instanceof Error ? e.message : e);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
/** The project of `key`'s conversation, waiting for git; `null` = in none. */
|
|
1012
|
+
async function getTodoProjectOf(key) {
|
|
1013
|
+
const conversationKey = (0, sessionKey_1.keyToString)(key);
|
|
1014
|
+
return (await listTodoProjects()).find((project) => project.memberKeys.includes(conversationKey)) ?? null;
|
|
1015
|
+
}
|
|
1016
|
+
/** The project of `key`'s conversation as known now; `null` = in none, `undefined` = not known without waiting. */
|
|
1017
|
+
function peekTodoProjectOf(key) {
|
|
1018
|
+
const conversationKey = (0, sessionKey_1.keyToString)(key);
|
|
1019
|
+
const projects = peekTodoProjects();
|
|
1020
|
+
return projects === null ? undefined : (projects.find((project) => project.memberKeys.includes(conversationKey)) ?? null);
|
|
1021
|
+
}
|
|
1022
|
+
/** How a conversation is named on a board and in the picker: `#topic (folder)`, or the issue `PROJ-123 «summary»` (D55). */
|
|
1023
|
+
function getTodoConversationLabel(conversationKey) {
|
|
1024
|
+
const key = (0, sessionKey_1.tryKeyFromString)(conversationKey);
|
|
1025
|
+
if (key === null)
|
|
1026
|
+
return conversationKey;
|
|
1027
|
+
if (key.platform === 'jira') {
|
|
1028
|
+
const title = todoJiraConversations?.getConversationTitle(key) ?? null;
|
|
1029
|
+
return title === null ? key.thread : `${key.thread} «${title}»`;
|
|
1030
|
+
}
|
|
1031
|
+
const binding = state.getBinding(key);
|
|
1032
|
+
return `#${binding?.topicName ?? `topic ${key.thread}`}${binding ? ` (${binding.subdir})` : ''}`;
|
|
1033
|
+
}
|
|
1034
|
+
function getTodoSessionActivity(conversationKey) {
|
|
1035
|
+
const key = (0, sessionKey_1.tryKeyFromString)(conversationKey);
|
|
1036
|
+
if (key === null)
|
|
1037
|
+
return 'asleep';
|
|
1038
|
+
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
1039
|
+
if (!adapter.checkIsActive(key))
|
|
1040
|
+
return 'asleep';
|
|
1041
|
+
return adapter.checkIsBusy?.(key) ? 'working' : 'idle';
|
|
1042
|
+
}
|
|
1043
|
+
let todoBoard = null;
|
|
1044
|
+
/** The project boards (`todos/board.ts`), built on first use like the registry. */
|
|
1045
|
+
function getTodoBoard() {
|
|
1046
|
+
todoBoard ??= (0, board_1.createTodoBoard)({
|
|
1047
|
+
store: getTodoRegistry().getStore(),
|
|
1048
|
+
getProjectOf: getTodoProjectOf,
|
|
1049
|
+
peekProjectOf: peekTodoProjectOf,
|
|
1050
|
+
getPeerLabel: getTodoConversationLabel,
|
|
1051
|
+
getSessionActivity: getTodoSessionActivity,
|
|
1052
|
+
getNowMs: Date.now,
|
|
1053
|
+
});
|
|
1054
|
+
return todoBoard;
|
|
1055
|
+
}
|
|
1056
|
+
/** The agent lost its context: its next prompt carries the whole board (D20). Logged, never thrown, on a lifecycle path. */
|
|
1057
|
+
function noteTodoFreshContext(key) {
|
|
1058
|
+
try {
|
|
1059
|
+
getTodoBoard().noteFreshContext(key);
|
|
1060
|
+
}
|
|
1061
|
+
catch (e) {
|
|
1062
|
+
console.error(`[todos] ${(0, sessionKey_1.keyToString)(key)}: the board's seen state could not be reset:`, e instanceof Error ? e.message : e);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
let todoMidTurn = null;
|
|
1066
|
+
/** Mid-turn delivery of the board (`todos/midTurn.ts`): the Claude inbox hook, the note into a running OpenCode turn. */
|
|
1067
|
+
function getTodoMidTurn() {
|
|
1068
|
+
todoMidTurn ??= (0, midTurn_1.createTodoMidTurn)({
|
|
1069
|
+
board: getTodoBoard(),
|
|
1070
|
+
inboxDir: (0, midTurn_1.getTodoInboxDir)(getDataDir()),
|
|
1071
|
+
getClaudeSessionId: (key) => {
|
|
1072
|
+
const adapterName = (0, createAdapter_1.getThreadAdapterNameRaw)(key) ?? state.getAgent(key)?.name;
|
|
1073
|
+
return adapterName !== undefined && (0, createAdapter_1.checkIsClaudeBackend)(adapterName) ? getResumableSessionId(key) : null;
|
|
1074
|
+
},
|
|
1075
|
+
// Into the running turn only (D21): never a new turn, never through the prompt path that resets the turn.
|
|
1076
|
+
postRunningTurnNote: async (key, text) => (0, createAdapter_1.getThreadAdapter)(key).queueNoteIntoRunningTurn?.(key, text) ?? false,
|
|
1077
|
+
});
|
|
1078
|
+
return todoMidTurn;
|
|
1079
|
+
}
|
|
1080
|
+
let todoTools = null;
|
|
1081
|
+
function getTodoTools() {
|
|
1082
|
+
todoTools ??= (0, tools_1.createTodoTools)({ registry: getTodoRegistry(), board: getTodoBoard(), onBoardShown: discardTodoInbox });
|
|
1083
|
+
return todoTools;
|
|
1084
|
+
}
|
|
1085
|
+
/** A turn start the bot makes carries the board itself: a waiting mid-turn inbox is withdrawn (or counted, if taken). */
|
|
1086
|
+
function discardTodoInbox(key) {
|
|
1087
|
+
try {
|
|
1088
|
+
getTodoMidTurn().discardInbox(key);
|
|
1089
|
+
}
|
|
1090
|
+
catch (e) {
|
|
1091
|
+
console.error(`[todos] ${(0, sessionKey_1.keyToString)(key)}: the mid-turn inbox could not be withdrawn:`, e instanceof Error ? e.message : e);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* The board block a prompt forwarded now carries (D20), or `null`; taking it counts as seen. A failure never stops the prompt.
|
|
1096
|
+
* Synchronous: it sits between the forward's decision and the write (see `TodoBoard.takePromptBoard`).
|
|
1097
|
+
*/
|
|
1098
|
+
function takeTodoPromptBoard(key) {
|
|
1099
|
+
if (!readTodoSettings().isEnabled)
|
|
1100
|
+
return null;
|
|
1101
|
+
try {
|
|
1102
|
+
return getTodoBoard().takePromptBoard(key);
|
|
1103
|
+
}
|
|
1104
|
+
catch (e) {
|
|
1105
|
+
console.error(`[todos] ${(0, sessionKey_1.keyToString)(key)}: the project board could not be built:`, e instanceof Error ? e.message : e);
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
853
1109
|
const threadMessageStates = new Map();
|
|
854
1110
|
const outputQueues = new Map();
|
|
855
1111
|
const pendingQuestions = new Map();
|
|
@@ -1043,10 +1299,12 @@ function onThreadActivityWhileQuestionPending(key) {
|
|
|
1043
1299
|
}
|
|
1044
1300
|
/**
|
|
1045
1301
|
* @description Re-post the current pending question as the newest thread message
|
|
1046
|
-
* (S3):
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1302
|
+
* (S3): send the current question again via {@link postPendingQuestionAt} (which
|
|
1303
|
+
* re-renders with S1 descriptions / S2 current index and re-owns `messageId`), then
|
|
1304
|
+
* end the old message. The ORIGINAL message of the question is its record: it is closed as «asked again
|
|
1305
|
+
* below» (text stays, buttons go) and never deleted. A copy that an earlier re-post made is only a duplicate of that record
|
|
1306
|
+
* and is deleted, so a question that waits through a long stream of output leaves two messages, not a pile. No-op if the
|
|
1307
|
+
* question was answered meanwhile, or if it is already the last message (the loop guard re-checked at
|
|
1050
1308
|
* fire time, since the debounce window may have closed the gap).
|
|
1051
1309
|
*/
|
|
1052
1310
|
async function repostPendingQuestionToBottom(key) {
|
|
@@ -1067,12 +1325,77 @@ async function repostPendingQuestionToBottom(key) {
|
|
|
1067
1325
|
if (s.isPostInFlight)
|
|
1068
1326
|
return;
|
|
1069
1327
|
const oldMessageId = pending.messageId;
|
|
1070
|
-
|
|
1328
|
+
const oldQuestion = pending.data.questions[pending.currentIndex];
|
|
1329
|
+
// The first message of the question is its record. A state that predates the mark has none: the message found is the
|
|
1330
|
+
// original then, which is never deleted.
|
|
1331
|
+
if (oldMessageId !== null)
|
|
1332
|
+
pending.originalMessageId ??= oldMessageId;
|
|
1333
|
+
await postPendingQuestionAt(key);
|
|
1334
|
+
// The old message is touched only once there IS a copy below. A post that failed leaves the question where it was (its
|
|
1335
|
+
// message id unchanged) and still answerable there.
|
|
1336
|
+
const newMessageId = pendingQuestions.get(kStr)?.messageId ?? null;
|
|
1337
|
+
if (oldMessageId === null || !oldQuestion || newMessageId === null || newMessageId === oldMessageId)
|
|
1338
|
+
return;
|
|
1339
|
+
if (oldMessageId === pending.originalMessageId) {
|
|
1340
|
+
// The original: it stays as the record of the question, saying where the live copy is.
|
|
1341
|
+
await closeQuestionMessage(key, oldMessageId, oldQuestion, 'askedAgain');
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
// An earlier copy: the record already tells the question, so this one only duplicates it.
|
|
1071
1345
|
await deleteThreadMessage(key, oldMessageId);
|
|
1072
1346
|
}
|
|
1073
|
-
await postPendingQuestionAt(key);
|
|
1074
1347
|
});
|
|
1075
1348
|
}
|
|
1349
|
+
/**
|
|
1350
|
+
* @description The note a question's message ends with, by how it ended (see {@link QuestionClosing}). An answer's
|
|
1351
|
+
* note is built where the chosen label is known (`agent.question_answered_note`).
|
|
1352
|
+
*/
|
|
1353
|
+
const questionClosingNoteKeys = {
|
|
1354
|
+
unanswered: 'agent.question_unanswered_note',
|
|
1355
|
+
askedAgain: 'agent.question_asked_again_note',
|
|
1356
|
+
};
|
|
1357
|
+
/**
|
|
1358
|
+
* @description End a question's message in the topic WITHOUT deleting it: the buttons are replaced by `note`, and the
|
|
1359
|
+
* header, the question and its options stay, so what the agent asked and how it ended can still be read. Resolves
|
|
1360
|
+
* whether the text edit landed. Every way a question ends goes through this — a delete would take the record away.
|
|
1361
|
+
*
|
|
1362
|
+
* Telegram that cannot parse the Markdown render gets the plain one (no escape marks shown), and a question too long for
|
|
1363
|
+
* the note gets a shortened plain one. An edit that still fails (the message gone, say) leaves the old text, and the
|
|
1364
|
+
* buttons would stay on it dead — so they are taken off by themselves.
|
|
1365
|
+
*/
|
|
1366
|
+
async function editQuestionMessageToClosed(key, messageId, question, note) {
|
|
1367
|
+
const markdownText = (0, openCodeQuestionFlow_1.buildClosedQuestionText)(question, note, escapeMarkdown);
|
|
1368
|
+
const plainText = (0, openCodeQuestionFlow_1.buildClosedQuestionPlainText)(question, note, messageSplit_1.MAX_MESSAGE_LEN);
|
|
1369
|
+
// A question that filled a message when it was asked has no room for the note under it, and Telegram refuses the text:
|
|
1370
|
+
// the note is the result, so the body gives way (descriptions first) and goes out as plain text, which cutting cannot break.
|
|
1371
|
+
const isEdited = markdownText.length <= messageSplit_1.MAX_MESSAGE_LEN
|
|
1372
|
+
? await editThreadMessage(key, messageId, markdownText, { parse_mode: 'Markdown' }, plainText)
|
|
1373
|
+
: await editThreadMessage(key, messageId, plainText);
|
|
1374
|
+
if (!isEdited)
|
|
1375
|
+
await editThreadKeyboard(key, messageId, null, 'questionClose');
|
|
1376
|
+
return isEdited;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* {@link editQuestionMessageToClosed} for a question that ended some way ({@link QuestionEnd}). The note speaks the topic's
|
|
1380
|
+
* language whatever called it — a boot or a session teardown runs in no request, so no locale is set by one.
|
|
1381
|
+
*/
|
|
1382
|
+
async function closeQuestionMessage(key, messageId, question, ending) {
|
|
1383
|
+
const note = withThreadLocale(key, () => (typeof ending === 'string' ? (0, i18n_1.t)(questionClosingNoteKeys[ending]) : (0, i18n_1.t)('agent.question_answered_note', { answer: ending.answer })));
|
|
1384
|
+
return editQuestionMessageToClosed(key, messageId, question, note);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* @description End a question message the bot holds no record of — a copy a restart left while its re-ask was being sent, an
|
|
1388
|
+
* older copy whose close Telegram refused — when somebody taps its buttons: its own text stays (`messageText`, as the tap
|
|
1389
|
+
* delivered it) and «not answered» takes the place of the buttons, so the question does not end with no result at all. Without
|
|
1390
|
+
* the text, or when the note does not fit under it, only the buttons come off. Resolves whether the message was changed at all
|
|
1391
|
+
* (the text edit or the removal of the buttons landed): when not, the buttons are still there and the next tap tries again.
|
|
1392
|
+
*/
|
|
1393
|
+
async function closeOrphanQuestionMessage(key, messageId, messageText) {
|
|
1394
|
+
const note = withThreadLocale(key, () => (0, i18n_1.t)(questionClosingNoteKeys.unanswered));
|
|
1395
|
+
const closedText = messageText === null ? null : `${messageText}\n\n${note}`;
|
|
1396
|
+
const isEdited = closedText !== null && closedText.length <= messageSplit_1.MAX_MESSAGE_LEN && await editThreadMessage(key, messageId, closedText);
|
|
1397
|
+
return isEdited || editThreadKeyboard(key, messageId, null, 'reask');
|
|
1398
|
+
}
|
|
1076
1399
|
// ── auto-retry after a provider-side API error (plan S4/S5/S6) ──
|
|
1077
1400
|
/** One minute in ms — used only to render the "in N min" notices. */
|
|
1078
1401
|
const apiRetryMsPerMinute = 60_000;
|
|
@@ -1172,6 +1495,13 @@ const wedgeRecoveriesInFlight = new Set();
|
|
|
1172
1495
|
*/
|
|
1173
1496
|
function handleApiError(key, cls, options = {}) {
|
|
1174
1497
|
const k = (0, sessionKey_1.keyToString)(key);
|
|
1498
|
+
// The copy of a limit error that the adopt's replay reads: it ended a turn BEFORE this boot, and the boot recovery has armed
|
|
1499
|
+
// that very episode (with the error's own time, which the replay cannot know). Handling it again announced a limit for a
|
|
1500
|
+
// window that was over, and resumed the topic a second time.
|
|
1501
|
+
if (cls.isReplayed === true && cls.kind === 'usageLimit' && limitEpisodesOwnedByRecovery.has(k)) {
|
|
1502
|
+
console.log(`[limitRecovery] ${k}: the replayed limit error is the episode the recovery re-armed — not handled twice`);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1175
1505
|
// A used-up API credit balance: no wait refills it — the account is marked, and the chain or the operator takes over.
|
|
1176
1506
|
if (cls.kind === 'usageLimit' && cls.isBalanceExhausted === true) {
|
|
1177
1507
|
void handleBalanceExhausted(key, cls);
|
|
@@ -1927,8 +2257,45 @@ const awaitingSessionSelection = new Set();
|
|
|
1927
2257
|
* on an invalid name so the user can retry.
|
|
1928
2258
|
*/
|
|
1929
2259
|
const awaitingFolderName = new Set();
|
|
2260
|
+
/**
|
|
2261
|
+
* The `/bind` picker message a «create new folder» tap came from, per thread: once the typed name is bound, that picker is
|
|
2262
|
+
* deleted. Written only together with the arming of {@link awaitingFolderName}, and read only by the bind that arming
|
|
2263
|
+
* leads to — so a leftover entry of an abandoned attempt is overwritten by the next tap.
|
|
2264
|
+
*/
|
|
2265
|
+
const bindPickerMessageIds = new Map();
|
|
2266
|
+
/** The `/bind` picker messages a bind or a leave holds (in flight or done), so a second tap on the same keyboard acts on nothing again. */
|
|
2267
|
+
const bindPickerClaims = new bindPickerClaims_1.BindPickerClaims(ENV.testTimings.bindPickerClaimLimit);
|
|
2268
|
+
/**
|
|
2269
|
+
* The messages open in each topic that offer a bind or ask for a folder's name (every `/bind` picker, the «not bound» replies
|
|
2270
|
+
* that carry one, the «create new folder» prompt): any bind or leave of the topic deletes them all — see
|
|
2271
|
+
* {@link closeOpenBindMessages}. In memory: a picker sent before a restart is not known, and only its own tap removes it.
|
|
2272
|
+
*/
|
|
2273
|
+
const openBindMessages = new openBindMessages_1.OpenBindMessages();
|
|
2274
|
+
/**
|
|
2275
|
+
* The welcome each topic last got (per topic, newest last): its ▶️ Claude / ▶️ OpenCode / 📋 Resume buttons act on the topic's
|
|
2276
|
+
* CURRENT folder whatever the message says, so the next bind or a leave strips the buttons of the ones before. In memory, like
|
|
2277
|
+
* {@link openBindMessages}: a welcome sent before a restart keeps its buttons. The same per-topic, bounded, timestamped list of
|
|
2278
|
+
* message ids as {@link openBindMessages}, reused: these messages are never deleted, only stripped.
|
|
2279
|
+
*/
|
|
2280
|
+
const openWelcomeMessages = new openBindMessages_1.OpenBindMessages();
|
|
1930
2281
|
/** Max sessions shown in a `/sessions` list (Telegram-friendly, plan S3). */
|
|
1931
2282
|
const sessionsDisplayLimit = 10;
|
|
2283
|
+
/**
|
|
2284
|
+
* @description Forum-topic names that arrived via `forum_topic_created`
|
|
2285
|
+
* but couldn't be processed because the creator wasn't an authorised user
|
|
2286
|
+
* (a group admin/creator) (audit S2 / #5). We can't read a thread's title
|
|
2287
|
+
* later via Telegram Bot API in any portable way, so keeping the name here
|
|
2288
|
+
* lets the first message from an authorised user trigger fuzzy auto-bind.
|
|
2289
|
+
*
|
|
2290
|
+
* Also holds the name of a topic an authorised user made (or renamed) that matched
|
|
2291
|
+
* no folder, so the binding made through the picker later can keep it
|
|
2292
|
+
* (`PendingTopicName.isFromAdmin`: only such a name may reach a binding).
|
|
2293
|
+
*
|
|
2294
|
+
* In-memory by design: this is a UX nicety, not a security boundary —
|
|
2295
|
+
* losing the cache on restart just means the user has to bind manually
|
|
2296
|
+
* once. The TTL guards against unbounded growth in a busy group where
|
|
2297
|
+
* non-authorised members keep creating topics.
|
|
2298
|
+
*/
|
|
1932
2299
|
const pendingTopicNames = new Map();
|
|
1933
2300
|
const PENDING_TOPIC_NAME_TTL_MS = 24 * 60 * 60 * 1000;
|
|
1934
2301
|
/**
|
|
@@ -1958,6 +2325,10 @@ const threadContextMarkers = new Map();
|
|
|
1958
2325
|
*/
|
|
1959
2326
|
function clearThreadContextMarker(key) {
|
|
1960
2327
|
threadContextMarkers.delete((0, sessionKey_1.keyToString)(key));
|
|
2328
|
+
// The same boundary for the project board: the next turn start carries it whole (D20), and an inbox no tool call of
|
|
2329
|
+
// this session will take is withdrawn.
|
|
2330
|
+
noteTodoFreshContext(key);
|
|
2331
|
+
discardTodoInbox(key);
|
|
1961
2332
|
}
|
|
1962
2333
|
function getThreadMessageState(key) {
|
|
1963
2334
|
const k = (0, sessionKey_1.keyToString)(key);
|
|
@@ -2004,7 +2375,7 @@ function getOutputQueueState(key) {
|
|
|
2004
2375
|
const k = (0, sessionKey_1.keyToString)(key);
|
|
2005
2376
|
let s = outputQueues.get(k);
|
|
2006
2377
|
if (!s) {
|
|
2007
|
-
s = {
|
|
2378
|
+
s = { pendingSegments: [], isProcessing: false, debounceTimer: null };
|
|
2008
2379
|
outputQueues.set(k, s);
|
|
2009
2380
|
}
|
|
2010
2381
|
return s;
|
|
@@ -2025,7 +2396,7 @@ function checkIsOutputStreaming(key) {
|
|
|
2025
2396
|
const q = outputQueues.get((0, sessionKey_1.keyToString)(key));
|
|
2026
2397
|
if (!q)
|
|
2027
2398
|
return false;
|
|
2028
|
-
return q
|
|
2399
|
+
return (0, outputFlushPlan_1.checkHasPendingOutput)(q) || q.isProcessing || q.debounceTimer !== null;
|
|
2029
2400
|
}
|
|
2030
2401
|
function getStatusCoalesceState(key) {
|
|
2031
2402
|
const k = (0, sessionKey_1.keyToString)(key);
|
|
@@ -2088,11 +2459,15 @@ function clearThreadQueues(key) {
|
|
|
2088
2459
|
// `/quit` release, session `closed`/`stopped`, `/unbind`, and topic-delete —
|
|
2089
2460
|
// all route through here. CRITICAL ORDER: finalize BEFORE
|
|
2090
2461
|
// `clearThreadOutputQueues`, because the group finalize drains
|
|
2091
|
-
//
|
|
2462
|
+
// the pending segments and the clear would otherwise drop them first (S2). Both
|
|
2092
2463
|
// transports capture + reset their in-flight state SYNCHRONOUSLY before any
|
|
2093
2464
|
// await, so the clear below can't race the drain. Fire-and-forget.
|
|
2094
2465
|
void getOutboundFor(key).finalize(key);
|
|
2095
|
-
|
|
2466
|
+
const queue = outputQueues.get(k);
|
|
2467
|
+
// Text that took the new-message mark and is dropped unsent leaves the mark with the thread again.
|
|
2468
|
+
if (queue?.pendingSegments.some((segment) => segment.startsNewMessage))
|
|
2469
|
+
markNeedsNewMessage(key);
|
|
2470
|
+
(0, clearThreadOutputQueues_1.clearThreadOutputQueues)(queue, statusCoalescers.get(k));
|
|
2096
2471
|
// A new session starts with empty context — forget the last-sent outputs so
|
|
2097
2472
|
// the identical-output backstop can't suppress a legitimate repeat across a
|
|
2098
2473
|
// session boundary (the same convergence point that clears the other
|
|
@@ -2365,6 +2740,7 @@ function clearInMemoryThreadState(key) {
|
|
|
2365
2740
|
disconnectProviderLists.delete(pickerKey);
|
|
2366
2741
|
}
|
|
2367
2742
|
awaitingFolderName.delete(k);
|
|
2743
|
+
bindPickerMessageIds.delete(k);
|
|
2368
2744
|
pinnedStatusTextCache.delete(k);
|
|
2369
2745
|
statusCoalescers.delete(k);
|
|
2370
2746
|
// `clearThreadQueues` → `finalizeInFlight` above already finalized any accumulated
|
|
@@ -2764,11 +3140,17 @@ async function replyToThread(key, text, extra = {}, options = {}) {
|
|
|
2764
3140
|
const send = () => bot.telegram.sendMessage((0, sessionKeyCodec_1.getTelegramChatId)(key), text, sendExtra);
|
|
2765
3141
|
return options.unpaced ? (0, rateLimiter_1.sendUnpaced)(key, send) : (0, rateLimiter_1.enqueueSend)(key, send);
|
|
2766
3142
|
};
|
|
2767
|
-
|
|
2768
|
-
|
|
3143
|
+
// A sent message is tracked for `/clear_messages`, and a `/bind` picker is remembered to be closed by the next bind or leave.
|
|
3144
|
+
const trackSent = async (sent) => {
|
|
2769
3145
|
const messageId = sent.message_id;
|
|
2770
3146
|
await state.pushMessageId(key, messageId);
|
|
3147
|
+
if ((0, bindPicker_1.checkIsBindPickerExtra)(extra))
|
|
3148
|
+
openBindMessages.remember((0, sessionKey_1.keyToString)(key), messageId, Date.now());
|
|
2771
3149
|
return messageId;
|
|
3150
|
+
};
|
|
3151
|
+
const releaseSecret = options.isSecret ? (0, outputTrace_1.markOutboundTextSecret)(text) : null;
|
|
3152
|
+
try {
|
|
3153
|
+
return await trackSent(await sendOnce(buildSendExtra(key, extra)));
|
|
2772
3154
|
}
|
|
2773
3155
|
catch (e) {
|
|
2774
3156
|
// Markdown parse failure fallback. Several `t(...)` templates interpolate
|
|
@@ -2786,10 +3168,7 @@ async function replyToThread(key, text, extra = {}, options = {}) {
|
|
|
2786
3168
|
const plainExtra = { ...extra };
|
|
2787
3169
|
delete plainExtra.parse_mode;
|
|
2788
3170
|
try {
|
|
2789
|
-
|
|
2790
|
-
const messageId = sent.message_id;
|
|
2791
|
-
await state.pushMessageId(key, messageId);
|
|
2792
|
-
return messageId;
|
|
3171
|
+
return await trackSent(await sendOnce(buildSendExtra(key, plainExtra)));
|
|
2793
3172
|
}
|
|
2794
3173
|
catch (e2) {
|
|
2795
3174
|
await handleSendError(key, e2);
|
|
@@ -2804,6 +3183,9 @@ async function replyToThread(key, text, extra = {}, options = {}) {
|
|
|
2804
3183
|
await handleSendError(key, e);
|
|
2805
3184
|
return null;
|
|
2806
3185
|
}
|
|
3186
|
+
finally {
|
|
3187
|
+
releaseSecret?.();
|
|
3188
|
+
}
|
|
2807
3189
|
}
|
|
2808
3190
|
/**
|
|
2809
3191
|
* @description Edit an existing assistant message in the thread.
|
|
@@ -2811,13 +3193,17 @@ async function replyToThread(key, text, extra = {}, options = {}) {
|
|
|
2811
3193
|
* Returns `true` on success, `false` if the edit failed for any reason —
|
|
2812
3194
|
* caller decides whether to fall back to a new `replyToThread`. We don't
|
|
2813
3195
|
* track edits as new ids (the original was already tracked).
|
|
3196
|
+
*
|
|
3197
|
+
* `plainFallbackText`: the text for the retry without `parse_mode` when Telegram cannot parse the Markdown one — the
|
|
3198
|
+
* same text when absent, which shows a Markdown render's escape marks as they are; a caller that has a real plain
|
|
3199
|
+
* rendering passes it.
|
|
2814
3200
|
*/
|
|
2815
|
-
async function editThreadMessage(key, messageId, text, extra = {}) {
|
|
3201
|
+
async function editThreadMessage(key, messageId, text, extra = {}, plainFallbackText = text) {
|
|
2816
3202
|
if (!checkIsTelegramPrimitiveKey(key, 'editThreadMessage'))
|
|
2817
3203
|
return false;
|
|
2818
|
-
const editOnce = (editExtra) => (0, rateLimiter_1.enqueueSend)(key, () => bot.telegram.editMessageText((0, sessionKeyCodec_1.getTelegramChatId)(key), messageId, undefined,
|
|
3204
|
+
const editOnce = (editText, editExtra) => (0, rateLimiter_1.enqueueSend)(key, () => bot.telegram.editMessageText((0, sessionKeyCodec_1.getTelegramChatId)(key), messageId, undefined, editText, editExtra));
|
|
2819
3205
|
try {
|
|
2820
|
-
await editOnce(extra);
|
|
3206
|
+
await editOnce(text, extra);
|
|
2821
3207
|
return true;
|
|
2822
3208
|
}
|
|
2823
3209
|
catch (e) {
|
|
@@ -2837,7 +3223,7 @@ async function editThreadMessage(key, messageId, text, extra = {}) {
|
|
|
2837
3223
|
const plainExtra = { ...extra };
|
|
2838
3224
|
delete plainExtra.parse_mode;
|
|
2839
3225
|
try {
|
|
2840
|
-
await editOnce(plainExtra);
|
|
3226
|
+
await editOnce(plainFallbackText, plainExtra);
|
|
2841
3227
|
return true;
|
|
2842
3228
|
}
|
|
2843
3229
|
catch (e2) {
|
|
@@ -2852,14 +3238,63 @@ async function editThreadMessage(key, messageId, text, extra = {}) {
|
|
|
2852
3238
|
return false;
|
|
2853
3239
|
}
|
|
2854
3240
|
}
|
|
2855
|
-
|
|
3241
|
+
/**
|
|
3242
|
+
* @description Replace the inline keyboard of a message of the thread (`null` removes it) through the paced send queue.
|
|
3243
|
+
* Returns `true` when the keyboard is as asked — a no-change edit («message is not modified») counts — and `false`
|
|
3244
|
+
* when the edit failed, which is logged under `logTag` and left to the caller to act on (a re-render may simply skip it).
|
|
3245
|
+
*/
|
|
3246
|
+
async function editThreadKeyboard(key, messageId, keyboard, logTag) {
|
|
3247
|
+
if (!checkIsTelegramPrimitiveKey(key, 'editThreadKeyboard'))
|
|
3248
|
+
return false;
|
|
3249
|
+
try {
|
|
3250
|
+
await (0, rateLimiter_1.enqueueSend)(key, () => bot.telegram.editMessageReplyMarkup((0, sessionKeyCodec_1.getTelegramChatId)(key), messageId, undefined, keyboard ?? undefined));
|
|
3251
|
+
return true;
|
|
3252
|
+
}
|
|
3253
|
+
catch (e) {
|
|
3254
|
+
const desc = (0, sendErrorClassifier_1.checkIsApiError)(e) ? (0, sendErrorClassifier_1.getErrorDescription)(e) : '';
|
|
3255
|
+
if (/message is not modified/i.test(desc))
|
|
3256
|
+
return true;
|
|
3257
|
+
console.warn(`[${logTag}] keyboard edit failed:`, desc || e);
|
|
3258
|
+
return false;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
async function deleteThreadMessageDetailed(key, messageId) {
|
|
2856
3262
|
if (!checkIsTelegramPrimitiveKey(key, 'deleteThreadMessage'))
|
|
2857
|
-
return;
|
|
3263
|
+
return 'stayed';
|
|
2858
3264
|
try {
|
|
2859
3265
|
await (0, rateLimiter_1.enqueueSend)(key, () => bot.telegram.deleteMessage((0, sessionKeyCodec_1.getTelegramChatId)(key), messageId));
|
|
3266
|
+
return 'deleted';
|
|
3267
|
+
}
|
|
3268
|
+
catch (e) {
|
|
3269
|
+
return (0, telegramError_1.checkIsMessageNotFoundError)(e) ? 'gone' : 'stayed';
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
/**
|
|
3273
|
+
* @description Delete a message of the thread. Resolves `false` when it is still there — older than 48 hours, refused, or no
|
|
3274
|
+
* Telegram topic's key — which a caller that needs it gone can act on.
|
|
3275
|
+
*/
|
|
3276
|
+
async function deleteThreadMessage(key, messageId) {
|
|
3277
|
+
return (await deleteThreadMessageDetailed(key, messageId)) === 'deleted';
|
|
3278
|
+
}
|
|
3279
|
+
/** The most message ids one `deleteMessages` call takes (the Bot API documents 1-100). */
|
|
3280
|
+
const deleteMessagesPerCallLimit = 100;
|
|
3281
|
+
/**
|
|
3282
|
+
* @description Delete several messages of the thread in ONE paced send (`deleteMessages`: «if some of the specified messages
|
|
3283
|
+
* can't be found, they are skipped»). Resolves `true` when the call went through; `false` when Telegram refused it — the docs do
|
|
3284
|
+
* not say what it does with a message that is there but may not be deleted — and the caller tries the messages one by one.
|
|
3285
|
+
*/
|
|
3286
|
+
async function deleteThreadMessages(key, messageIds) {
|
|
3287
|
+
if (!checkIsTelegramPrimitiveKey(key, 'deleteThreadMessages'))
|
|
3288
|
+
return false;
|
|
3289
|
+
try {
|
|
3290
|
+
for (let start = 0; start < messageIds.length; start += deleteMessagesPerCallLimit) {
|
|
3291
|
+
const chunk = messageIds.slice(start, start + deleteMessagesPerCallLimit);
|
|
3292
|
+
await (0, rateLimiter_1.enqueueSend)(key, () => bot.telegram.deleteMessages((0, sessionKeyCodec_1.getTelegramChatId)(key), chunk));
|
|
3293
|
+
}
|
|
3294
|
+
return true;
|
|
2860
3295
|
}
|
|
2861
3296
|
catch {
|
|
2862
|
-
|
|
3297
|
+
return false;
|
|
2863
3298
|
}
|
|
2864
3299
|
}
|
|
2865
3300
|
/**
|
|
@@ -2941,7 +3376,7 @@ function startTypingLoader(key) {
|
|
|
2941
3376
|
const q = outputQueues.get((0, sessionKey_1.keyToString)(key));
|
|
2942
3377
|
console.warn(`[typing] ${(0, sessionKey_1.keyToString)(key)} force-stop: kept alive by leaked output-queue state — ` +
|
|
2943
3378
|
`isOutputStreaming=${checkIsOutputStreaming(key)} isAdapterBusy=${checkIsAdapterBusy(key)} ` +
|
|
2944
|
-
`pendingOutput=${q
|
|
3379
|
+
`pendingOutput=${q !== undefined && (0, outputFlushPlan_1.checkHasPendingOutput)(q)} ` +
|
|
2945
3380
|
`isProcessing=${q?.isProcessing === true} debounceTimer=${q?.debounceTimer !== null && q?.debounceTimer !== undefined}`);
|
|
2946
3381
|
if (q?.debounceTimer) {
|
|
2947
3382
|
clearTimeout(q.debounceTimer);
|
|
@@ -2968,7 +3403,7 @@ function checkIsTypingLoopStuck(key) {
|
|
|
2968
3403
|
isAdapterBusy: checkIsAdapterBusy(key),
|
|
2969
3404
|
isCompacting: checkIsThreadCompacting(key),
|
|
2970
3405
|
isTransportStreaming: getOutboundFor(key).checkIsDelivering(key),
|
|
2971
|
-
hasPendingOutput: q
|
|
3406
|
+
hasPendingOutput: q !== undefined && (0, outputFlushPlan_1.checkHasPendingOutput)(q),
|
|
2972
3407
|
isProcessing: q?.isProcessing === true,
|
|
2973
3408
|
hasDebounceTimer: q?.debounceTimer != null,
|
|
2974
3409
|
});
|
|
@@ -3033,7 +3468,7 @@ function escapeMarkdown(text) {
|
|
|
3033
3468
|
* @description Queue an output text for a thread with debounce.
|
|
3034
3469
|
*
|
|
3035
3470
|
* Multiple `output` events from the adapter are coalesced into one pending
|
|
3036
|
-
* buffer (see {@link
|
|
3471
|
+
* buffer (see {@link addPendingOutput}), not overwritten — every emitted
|
|
3037
3472
|
* chunk reaches Telegram. A continuation tail concatenates as-is (it may be
|
|
3038
3473
|
* cut mid-word); a standalone output joins the buffer with `\n`. The first
|
|
3039
3474
|
* batch's `isContinuation` is recorded so the flush knows whether the buffer
|
|
@@ -3069,11 +3504,9 @@ function getOutputDelay(chatId) {
|
|
|
3069
3504
|
*/
|
|
3070
3505
|
function queueOutput(key, output, isContinuation = false, isFinal = false, isComplete = false, startsNewParagraph = false) {
|
|
3071
3506
|
const q = getOutputQueueState(key);
|
|
3072
|
-
//
|
|
3073
|
-
//
|
|
3074
|
-
|
|
3075
|
-
q.pendingIsContinuation = isContinuation;
|
|
3076
|
-
q.pendingOutput = (0, outputFlushPlan_1.appendPendingOutput)(q.pendingOutput, output, isContinuation, startsNewParagraph);
|
|
3507
|
+
// A batch joins the newest pending segment; one queued after a turn boundary was marked starts a segment of its own, which
|
|
3508
|
+
// opens a new message, and leaves the earlier segment to flush where it was going.
|
|
3509
|
+
(0, outputFlushPlan_1.addPendingOutput)(q, getThreadMessageState(key), { output, isContinuation, startsNewParagraph });
|
|
3077
3510
|
// `isFinal` / `isComplete` still drive the immediate-flush timing below (a
|
|
3078
3511
|
// finished turn must not wait out the debounce) — they no longer mark the
|
|
3079
3512
|
// buffer for redelivery (retired with the global send pacer, S4).
|
|
@@ -3098,7 +3531,8 @@ function queueOutput(key, output, isContinuation = false, isFinal = false, isCom
|
|
|
3098
3531
|
// S3: scale the in-cooldown debounce to the live remaining cooldown.
|
|
3099
3532
|
remainingCooldownMs: (0, rateLimiter_1.getRateLimitRemainingMs)((0, sessionKeyCodec_1.getTelegramChatId)(key)),
|
|
3100
3533
|
});
|
|
3101
|
-
|
|
3534
|
+
// An earlier segment, sealed by a boundary, has nothing more to wait for: its turn is over.
|
|
3535
|
+
if (timing === 'now' || q.pendingSegments.length > 1) {
|
|
3102
3536
|
// Final frame: flush immediately. `processOutputQueue` already guards
|
|
3103
3537
|
// `isProcessing`, so this is idempotent if a flush is mid-flight (the
|
|
3104
3538
|
// pending frame will be picked up by the re-trigger timer it arms).
|
|
@@ -3112,14 +3546,14 @@ function queueOutput(key, output, isContinuation = false, isFinal = false, isCom
|
|
|
3112
3546
|
}
|
|
3113
3547
|
async function processOutputQueue(key) {
|
|
3114
3548
|
const q = getOutputQueueState(key);
|
|
3115
|
-
if (q.isProcessing || !
|
|
3549
|
+
if (q.isProcessing || !(0, outputFlushPlan_1.checkHasPendingOutput)(q))
|
|
3116
3550
|
return;
|
|
3117
3551
|
q.isProcessing = true;
|
|
3118
3552
|
try {
|
|
3119
|
-
const
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
const { unsentRemainder } = await sendOutputImmediate(key,
|
|
3553
|
+
const flush = (0, outputFlushPlan_1.takePendingOutput)(q);
|
|
3554
|
+
if (!flush)
|
|
3555
|
+
return;
|
|
3556
|
+
const { unsentRemainder } = await sendOutputImmediate(key, flush.output, flush.isContinuation, flush.startsNewMessage);
|
|
3123
3557
|
// No silent drop: a chunk dropped on a 429 after the retry-after wait comes
|
|
3124
3558
|
// back as `unsentRemainder`. Put it BACK at the FRONT of the buffer (it is
|
|
3125
3559
|
// older than anything that arrived during the await) so the re-trigger flush
|
|
@@ -3127,17 +3561,12 @@ async function processOutputQueue(key) {
|
|
|
3127
3561
|
// `getUnsentRemainder`, so this never re-sends what already reached Telegram.
|
|
3128
3562
|
// The remainder restarts a fresh message (its predecessors landed / edited in
|
|
3129
3563
|
// place already), so it is NOT a continuation.
|
|
3130
|
-
if (unsentRemainder)
|
|
3131
|
-
q
|
|
3132
|
-
q.pendingOutput === null
|
|
3133
|
-
? unsentRemainder
|
|
3134
|
-
: (0, outputFlushPlan_1.appendPendingOutput)(unsentRemainder, q.pendingOutput, false);
|
|
3135
|
-
q.pendingIsContinuation = false;
|
|
3136
|
-
}
|
|
3564
|
+
if (unsentRemainder)
|
|
3565
|
+
(0, outputFlushPlan_1.requeueUnsentRemainder)(q, unsentRemainder);
|
|
3137
3566
|
}
|
|
3138
3567
|
finally {
|
|
3139
3568
|
q.isProcessing = false;
|
|
3140
|
-
if (
|
|
3569
|
+
if ((0, outputFlushPlan_1.checkHasPendingOutput)(q)) {
|
|
3141
3570
|
// Audit S13 / #30: the re-trigger `setTimeout` used to be a bare
|
|
3142
3571
|
// call whose handle was never stored. A `queueOutput` call between
|
|
3143
3572
|
// here and the timer firing would `clearTimeout(null)` and
|
|
@@ -3149,7 +3578,7 @@ async function processOutputQueue(key) {
|
|
|
3149
3578
|
q.debounceTimer = setTimeout(() => {
|
|
3150
3579
|
q.debounceTimer = null;
|
|
3151
3580
|
processOutputQueue(key);
|
|
3152
|
-
}, getOutputDelay((0, sessionKeyCodec_1.getTelegramChatId)(key)));
|
|
3581
|
+
}, q.pendingSegments.length > 1 ? 0 : getOutputDelay((0, sessionKeyCodec_1.getTelegramChatId)(key)));
|
|
3153
3582
|
}
|
|
3154
3583
|
}
|
|
3155
3584
|
}
|
|
@@ -3158,7 +3587,7 @@ async function processOutputQueue(key) {
|
|
|
3158
3587
|
* coalesced-but-unsent output against what actually landed, and force-send the
|
|
3159
3588
|
* remainder so the agent's FINAL answer is never discarded on teardown (S2).
|
|
3160
3589
|
*
|
|
3161
|
-
* Today the group output queue coalesces into `q.
|
|
3590
|
+
* Today the group output queue coalesces into `q.pendingSegments` and the
|
|
3162
3591
|
* (possibly 429-stretched) debounce flushes it later; a teardown
|
|
3163
3592
|
* (`idle`/`isFinal` → `handleAgentStopped`, `/new`, `/quit`, `/unbind`,
|
|
3164
3593
|
* topic-delete) runs `clearThreadOutputQueues` which DISCARDS that buffer — so
|
|
@@ -3180,20 +3609,23 @@ async function finalizeGroupOutput(key) {
|
|
|
3180
3609
|
const plan = (0, groupFinalizePlan_1.getGroupFinalizePlan)(q);
|
|
3181
3610
|
if (plan.action === 'noop')
|
|
3182
3611
|
return; // fully delivered → no-op, no dupe.
|
|
3183
|
-
// Snapshot is captured by the plan;
|
|
3612
|
+
// Snapshot is captured by the plan; empty the buffer SYNCHRONOUSLY before the
|
|
3184
3613
|
// await so the immediately-following clear / a concurrent flush can't re-send it.
|
|
3185
|
-
q.
|
|
3614
|
+
q.pendingSegments = [];
|
|
3186
3615
|
if (q.debounceTimer) {
|
|
3187
3616
|
clearTimeout(q.debounceTimer);
|
|
3188
3617
|
q.debounceTimer = null;
|
|
3189
3618
|
}
|
|
3190
|
-
const
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3619
|
+
for (const segment of plan.segments) {
|
|
3620
|
+
const { unsentRemainder } = await sendOutputImmediate(key, segment.output, segment.isContinuation, segment.startsNewMessage);
|
|
3621
|
+
// A remainder that still 429'd at teardown is dropped (the buffer is being
|
|
3622
|
+
// torn down anyway — unchanged from the pre-S2 discard), and so is everything behind it. Do NOT re-queue: the
|
|
3623
|
+
// queue is about to be cleared, and re-queuing would race that clear. At 1/2s
|
|
3624
|
+
// global pacing a 429 here is essentially impossible.
|
|
3625
|
+
if (unsentRemainder) {
|
|
3626
|
+
console.warn(`[send] ${(0, sessionKey_1.keyToString)(key)} finalize remainder 429'd and was dropped at teardown`);
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3197
3629
|
}
|
|
3198
3630
|
}
|
|
3199
3631
|
/**
|
|
@@ -3210,12 +3642,17 @@ async function finalizeGroupOutput(key) {
|
|
|
3210
3642
|
* previous text on the same message id, so the user could read only the last
|
|
3211
3643
|
* tail. Editing is now reserved for explicit continuations of the SAME reply.
|
|
3212
3644
|
*
|
|
3645
|
+
* `startsNewMessage`: the text was queued after a turn boundary, so it opens a
|
|
3646
|
+
* message of its own — decided when the text was queued ({@link addPendingOutput}),
|
|
3647
|
+
* not read from the thread here, where a flush of EARLIER text finishing after the
|
|
3648
|
+
* boundary would have spent the mark.
|
|
3649
|
+
*
|
|
3213
3650
|
* On Markdown rejection by Telegram we fall back to plain text (`parse_mode`
|
|
3214
3651
|
* unset) — the message lands either way. If the in-place edit fails (message
|
|
3215
3652
|
* deleted / API hiccup) we send every chunk fresh so the full combined text
|
|
3216
3653
|
* still reaches the user.
|
|
3217
3654
|
*/
|
|
3218
|
-
async function sendOutputImmediate(key, output, isContinuation = false) {
|
|
3655
|
+
async function sendOutputImmediate(key, output, isContinuation = false, startsNewMessage = false) {
|
|
3219
3656
|
// The typing loader is stopped upstream in `handleAgentOutput` (the shared
|
|
3220
3657
|
// onOutput entry for BOTH transports), so it's not repeated here.
|
|
3221
3658
|
await deleteStatusMessage(key);
|
|
@@ -3223,7 +3660,7 @@ async function sendOutputImmediate(key, output, isContinuation = false) {
|
|
|
3223
3660
|
const { chunks: planChunks, shouldEditFirstChunk } = (0, outputFlushPlan_1.getOutputFlushPlan)({
|
|
3224
3661
|
output,
|
|
3225
3662
|
isContinuation,
|
|
3226
|
-
needsNewMessage:
|
|
3663
|
+
needsNewMessage: startsNewMessage,
|
|
3227
3664
|
lastMessageId: msgState.lastMessageId,
|
|
3228
3665
|
lastMessageText: msgState.lastMessageText,
|
|
3229
3666
|
});
|
|
@@ -3261,7 +3698,6 @@ async function sendOutputImmediate(key, output, isContinuation = false) {
|
|
|
3261
3698
|
break;
|
|
3262
3699
|
msgState.lastMessageId = id;
|
|
3263
3700
|
msgState.lastMessageText = chunks[i];
|
|
3264
|
-
msgState.needsNewMessage = false;
|
|
3265
3701
|
sentCount = i + 1;
|
|
3266
3702
|
}
|
|
3267
3703
|
// S3: output just landed below any pending question — bring the question back
|
|
@@ -3399,7 +3835,7 @@ const shutdownOutputFlushMaxMs = rateLimiter_1.shutdownDrainMaxMs + 1000;
|
|
|
3399
3835
|
* already-emitted answer chunks died in the send pipeline with the
|
|
3400
3836
|
* process and were never re-sent). Two buffers drain, in order:
|
|
3401
3837
|
*
|
|
3402
|
-
* [A] per-thread COALESCE state — the group/baseline `q.
|
|
3838
|
+
* [A] per-thread COALESCE state — the group/baseline `q.pendingSegments`
|
|
3403
3839
|
* buffers (enumerated here from {@link outputQueues}) and the DM
|
|
3404
3840
|
* transport's active drafts (enumerated via `getInFlightThreadKeys`) —
|
|
3405
3841
|
* each finalized to a permanent message via the transport's
|
|
@@ -3429,7 +3865,7 @@ async function finalizePendingOutputOnShutdown() {
|
|
|
3429
3865
|
// queues + the connector's own unfinalized content (deduped by serialised key).
|
|
3430
3866
|
const pendingKeys = new Map();
|
|
3431
3867
|
for (const [keyStr, q] of outputQueues) {
|
|
3432
|
-
if (
|
|
3868
|
+
if (!(0, outputFlushPlan_1.checkHasPendingOutput)(q))
|
|
3433
3869
|
continue;
|
|
3434
3870
|
try {
|
|
3435
3871
|
pendingKeys.set(keyStr, (0, sessionKey_1.keyFromString)(keyStr));
|
|
@@ -3447,11 +3883,11 @@ async function finalizePendingOutputOnShutdown() {
|
|
|
3447
3883
|
// A DM-surface thread on a non-draft adapter (e.g. terminal) coalesces via
|
|
3448
3884
|
// the group `queueOutput` path, but its transport finalize handled only the
|
|
3449
3885
|
// draft — drain any such leftover buffer directly. Group keys were already
|
|
3450
|
-
// drained above (their finalize
|
|
3886
|
+
// drained above (their finalize empties the pending segments synchronously), so
|
|
3451
3887
|
// this is a no-op for them.
|
|
3452
3888
|
const leftoverDrains = [];
|
|
3453
3889
|
for (const [keyStr, q] of outputQueues) {
|
|
3454
|
-
if (
|
|
3890
|
+
if (!(0, outputFlushPlan_1.checkHasPendingOutput)(q))
|
|
3455
3891
|
continue;
|
|
3456
3892
|
try {
|
|
3457
3893
|
leftoverDrains.push(finalizeGroupOutput((0, sessionKey_1.keyFromString)(keyStr)));
|
|
@@ -3663,7 +4099,7 @@ function stopAllAdaptersFor(key, adapterNames) {
|
|
|
3663
4099
|
return (0, createAdapter_1.stopAllAdaptersFor)(key, createAdapter_1.getAdapter, adapterNames);
|
|
3664
4100
|
}
|
|
3665
4101
|
/**
|
|
3666
|
-
* @description Explicit-stop teardown used by `/new
|
|
4102
|
+
* @description Explicit-stop teardown used by `/new`:
|
|
3667
4103
|
* sweep every adapter active for the thread, then RELEASE the persisted session
|
|
3668
4104
|
* ids (even when nothing was running) so a later bot restart won't auto-reattach
|
|
3669
4105
|
* and any half-dead state from a crash/SSE-giveup is cleared. The session stays
|
|
@@ -3687,9 +4123,18 @@ async function releaseThreadSession(key) {
|
|
|
3687
4123
|
// Claude / no pending question.
|
|
3688
4124
|
(0, createAdapter_1.getThreadAdapter)(key).rejectQuestion?.(key);
|
|
3689
4125
|
const result = stopAllAdaptersFor(key);
|
|
3690
|
-
await
|
|
4126
|
+
await releaseConversationIds(key);
|
|
3691
4127
|
return result;
|
|
3692
4128
|
}
|
|
4129
|
+
/**
|
|
4130
|
+
* @description Release a conversation for good: its saved session ids go, so nothing resumes it, and the question the idle
|
|
4131
|
+
* compaction re-asked for it is closed (its message stays, without its buttons). A conversation that is asleep has no process
|
|
4132
|
+
* whose stop would say so, so the release itself has to — every release of a conversation that may be asleep goes through here.
|
|
4133
|
+
*/
|
|
4134
|
+
async function releaseConversationIds(key) {
|
|
4135
|
+
await state.clearAgentSessionIds(key);
|
|
4136
|
+
clearThreadCompaction(key);
|
|
4137
|
+
}
|
|
3693
4138
|
/**
|
|
3694
4139
|
* @description Switch a thread's adapter and clear the previous adapter's
|
|
3695
4140
|
* persisted session id, so a later restart can't re-attach to the wrong
|
|
@@ -3712,8 +4157,11 @@ async function switchThreadAdapter(key, newName) {
|
|
|
3712
4157
|
// Another agent or a shell takes the topic over: the open request belonged to
|
|
3713
4158
|
// the old conversation, so it closes silently instead of a later wake-up
|
|
3714
4159
|
// reminding (or, in a terminal, TYPING into) a session that never saw it.
|
|
3715
|
-
if (prevName && !(0, createAdapter_1.checkIsSameConversationSwitch)(prevName, newName))
|
|
4160
|
+
if (prevName && !(0, createAdapter_1.checkIsSameConversationSwitch)(prevName, newName)) {
|
|
3716
4161
|
cancelConversationRequest(key);
|
|
4162
|
+
// Nor does the conversation left behind get its re-asked question answered: its buttons would send the label into the new one.
|
|
4163
|
+
closeReAskedQuestion(key);
|
|
4164
|
+
}
|
|
3717
4165
|
let switchCutOffWork = null;
|
|
3718
4166
|
if (prevName && prevName !== newName) {
|
|
3719
4167
|
try {
|
|
@@ -3848,8 +4296,8 @@ function getStartReadyMessage(adapter, subdir, args, model, effort) {
|
|
|
3848
4296
|
return (0, i18n_1.t)('agent.ready', { ...vars, infoBlock: getStartReadyInfoBlock(model, effort) });
|
|
3849
4297
|
}
|
|
3850
4298
|
/**
|
|
3851
|
-
* The bot-MCP tool digest
|
|
3852
|
-
* at boot; `null` before that (no digest is persisted, which reads as stale).
|
|
4299
|
+
* The bot-MCP tool digest of a conversation (its platform's tools, its «Bot skills» instructions), from the scheduler handle
|
|
4300
|
+
* once it is wired at boot; `null` before that (no digest is persisted, which reads as stale).
|
|
3853
4301
|
*/
|
|
3854
4302
|
let botMcpToolDigestReader = null;
|
|
3855
4303
|
/**
|
|
@@ -3889,7 +4337,7 @@ function getPerTurnRefusal(key) {
|
|
|
3889
4337
|
async function persistSessionStart(key, adapter) {
|
|
3890
4338
|
await (0, persistAdapterSessionIds_1.persistAdapterSessionIds)(key, adapter, state);
|
|
3891
4339
|
if ((0, adapterNames_1.checkIsJsonStreamBackend)(adapter.name) && botMcpToolDigestReader) {
|
|
3892
|
-
await state.setAgentMcpToolDigest(key, botMcpToolDigestReader(key
|
|
4340
|
+
await state.setAgentMcpToolDigest(key, botMcpToolDigestReader(key));
|
|
3893
4341
|
}
|
|
3894
4342
|
}
|
|
3895
4343
|
/**
|
|
@@ -4231,6 +4679,11 @@ const autoStopUnsupportedLoggedKeys = new Set();
|
|
|
4231
4679
|
* and that turn ends here too) or the process still works (a background task,
|
|
4232
4680
|
* input not yet taken in). The session sleeps; the next trigger resumes it with
|
|
4233
4681
|
* the prompt cache warm.
|
|
4682
|
+
*
|
|
4683
|
+
* The stop is QUEUED here, in the same tick the adapter marks the turn done (nothing awaits in between): a
|
|
4684
|
+
* `postToSession` that polled the session idle and ensures it again (`postToSession.ts`) therefore always finds
|
|
4685
|
+
* the stop already on the transition queue and runs after it. Keep it that way — an await ahead of the queueing
|
|
4686
|
+
* would let that ensure run beside the stop and forward to the process it ends.
|
|
4234
4687
|
*/
|
|
4235
4688
|
function handleAgentTurnEnded(key) {
|
|
4236
4689
|
noteThreadActivity(key);
|
|
@@ -4273,7 +4726,7 @@ async function replayBufferedPrompts(key) {
|
|
|
4273
4726
|
// like the API-error retry's nudge, hears whether it reached the session).
|
|
4274
4727
|
await startupPromptBuffer.replayPrompts((0, sessionKey_1.keyToString)(key), {
|
|
4275
4728
|
isSessionActive,
|
|
4276
|
-
forward: (prompt) => forwardPromptToAgent(key, adapter, prompt),
|
|
4729
|
+
forward: async (prompt) => { await forwardPromptToAgent(key, adapter, prompt); },
|
|
4277
4730
|
});
|
|
4278
4731
|
if (!isSessionActive)
|
|
4279
4732
|
return;
|
|
@@ -4307,17 +4760,61 @@ async function replayBufferedPrompts(key) {
|
|
|
4307
4760
|
* `session.status=retry` will not read queued prompts until the provider retry
|
|
4308
4761
|
* time, so a new prompt aborts that stale wait before using the current model.
|
|
4309
4762
|
* An adapter without the method forwards directly.
|
|
4763
|
+
*
|
|
4764
|
+
* Resolves where the prompt went (`LatePromptOutcome`): `forwarded` to the session, `buffered` behind a stop window, `noSession`
|
|
4765
|
+
* (nothing to take it; the person was told), `dropped` (a technical prompt found no live idle session). Only a prompt that waited
|
|
4766
|
+
* for an adopt replay can come out as anything but `forwarded`; most callers have no use for the answer.
|
|
4310
4767
|
*/
|
|
4311
4768
|
async function forwardPromptToAgent(key, adapter, text, sentAtMs, options = {}) {
|
|
4769
|
+
// An adopted session replays what its process wrote during the downtime first: a prompt written meanwhile would reset
|
|
4770
|
+
// the answer and error state those frames still use, and the replayed text would join the new reply's message (the
|
|
4771
|
+
// «new message» mark would go to the replayed text). Nothing is awaited when no replay is pending: the forward then runs
|
|
4772
|
+
// in the caller's tick, as it always did — an await ahead of it would let a stop queued meanwhile begin first.
|
|
4773
|
+
if (!adapter.checkIsAdoptReplayPending?.(key)) {
|
|
4774
|
+
await forwardPromptToLiveAgent(key, adapter, text, sentAtMs, options);
|
|
4775
|
+
return 'forwarded';
|
|
4776
|
+
}
|
|
4777
|
+
await (0, adoptReplayGate_1.waitForAdoptedReplay)(adapter, key);
|
|
4778
|
+
// The wait let the replay run: the replayed turn's end may have queued the per-turn stop, which holds the window open and
|
|
4779
|
+
// ends the process — a prompt written now would go to a process being killed and be lost. Routed again, as a late prompt is.
|
|
4780
|
+
// A technical prompt (the standalone todo reminder) goes only into a session that is live and idle now, and is otherwise
|
|
4781
|
+
// dropped without a word: never buffered, never a resume, never a "no session" line to the person.
|
|
4782
|
+
const isTechnicalPrompt = options.isTechnicalPrompt === true;
|
|
4783
|
+
const outcome = await (0, promptDelivery_1.deliverLatePrompt)({
|
|
4784
|
+
isTechnical: isTechnicalPrompt,
|
|
4785
|
+
checkIsWindowOpen: () => startupPromptBuffer.checkIsStarting((0, sessionKey_1.keyToString)(key)),
|
|
4786
|
+
checkIsSessionActive: () => adapter.checkIsActive(key) && !(isTechnicalPrompt && (adapter.checkIsBusy?.(key) ?? false)),
|
|
4787
|
+
buffer: async () => {
|
|
4788
|
+
await deliverPromptOrBuffer(key, `${options.requestHeader ?? ''}${getPromptWithThreadReplyQuote(key, text, options.replyContext)}`, true);
|
|
4789
|
+
},
|
|
4790
|
+
wake: () => wakeSleepingSessionForPrompt(key),
|
|
4791
|
+
forward: async () => {
|
|
4792
|
+
await forwardPromptToLiveAgent(key, (0, createAdapter_1.getThreadAdapter)(key), text, sentAtMs, options);
|
|
4793
|
+
return 'forwarded';
|
|
4794
|
+
},
|
|
4795
|
+
reportNoSession: async (isAlreadyTold) => {
|
|
4796
|
+
console.warn(`[adopt] ${(0, sessionKey_1.keyToString)(key)}: no session is left to take the prompt that waited for the replay; it is dropped`);
|
|
4797
|
+
if (!isAlreadyTold)
|
|
4798
|
+
await replyToThread(key, (0, i18n_1.t)('agent.no_session'));
|
|
4799
|
+
},
|
|
4800
|
+
});
|
|
4801
|
+
if (outcome === 'dropped')
|
|
4802
|
+
console.log(`[adopt] ${(0, sessionKey_1.keyToString)(key)}: the technical prompt found no live, idle session after the replay wait; it is dropped`);
|
|
4803
|
+
return outcome;
|
|
4804
|
+
}
|
|
4805
|
+
/** @description The forward itself, for a session that is ready to take the prompt: {@link forwardPromptToAgent} minus the replay wait. */
|
|
4806
|
+
async function forwardPromptToLiveAgent(key, adapter, text, sentAtMs, options = {}) {
|
|
4312
4807
|
// A forwarded prompt is thread activity — reset the compact-on-idle watchdog (F2).
|
|
4313
4808
|
noteThreadActivity(key);
|
|
4809
|
+
const isTechnicalPrompt = options.isTechnicalPrompt ?? false;
|
|
4314
4810
|
// Skip slash commands (not worth replaying), the recovery replay itself (keeps
|
|
4315
|
-
// the once-per-episode guard intact)
|
|
4316
|
-
// prompt stays the one to replay).
|
|
4811
|
+
// the once-per-episode guard intact), a request reminder (the request's own
|
|
4812
|
+
// prompt stays the one to replay) and a bot-made technical prompt.
|
|
4317
4813
|
const isReplayable = (0, wedgeRecovery_1.checkIsReplayablePrompt)({
|
|
4318
4814
|
isSlashCommand: (0, threadContextPreamble_1.checkShouldSkipPreambleForText)(text),
|
|
4319
4815
|
isRecoveryReplay: options.isRecoveryReplay ?? false,
|
|
4320
4816
|
isRequestReminder: options.isRequestReminder ?? false,
|
|
4817
|
+
isTechnicalPrompt,
|
|
4321
4818
|
});
|
|
4322
4819
|
// R23: prompts held during a usage-limit wait ride the first such prompt
|
|
4323
4820
|
// forwarded to a live session once no wait holds them — after it, in arrival
|
|
@@ -4366,6 +4863,18 @@ async function forwardPromptToAgent(key, adapter, text, sentAtMs, options = {})
|
|
|
4366
4863
|
if (state.checkIsTimestampsEnabled(key) && !(0, threadContextPreamble_1.checkShouldSkipPreambleForText)(text)) {
|
|
4367
4864
|
promptText = `${(0, isoTimestamp_1.formatIsoLocalOffset)(sentAtMs ?? Date.now())}\n\n${promptText}`;
|
|
4368
4865
|
}
|
|
4866
|
+
// The project board rides every turn start the bot makes (D20: whole on a fresh context, else only what changed),
|
|
4867
|
+
// then the todo list's staleness reminder rides the next real prompt from any source (D11 form a) — never a slash
|
|
4868
|
+
// command or a shell line; the reminder never rides the standalone reminder itself.
|
|
4869
|
+
const isTodoDecorated = !(0, threadContextPreamble_1.checkShouldSkipPreambleForText)(text) && adapter.name !== 'terminal';
|
|
4870
|
+
if (isTodoDecorated)
|
|
4871
|
+
discardTodoInbox(key);
|
|
4872
|
+
const todoBoardBlock = isTodoDecorated ? takeTodoPromptBoard(key) : null;
|
|
4873
|
+
if (todoBoardBlock !== null)
|
|
4874
|
+
promptText = `${promptText}\n\n${todoBoardBlock}`;
|
|
4875
|
+
const todoReminderLine = isTodoDecorated && !isTechnicalPrompt ? takeTodoPromptReminder(key) : null;
|
|
4876
|
+
if (todoReminderLine !== null)
|
|
4877
|
+
promptText = `${promptText}\n\n${todoReminderLine}`;
|
|
4369
4878
|
markNeedsNewMessage(key);
|
|
4370
4879
|
// A new prompt is a new turn: lift the S2 idle-suppress latch and re-base the
|
|
4371
4880
|
// working-status elapsed so the next busy period shows a fresh 0:00 counter
|
|
@@ -4380,7 +4889,8 @@ async function forwardPromptToAgent(key, adapter, text, sentAtMs, options = {})
|
|
|
4380
4889
|
// persists while output is streaming OR the agent is busy — self-stopping only
|
|
4381
4890
|
// when the topic drains + idles.
|
|
4382
4891
|
getOutboundFor(key).setActivity(key, 'working');
|
|
4383
|
-
|
|
4892
|
+
// A technical prompt is posted only into an idle session: there is no turn to interrupt or sub-agent to detach.
|
|
4893
|
+
if (adapter.interruptAndWaitIdle && !isTechnicalPrompt) {
|
|
4384
4894
|
await adapter.interruptAndWaitIdle(key);
|
|
4385
4895
|
}
|
|
4386
4896
|
// OpenCode: if a sub-agent is synchronously blocking this session, detach it to
|
|
@@ -4388,7 +4898,7 @@ async function forwardPromptToAgent(key, adapter, text, sentAtMs, options = {})
|
|
|
4388
4898
|
// the whole (possibly long) sub-agent run — the sub-agent keeps working and its
|
|
4389
4899
|
// result is injected back when it finishes. No-op for other adapters and when
|
|
4390
4900
|
// no sub-agent is running. Best-effort; never blocks the forward on failure.
|
|
4391
|
-
if (adapter.detachRunningSubagents) {
|
|
4901
|
+
if (adapter.detachRunningSubagents && !isTechnicalPrompt) {
|
|
4392
4902
|
await adapter.detachRunningSubagents(key);
|
|
4393
4903
|
}
|
|
4394
4904
|
adapter.sendInput(key, promptText);
|
|
@@ -4567,6 +5077,7 @@ const botCore = {
|
|
|
4567
5077
|
getState: () => state,
|
|
4568
5078
|
replyToThread,
|
|
4569
5079
|
editThreadMessage,
|
|
5080
|
+
editThreadKeyboard,
|
|
4570
5081
|
deleteThreadMessage,
|
|
4571
5082
|
authoriseContext,
|
|
4572
5083
|
withThreadLocale,
|
|
@@ -4575,17 +5086,25 @@ const botCore = {
|
|
|
4575
5086
|
cancelConversationRequest,
|
|
4576
5087
|
wakeSleepingSession,
|
|
4577
5088
|
};
|
|
4578
|
-
const { checkIsThreadCompacting, noteThreadActivity, noteThreadUserActivity, markThreadTurnProducedOutput, clearThreadCompaction, rearmThreadIdleTimer, armDeferredCompaction, checkIsDeferredCompactionArmed, threadsCompacting,
|
|
5089
|
+
const { checkIsThreadCompacting, noteThreadActivity, noteThreadUserActivity, markThreadTurnProducedOutput, clearThreadCompaction, rearmThreadIdleTimer, armDeferredCompaction, checkIsDeferredCompactionArmed, threadsCompacting, answerReAskedQuestionByNumber, reAskInterruptedQuestion, restoreReAskedQuestion, closeSavedReAskedQuestion, closeReAskedQuestion, registerCompactionCommands, registerCompactionCallbacks, } = (0, compaction_1.createCompaction)({
|
|
4579
5090
|
...botCore,
|
|
4580
5091
|
startTypingLoader,
|
|
4581
|
-
forwardPromptToAgent,
|
|
5092
|
+
forwardPromptToAgent: async (key, adapter, text) => { await forwardPromptToAgent(key, adapter, text); },
|
|
4582
5093
|
pendingQuestions,
|
|
4583
5094
|
clearPendingQuestion,
|
|
5095
|
+
closeQuestionMessage,
|
|
4584
5096
|
idleWindowMs: (0, compactOnIdle_1.getAgentIdleMs)(process.env.AGENT_IDLE_MINUTES),
|
|
4585
5097
|
checkIsLimitWaitArmed: (key) => getArmedApiRetry(key)?.kind === 'usageLimit',
|
|
4586
5098
|
suspendThreadSession,
|
|
4587
5099
|
// L-D7: a sleeping per-turn session is woken for its idle compaction — a resume only, never a fresh start.
|
|
4588
5100
|
resumeSleepingSessionForCompaction: async (key) => (await ensureAgentSession(key, { isResumeOnly: true })).ok && (0, createAdapter_1.getThreadAdapter)(key).checkIsActive(key),
|
|
5101
|
+
getThreadAdapter: createAdapter_1.getThreadAdapter,
|
|
5102
|
+
// An answer to the agent's own question is never a request (S7), even delivered as a prompt.
|
|
5103
|
+
deliverLabel: (key, label) => withThreadLocale(key, () => deliverActivePrompt(key, (0, createAdapter_1.getThreadAdapter)(key), label, null)),
|
|
5104
|
+
resumeSleepingSessionForPrompt: (key) => resumeSleepingSessionForPrompt(key),
|
|
5105
|
+
checkIsStarting: (key) => startupPromptBuffer.checkIsStarting((0, sessionKey_1.keyToString)(key)),
|
|
5106
|
+
closeOrphanQuestionMessage,
|
|
5107
|
+
notePersonActivity,
|
|
4589
5108
|
});
|
|
4590
5109
|
/**
|
|
4591
5110
|
* @description True iff the neutral router owns `name`, i.e. a `/name` message
|
|
@@ -4703,6 +5222,11 @@ command('start', async (_ctx, key) => {
|
|
|
4703
5222
|
'/quit — quit current agent\n' +
|
|
4704
5223
|
'/status — show status');
|
|
4705
5224
|
});
|
|
5225
|
+
/** The `/status` agent line (`Claude Code (claude)`), flagged when the backend is the deprecated tmux one. */
|
|
5226
|
+
function formatStatusAgentLine(label, adapterName) {
|
|
5227
|
+
const agentLine = `${label} (${adapterName})`;
|
|
5228
|
+
return adapterName === adapterNames_1.claudeTmuxAdapterName ? (0, i18n_1.t)('status.thread_agentTmuxDeprecated', { agent: agentLine }) : agentLine;
|
|
5229
|
+
}
|
|
4706
5230
|
command('status', async (_ctx, key) => {
|
|
4707
5231
|
// In General the per-thread status is meaningless (no binding ever) — so
|
|
4708
5232
|
// we promote /status to a workspace-wide overview there. Topical threads
|
|
@@ -4725,7 +5249,7 @@ command('status', async (_ctx, key) => {
|
|
|
4725
5249
|
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
4726
5250
|
const isActive = adapter.checkIsActive(key);
|
|
4727
5251
|
const adapterName = (0, createAdapter_1.getThreadAdapterNameRaw)(key);
|
|
4728
|
-
const agentLine = adapterName ?
|
|
5252
|
+
const agentLine = adapterName ? formatStatusAgentLine(adapter.label, adapterName) : (0, i18n_1.t)('status.thread_no_agent');
|
|
4729
5253
|
const binding = state.getBinding(key);
|
|
4730
5254
|
const subdir = binding?.subdir ?? (0, i18n_1.t)('status.thread_no_binding');
|
|
4731
5255
|
let workDir = null;
|
|
@@ -4901,6 +5425,19 @@ command('timezone', async (_ctx, key, parsed) => {
|
|
|
4901
5425
|
const view = buildTimezoneRegionView();
|
|
4902
5426
|
await replyToThread(key, view.text, view.keyboard);
|
|
4903
5427
|
});
|
|
5428
|
+
/**
|
|
5429
|
+
* @description How the welcome's shared-folder row writes `peerKey`, a topic of the SAME chat as the one being bound: by the
|
|
5430
|
+
* name stored on its binding, else by the link that opens it (a person can follow it; a session key means nothing to them).
|
|
5431
|
+
* The link is the numeric `t.me/c/…` form, which opens a topic for any member of the group. `null` for a topic that can be
|
|
5432
|
+
* neither named nor linked (a thread of a private chat, say) — it is only counted, never shown as an id.
|
|
5433
|
+
*/
|
|
5434
|
+
function describeSharedTopic(peerKey) {
|
|
5435
|
+
const name = state.getBinding(peerKey)?.topicName;
|
|
5436
|
+
const link = (0, topicCommand_1.buildTopicLink)({ chatId: (0, sessionKeyCodec_1.getTelegramChatId)(peerKey), username: null }, (0, sessionKeyCodec_1.getTelegramThreadId)(peerKey));
|
|
5437
|
+
if (name === undefined && link === null)
|
|
5438
|
+
return null;
|
|
5439
|
+
return { name, link: link ?? '' };
|
|
5440
|
+
}
|
|
4904
5441
|
async function applyBinding(key, rawSubdir, options = {}) {
|
|
4905
5442
|
let subdir;
|
|
4906
5443
|
try {
|
|
@@ -4920,18 +5457,88 @@ async function applyBinding(key, rawSubdir, options = {}) {
|
|
|
4920
5457
|
// user should know they're about to join an existing workspace rather
|
|
4921
5458
|
// than start a fresh one.
|
|
4922
5459
|
const peers = state.listKeysForSubdir(subdir).filter(k => (0, sessionKeyCodec_1.checkIsTelegramKey)(k) && (0, sessionKey_1.keyToString)(k) !== (0, sessionKey_1.keyToString)(key));
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
5460
|
+
const topicName = options.topicName ?? (0, pendingTopicName_1.getTrustedPendingTopicName)(pendingTopicNames.get((0, sessionKey_1.keyToString)(key)), Date.now(), PENDING_TOPIC_NAME_TTL_MS);
|
|
5461
|
+
await state.setBinding(key, subdir, topicName !== undefined ? { topicName } : {});
|
|
5462
|
+
// Bound: the set-aside name has done its part (an unbound topic is the only reader of it), and a «create new folder» wait
|
|
5463
|
+
// armed on a picker is over — the next plain message is nobody's folder name any more.
|
|
5464
|
+
const boundKey = (0, sessionKey_1.keyToString)(key);
|
|
5465
|
+
pendingTopicNames.delete(boundKey);
|
|
5466
|
+
awaitingFolderName.delete(boundKey);
|
|
5467
|
+
bindPickerMessageIds.delete(boundKey);
|
|
5468
|
+
warmTodoProjects();
|
|
5469
|
+
// Only topics of THIS chat are named: a member of one group is not told what another group (or a private chat) calls its
|
|
5470
|
+
// topics. The rest — and a topic of this chat that can be neither named nor linked — are counted.
|
|
5471
|
+
const sharedWith = [];
|
|
5472
|
+
let sharedMore = 0;
|
|
5473
|
+
for (const peerKey of peers) {
|
|
5474
|
+
const described = peerKey.space === key.space ? describeSharedTopic(peerKey) : null;
|
|
5475
|
+
if (described === null)
|
|
5476
|
+
sharedMore += 1;
|
|
5477
|
+
else
|
|
5478
|
+
sharedWith.push(described);
|
|
5479
|
+
}
|
|
4930
5480
|
// Rebind resume (S8): a thread that was unbound paused its schedules; binding
|
|
4931
5481
|
// it again (this thread, possibly a different folder) resumes them. Best
|
|
4932
5482
|
// effort — a folder pick must succeed even if the resume bookkeeping hiccups.
|
|
4933
5483
|
await resumeThreadSchedulesOnRebind(key).catch((e) => console.warn(`[scheduler] resume-on-rebind failed for ${(0, sessionKey_1.keyToString)(key)}:`, e));
|
|
4934
|
-
return { ok: true,
|
|
5484
|
+
return { ok: true, subdir, sharedWith, sharedMore };
|
|
5485
|
+
}
|
|
5486
|
+
/**
|
|
5487
|
+
* @description Answer a bind attempt in the topic: the refusal's message, or — once bound — the rich welcome, the one
|
|
5488
|
+
* message announcing the binding (with the shared-folder row when other topics use the folder, and whatever else the
|
|
5489
|
+
* caller knows of this bind: `notes`). Shared by `/bind <subdir>`, the `bind_<subdir>` picker button and the picker's
|
|
5490
|
+
* «create new folder».
|
|
5491
|
+
*/
|
|
5492
|
+
async function replyToBindResult(key, result, notes = {}) {
|
|
5493
|
+
if (!result.ok) {
|
|
5494
|
+
await replyToThread(key, result.message);
|
|
5495
|
+
return;
|
|
5496
|
+
}
|
|
5497
|
+
await sendBindingWelcome(key, result.subdir, { ...notes, sharedWith: result.sharedWith, sharedMore: result.sharedMore });
|
|
5498
|
+
}
|
|
5499
|
+
/**
|
|
5500
|
+
* @description The `/bind` picker is one-shot (§ Inline keyboards): once a folder is bound through it — or the topic left its
|
|
5501
|
+
* folder from it — the message is deleted, since its text («not bound, pick from the list», «currently bound: …») is no
|
|
5502
|
+
* longer true and the welcome replaces it; a stale tap cannot rebind the topic either. A pick that bound nothing never gets
|
|
5503
|
+
* here, and paging or the create-folder prompt leave the picker alone. A message that stayed ({@link deleteThreadMessage}
|
|
5504
|
+
* resolved `false`: older than 48 hours, say) loses at least its buttons.
|
|
5505
|
+
*/
|
|
5506
|
+
async function removeBindPicker(key, pickerMessageId) {
|
|
5507
|
+
openBindMessages.forget((0, sessionKey_1.keyToString)(key), pickerMessageId);
|
|
5508
|
+
// A message that is gone has no buttons to strip (and Telegram would only answer «not found»).
|
|
5509
|
+
if (await deleteThreadMessageDetailed(key, pickerMessageId) === 'stayed')
|
|
5510
|
+
await editThreadKeyboard(key, pickerMessageId, null, 'bind');
|
|
5511
|
+
}
|
|
5512
|
+
/**
|
|
5513
|
+
* @description Delete every message still open in the topic that offers a bind or asks for a folder's name — each `/bind`
|
|
5514
|
+
* picker (every plain message in an unbound topic posted another), the «not bound» replies, the «send the folder name»
|
|
5515
|
+
* prompt — once the topic is bound or has left its folder, whichever way that happened (a tap, a typed `/bind <folder>`, the
|
|
5516
|
+
* bot's own auto-bind, a leave): their text is no longer true, and a tap on a stale one would rebind the topic. A message
|
|
5517
|
+
* the bot has no record of (sent before a restart) is not touched.
|
|
5518
|
+
*
|
|
5519
|
+
* Every one of them is held first ({@link BindPickerClaims.retire}, outside the limited claim list), so a tap is refused
|
|
5520
|
+
* whatever becomes of its message; the hold is let go of only for the ones with nothing left to tap. ONE bulk call deletes them
|
|
5521
|
+
* (each single delete is a paced send, and there can be dozens). A message too old for Telegram to delete loses its buttons
|
|
5522
|
+
* instead, and so does every one of a bulk call Telegram refused — no delete per picker — but only the newest `keyboardStripsPerSweepLimit` of them; the
|
|
5523
|
+
* older rest keeps its buttons, refused by the hold (`sweepOpenBindMessages`). The hold lives in memory: after a restart such a
|
|
5524
|
+
* picker works again. Never rejects: the callers that do not wait for it ({@link sendBindingWelcome}) have nothing to catch.
|
|
5525
|
+
*/
|
|
5526
|
+
async function closeOpenBindMessages(key) {
|
|
5527
|
+
try {
|
|
5528
|
+
await (0, bindSweep_1.sweepOpenBindMessages)(openBindMessages.take((0, sessionKey_1.keyToString)(key)), Date.now(), {
|
|
5529
|
+
retire: (messageId) => bindPickerClaims.retire(key, messageId),
|
|
5530
|
+
unretire: (messageId) => bindPickerClaims.unretire(key, messageId),
|
|
5531
|
+
deleteMessages: (messageIds) => deleteThreadMessages(key, messageIds),
|
|
5532
|
+
removeKeyboard: (messageId) => editThreadKeyboard(key, messageId, null, 'bind'),
|
|
5533
|
+
});
|
|
5534
|
+
}
|
|
5535
|
+
catch (e) {
|
|
5536
|
+
console.warn(`[bind] closing the open pickers of ${(0, sessionKey_1.keyToString)(key)} failed:`, e);
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
/** Remove the buttons of the topic's earlier welcomes (the newest few; see `stripStaleKeyboards`). Never rejects. */
|
|
5540
|
+
async function stripWelcomeKeyboards(key, messageIds) {
|
|
5541
|
+
await (0, bindSweep_1.stripStaleKeyboards)(messageIds, (messageId) => editThreadKeyboard(key, messageId, null, 'welcome'));
|
|
4935
5542
|
}
|
|
4936
5543
|
/**
|
|
4937
5544
|
* @description Pause the AGENT-PROMPT schedules owned by `key` because its topic
|
|
@@ -5002,16 +5609,18 @@ async function resumeThreadSchedulesOnRebind(key) {
|
|
|
5002
5609
|
* armed-mode branches in the text handler never overlap (only one armed
|
|
5003
5610
|
* mode is active per thread at a time).
|
|
5004
5611
|
*/
|
|
5005
|
-
function armFolderCreation(key) {
|
|
5612
|
+
function armFolderCreation(key, pickerMessageId) {
|
|
5006
5613
|
const kStr = (0, sessionKey_1.keyToString)(key);
|
|
5007
5614
|
awaitingModelSelection.delete(kStr);
|
|
5008
5615
|
awaitingSessionSelection.delete(kStr);
|
|
5009
5616
|
// The `/topic` prompts are other readers of the next plain message.
|
|
5010
5617
|
cancelTopicInput(key);
|
|
5011
5618
|
// A stale pending question would otherwise consume the message AFTER the
|
|
5012
|
-
// folder name as an answer to the old (pre-rebind) agent question.
|
|
5013
|
-
|
|
5619
|
+
// folder name as an answer to the old (pre-rebind) agent question. Its message stays, closed as unanswered.
|
|
5620
|
+
retirePendingQuestionInBackground(key);
|
|
5014
5621
|
awaitingFolderName.add(kStr);
|
|
5622
|
+
if (pickerMessageId !== null)
|
|
5623
|
+
bindPickerMessageIds.set(kStr, pickerMessageId);
|
|
5015
5624
|
}
|
|
5016
5625
|
/** Map a `validateNewFolderName` rejection to its localised reply. */
|
|
5017
5626
|
function mapNewFolderError(reason) {
|
|
@@ -5027,10 +5636,10 @@ function mapNewFolderError(reason) {
|
|
|
5027
5636
|
/**
|
|
5028
5637
|
* @description Create a new folder under WORK_ROOT from a user-typed name:
|
|
5029
5638
|
* validate the name first (no traversal/slashes/dots), then `mkdir` (recursive:
|
|
5030
|
-
* an already-existing folder is fine — the caller just uses it, and
|
|
5031
|
-
*
|
|
5032
|
-
* the outcome says which, so the caller knows whether a retry
|
|
5033
|
-
* (`invalid`) or not (`failed`).
|
|
5639
|
+
* an already-existing folder is fine — the caller just uses it, and says so in its
|
|
5640
|
+
* own way: `alreadyExisted`). A refusal or a filesystem failure is reported to
|
|
5641
|
+
* `key`'s topic here; the outcome says which, so the caller knows whether a retry
|
|
5642
|
+
* is still wanted (`invalid`) or not (`failed`).
|
|
5034
5643
|
*/
|
|
5035
5644
|
async function createWorkFolder(key, rawName) {
|
|
5036
5645
|
const validated = (0, folderName_1.validateNewFolderName)(rawName);
|
|
@@ -5052,10 +5661,7 @@ async function createWorkFolder(key, rawName) {
|
|
|
5052
5661
|
// Folder freshly on disk → the cached subdir list is stale; drop it so the
|
|
5053
5662
|
// next picker render includes the new folder.
|
|
5054
5663
|
subdirCache.clear();
|
|
5055
|
-
|
|
5056
|
-
await replyToThread(key, (0, i18n_1.t)('bind.create_exists', { subdir: validated.name }));
|
|
5057
|
-
}
|
|
5058
|
-
return { outcome: 'ready', subdir: validated.name };
|
|
5664
|
+
return { outcome: 'ready', subdir: validated.name, alreadyExisted };
|
|
5059
5665
|
}
|
|
5060
5666
|
/**
|
|
5061
5667
|
* @description Create a new folder under WORK_ROOT from a user-typed name and
|
|
@@ -5068,21 +5674,36 @@ async function createWorkFolder(key, rawName) {
|
|
|
5068
5674
|
* filesystem failure disarms it.
|
|
5069
5675
|
*/
|
|
5070
5676
|
async function createAndBindFolder(key, rawName) {
|
|
5071
|
-
const
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
await
|
|
5080
|
-
|
|
5677
|
+
const kStr = (0, sessionKey_1.keyToString)(key);
|
|
5678
|
+
// Read before the bind: a bind drops it (and the wait) itself; deleted below for a refused one, which does not.
|
|
5679
|
+
const pickerMessageId = bindPickerMessageIds.get(kStr) ?? null;
|
|
5680
|
+
let isNameRefused = false;
|
|
5681
|
+
// The picker this name was asked on is held from BEFORE the folder is made, for the whole bind, like a tap on a folder holds
|
|
5682
|
+
// it: a tap on it that lands behind the typed name is told the picker was used instead of binding the topic a second time, and
|
|
5683
|
+
// a name that arrives when a tap already holds the picker makes no (empty) folder — it is told so, not dropped in silence.
|
|
5684
|
+
const run = await bindPickerClaims.runClaimed(key, pickerMessageId, async () => {
|
|
5685
|
+
const folder = await createWorkFolder(key, rawName);
|
|
5686
|
+
if (folder.outcome === 'invalid') {
|
|
5687
|
+
isNameRefused = true;
|
|
5688
|
+
return false;
|
|
5689
|
+
}
|
|
5690
|
+
if (folder.outcome === 'failed')
|
|
5691
|
+
return false;
|
|
5692
|
+
const result = await applyBinding(key, folder.subdir);
|
|
5693
|
+
bindPickerMessageIds.delete(kStr);
|
|
5694
|
+
if (result.ok && pickerMessageId !== null)
|
|
5695
|
+
await removeBindPicker(key, pickerMessageId);
|
|
5696
|
+
await replyToBindResult(key, result, { isExistingFolder: folder.alreadyExisted });
|
|
5697
|
+
return result.ok;
|
|
5698
|
+
});
|
|
5699
|
+
if (run === 'busy')
|
|
5700
|
+
await replyToThread(key, (0, i18n_1.t)('cb.bind_picker_used'));
|
|
5701
|
+
return { ok: !isNameRefused };
|
|
5081
5702
|
}
|
|
5082
5703
|
/**
|
|
5083
5704
|
* @description Bind a topic the bot has just created (`/topic`) to `subdir` and greet it with the usual binding welcome:
|
|
5084
|
-
* the folder's stats and the start buttons. No agent starts by itself: it is started there as in any bound topic. The
|
|
5085
|
-
* is left out
|
|
5705
|
+
* the folder's stats and the start buttons. No agent starts by itself: it is started there as in any bound topic. The welcome's
|
|
5706
|
+
* shared-folder row is left out (`sharedWith` is not passed on): it would warn of what the person just asked for. The topic's name
|
|
5086
5707
|
* rides on the binding for the thread-context preamble. Resolves why it could not bind, `null` once it is bound.
|
|
5087
5708
|
*/
|
|
5088
5709
|
async function bindNewTopic(key, subdir, topicName) {
|
|
@@ -5092,6 +5713,22 @@ async function bindNewTopic(key, subdir, topicName) {
|
|
|
5092
5713
|
await sendBindingWelcome(key, result.subdir);
|
|
5093
5714
|
return null;
|
|
5094
5715
|
}
|
|
5716
|
+
/**
|
|
5717
|
+
* @description Bind a topic the bot matched to the folder `subdirName` by the topic's own name, and welcome it. It goes
|
|
5718
|
+
* through {@link applyBinding} like every other bind (validation, the schedules a rebind resumes, the collision lookup), so
|
|
5719
|
+
* the welcome says why the bot did it AND names the topics already on the folder. The topic's name rides on the binding for
|
|
5720
|
+
* the thread-context preamble. Resolves whether the topic is bound: a refused folder is only logged, and the caller falls
|
|
5721
|
+
* back to the folder picker.
|
|
5722
|
+
*/
|
|
5723
|
+
async function autoBindTopicByName(key, subdirName, topicName) {
|
|
5724
|
+
const result = await applyBinding(key, subdirName, topicName ? { topicName } : {});
|
|
5725
|
+
if (!result.ok) {
|
|
5726
|
+
console.warn(`[auto-bind] "${subdirName}" refused for ${(0, sessionKey_1.keyToString)(key)}: ${result.message}`);
|
|
5727
|
+
return false;
|
|
5728
|
+
}
|
|
5729
|
+
await sendBindingWelcome(key, result.subdir, { isAutoBound: true, sharedWith: result.sharedWith, sharedMore: result.sharedMore });
|
|
5730
|
+
return true;
|
|
5731
|
+
}
|
|
5095
5732
|
command('bind', async (_ctx, key, parsed) => {
|
|
5096
5733
|
if (checkIsGeneral(key)) {
|
|
5097
5734
|
await replyToThread(key, (0, i18n_1.t)('bind.in_general'));
|
|
@@ -5118,10 +5755,7 @@ command('bind', async (_ctx, key, parsed) => {
|
|
|
5118
5755
|
await replyToThread(key, usage, (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!binding));
|
|
5119
5756
|
return;
|
|
5120
5757
|
}
|
|
5121
|
-
|
|
5122
|
-
await replyToThread(key, result.message);
|
|
5123
|
-
if (result.ok)
|
|
5124
|
-
await sendBindingWelcome(key, result.subdir);
|
|
5758
|
+
await replyToBindResult(key, await applyBinding(key, arg));
|
|
5125
5759
|
});
|
|
5126
5760
|
/**
|
|
5127
5761
|
* @description Unbind a thread from its folder: stop any live session, drop the
|
|
@@ -5142,6 +5776,11 @@ async function unbindThread(key) {
|
|
|
5142
5776
|
// Order matters: drop the pin FIRST (while binding.pinnedStatusMessageId
|
|
5143
5777
|
// is still readable), then stop the session, then wipe the binding.
|
|
5144
5778
|
await clearPinnedStatus(key);
|
|
5779
|
+
// A question still on screen ends here with its «not answered» note: the stop below is reported only after the process
|
|
5780
|
+
// is gone (json-stream stops in the background), by which time the teardown has cleared the question's state and the
|
|
5781
|
+
// report finds nothing to close — the buttons would stay, dead, with no note.
|
|
5782
|
+
if (pendingQuestions.has(kStr))
|
|
5783
|
+
await retirePendingQuestion(key);
|
|
5145
5784
|
// Stop any running session before discarding the binding so we don't leave
|
|
5146
5785
|
// an orphan tmux/SSE stream pointing at a directory we no longer track.
|
|
5147
5786
|
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
@@ -5160,7 +5799,7 @@ async function unbindThread(key) {
|
|
|
5160
5799
|
}
|
|
5161
5800
|
// Release persisted session ids too — otherwise re-binding this thread
|
|
5162
5801
|
// later + a bot restart would resurrect the old session the user unbound.
|
|
5163
|
-
await
|
|
5802
|
+
await releaseConversationIds(key);
|
|
5164
5803
|
// Pause this thread's schedules BEFORE dropping the binding: a fire against
|
|
5165
5804
|
// an unbound topic can't deliver, so the jobs park (paused + disarmed) and
|
|
5166
5805
|
// the next /bind resumes them. `removeBinding` only touches bindings/agents/
|
|
@@ -5247,7 +5886,7 @@ command('list', async (_ctx, key) => {
|
|
|
5247
5886
|
await replyToThread(key, `${(0, i18n_1.t)('list.header', { count: rows.length })}\n${body}`, { parse_mode: 'Markdown' });
|
|
5248
5887
|
});
|
|
5249
5888
|
/**
|
|
5250
|
-
* @description `/new`
|
|
5889
|
+
* @description `/new` — stop the thread's current agent
|
|
5251
5890
|
* session and immediately start a fresh one in the SAME topic with the SAME
|
|
5252
5891
|
* adapter. The old session is RELEASED (not deleted): its transcript stays on
|
|
5253
5892
|
* disk so it's still resumable via `/sessions`, but a bot restart won't
|
|
@@ -5257,7 +5896,7 @@ command('list', async (_ctx, key) => {
|
|
|
5257
5896
|
* Guards mirror `handleStartCommand`: General has no binding/agent, so it just
|
|
5258
5897
|
* hints; an unbound topic gets the standard bind-required reply.
|
|
5259
5898
|
*/
|
|
5260
|
-
command(
|
|
5899
|
+
command('new', async (_ctx, key) => {
|
|
5261
5900
|
if (checkIsGeneral(key)) {
|
|
5262
5901
|
await replyToThread(key, (0, i18n_1.t)('new.general_hint'));
|
|
5263
5902
|
return;
|
|
@@ -5480,11 +6119,15 @@ function buildStartAgentKeyboard() {
|
|
|
5480
6119
|
}
|
|
5481
6120
|
/**
|
|
5482
6121
|
* @description Compose and send the rich post-bind welcome described in
|
|
5483
|
-
* plan §20.5: header with the bound subdir,
|
|
5484
|
-
*
|
|
5485
|
-
*
|
|
5486
|
-
|
|
5487
|
-
|
|
6122
|
+
* plan §20.5: header with the bound subdir, the optional rows (why it was bound
|
|
6123
|
+
* when the bot did it by itself, that the folder was there already, CLAUDE.md /
|
|
6124
|
+
* .mcp.json / git, which other topics use the folder), and an inline keyboard to start an agent. It is the ONE
|
|
6125
|
+
* message every way of binding leaves in the topic. Falls back gracefully when
|
|
6126
|
+
* stats can't be probed.
|
|
6127
|
+
*/
|
|
6128
|
+
async function sendBindingWelcome(key, subdir, notes = {}) {
|
|
6129
|
+
// The welcomes this topic got before are outdated the moment this one goes out: their buttons would act on the CURRENT folder.
|
|
6130
|
+
const outdatedWelcomeIds = openWelcomeMessages.take((0, sessionKey_1.keyToString)(key)).map((entry) => entry.messageId);
|
|
5488
6131
|
// Pin the status banner first so the user sees it land near the top of
|
|
5489
6132
|
// the thread before the slower stats probe finishes. Fire-and-forget —
|
|
5490
6133
|
// a failed pin (missing permissions, closed topic) just logs a warning
|
|
@@ -5499,6 +6142,10 @@ async function sendBindingWelcome(key, subdir) {
|
|
|
5499
6142
|
stats = { claudeMdSize: null, mcpServerCount: null, gitBranch: null, gitClean: null };
|
|
5500
6143
|
}
|
|
5501
6144
|
const lines = [(0, i18n_1.t)('binding.welcome.header', { subdir })];
|
|
6145
|
+
if (notes.isAutoBound)
|
|
6146
|
+
lines.push((0, i18n_1.t)('binding.welcome.auto_bound'));
|
|
6147
|
+
if (notes.isExistingFolder)
|
|
6148
|
+
lines.push((0, i18n_1.t)('binding.welcome.existed'));
|
|
5502
6149
|
if (stats.claudeMdSize)
|
|
5503
6150
|
lines.push((0, i18n_1.t)('binding.welcome.claude_md', { size: stats.claudeMdSize }));
|
|
5504
6151
|
if (stats.mcpServerCount !== null) {
|
|
@@ -5513,12 +6160,23 @@ async function sendBindingWelcome(key, subdir) {
|
|
|
5513
6160
|
else if (fs.existsSync(path.join(workDir, '.git')) === false) {
|
|
5514
6161
|
lines.push((0, i18n_1.t)('binding.welcome.git_none'));
|
|
5515
6162
|
}
|
|
6163
|
+
const sharedMore = notes.sharedMore ?? 0;
|
|
6164
|
+
if ((notes.sharedWith && notes.sharedWith.length > 0) || sharedMore > 0) {
|
|
6165
|
+
const moreLabel = sharedMore > 0 ? (0, i18n_1.t)('binding.welcome.sharedMore', { count: sharedMore }) : null;
|
|
6166
|
+
lines.push((0, i18n_1.t)('binding.welcome.shared', { threads: (0, bindingWelcome_1.formatSharedTopicList)(notes.sharedWith ?? [], moreLabel) }));
|
|
6167
|
+
}
|
|
5516
6168
|
lines.push('');
|
|
5517
6169
|
lines.push((0, i18n_1.t)('binding.welcome.start_prompt'));
|
|
5518
|
-
await replyToThread(key, lines.join('\n'), {
|
|
6170
|
+
const welcomeMessageId = await replyToThread(key, lines.join('\n'), {
|
|
5519
6171
|
parse_mode: 'Markdown',
|
|
5520
6172
|
...buildStartAgentKeyboard(),
|
|
5521
6173
|
});
|
|
6174
|
+
if (welcomeMessageId !== null)
|
|
6175
|
+
openWelcomeMessages.remember((0, sessionKey_1.keyToString)(key), welcomeMessageId, Date.now());
|
|
6176
|
+
// Every bind ends in this welcome, so every bind sweeps here — after the welcome, and NOT waited for: what follows a bind
|
|
6177
|
+
// (an agent start, when the first message was a start phrase) must not hang on the deletions and edits, which are paced sends.
|
|
6178
|
+
void stripWelcomeKeyboards(key, outdatedWelcomeIds);
|
|
6179
|
+
void closeOpenBindMessages(key);
|
|
5522
6180
|
}
|
|
5523
6181
|
// `open_sessions` callback wires the [📋 Resume…] button to the `/sessions`
|
|
5524
6182
|
// flow — same `handleSessionsList` helper the slash command calls into, so
|
|
@@ -5740,10 +6398,9 @@ async function handleAgentStart(key, adapterName, args) {
|
|
|
5740
6398
|
await replyToThread(key, (0, i18n_1.t)('thread.no_binding'), extra);
|
|
5741
6399
|
return;
|
|
5742
6400
|
}
|
|
5743
|
-
// "Claude Code" is one user-facing choice with
|
|
5744
|
-
//
|
|
5745
|
-
|
|
5746
|
-
adapterName = (0, createAdapter_1.resolveClaudeBackendName)(key);
|
|
6401
|
+
// "Claude Code" is one user-facing choice with several backends (json-stream,
|
|
6402
|
+
// per-turn, the deprecated tmux-scrape); open the thread's picked backend, else the default.
|
|
6403
|
+
adapterName = (0, createAdapter_1.resolveStartAdapterName)(key, adapterName);
|
|
5747
6404
|
// The other backend, with a conversation to hand over: ask first; the answer makes the switch.
|
|
5748
6405
|
const targetFamily = (0, backendSwitchPlan_1.getBackendFamily)(adapterName);
|
|
5749
6406
|
if (targetFamily !== null && await askBeforeCrossBackendSwitch(key, targetFamily, args))
|
|
@@ -5785,19 +6442,32 @@ command('claude', (_ctx, key, parsed) => handleStartCommand(parsed, key, 'claude
|
|
|
5785
6442
|
command(['opencode', 'oc'], (_ctx, key, parsed) => handleStartCommand(parsed, key, 'opencode'));
|
|
5786
6443
|
command('terminal', (_ctx, key, parsed) => handleStartCommand(parsed, key, 'terminal'));
|
|
5787
6444
|
registerProviderCommands();
|
|
5788
|
-
/** Human label for a Claude backend name (the
|
|
6445
|
+
/** Human label for a Claude backend name (the adapters share `label`
|
|
5789
6446
|
* "Claude Code", so the picker/notices need a distinguishing name). */
|
|
5790
6447
|
function getClaudeBackendLabel(name) {
|
|
5791
6448
|
if (name === adapterNames_1.claudeJsonStreamAdapterName)
|
|
5792
6449
|
return '⚡ JSON-stream';
|
|
5793
6450
|
if (name === adapterNames_1.claudePerTurnAdapterName)
|
|
5794
6451
|
return '🔁 Per-turn';
|
|
5795
|
-
return '🖥
|
|
6452
|
+
return '🖥 tmux';
|
|
6453
|
+
}
|
|
6454
|
+
/** The `/claude_mode` button text of a backend: its label, the deprecated tmux backend marked as such. A notice
|
|
6455
|
+
* keeps the plain label instead — the deprecation hint under it already says it. */
|
|
6456
|
+
function getClaudeBackendButtonLabel(name) {
|
|
6457
|
+
return name === adapterNames_1.claudeTmuxAdapterName ? (0, i18n_1.t)('claudeMode.tmuxButtonLabel') : getClaudeBackendLabel(name);
|
|
6458
|
+
}
|
|
6459
|
+
/** The one-line hint that the tmux backend is deprecated and which backend to use instead. */
|
|
6460
|
+
function getTmuxDeprecatedHint() {
|
|
6461
|
+
return (0, i18n_1.t)('claudeMode.tmuxDeprecatedHint', { recommended: getClaudeBackendLabel((0, createAdapter_1.getDefaultClaudeBackendName)()) });
|
|
6462
|
+
}
|
|
6463
|
+
/** `text` with the tmux deprecation hint under it when `backendName` is the tmux backend; unchanged otherwise. */
|
|
6464
|
+
function appendTmuxDeprecatedHint(backendName, text) {
|
|
6465
|
+
return backendName === adapterNames_1.claudeTmuxAdapterName ? `${text}\n${getTmuxDeprecatedHint()}` : text;
|
|
5796
6466
|
}
|
|
5797
6467
|
/** Build the `/claude_mode` picker: one button per Claude backend, `✓` on the
|
|
5798
6468
|
* current one. Callback data `ccmode_<adapterName>`. */
|
|
5799
6469
|
function buildClaudeModeKeyboard(current) {
|
|
5800
|
-
const buttons = [adapterNames_1.claudeJsonStreamAdapterName, adapterNames_1.claudePerTurnAdapterName,
|
|
6470
|
+
const buttons = [adapterNames_1.claudeJsonStreamAdapterName, adapterNames_1.claudePerTurnAdapterName, adapterNames_1.claudeTmuxAdapterName].map((name) => telegraf_1.Markup.button.callback(name === current ? `${getClaudeBackendButtonLabel(name)} ✓` : getClaudeBackendButtonLabel(name), `ccmode_${name}`));
|
|
5801
6471
|
return telegraf_1.Markup.inlineKeyboard(buttons, { columns: 1 });
|
|
5802
6472
|
}
|
|
5803
6473
|
/**
|
|
@@ -5864,14 +6534,14 @@ command('claude_mode', async (_ctx, key, parsed) => {
|
|
|
5864
6534
|
return;
|
|
5865
6535
|
}
|
|
5866
6536
|
if (action.kind === 'already') {
|
|
5867
|
-
await replyToThread(key, (0, i18n_1.t)('claudeMode.already', { label: getClaudeBackendLabel(action.backendName) }));
|
|
6537
|
+
await replyToThread(key, appendTmuxDeprecatedHint(action.backendName, (0, i18n_1.t)('claudeMode.already', { label: getClaudeBackendLabel(action.backendName) })));
|
|
5868
6538
|
return;
|
|
5869
6539
|
}
|
|
5870
6540
|
if (action.kind === 'switch') {
|
|
5871
|
-
await replyToThread(key, await applyClaudeBackendSwitch(key, action.backendName));
|
|
6541
|
+
await replyToThread(key, appendTmuxDeprecatedHint(action.backendName, await applyClaudeBackendSwitch(key, action.backendName)));
|
|
5872
6542
|
return;
|
|
5873
6543
|
}
|
|
5874
|
-
await replyToThread(key, (0, i18n_1.t)('claudeMode.pick', { label: getClaudeBackendLabel(action.currentBackendName) }), buildClaudeModeKeyboard(action.currentBackendName));
|
|
6544
|
+
await replyToThread(key, (0, i18n_1.t)('claudeMode.pick', { label: getClaudeBackendLabel(action.currentBackendName), hint: getTmuxDeprecatedHint() }), buildClaudeModeKeyboard(action.currentBackendName));
|
|
5875
6545
|
});
|
|
5876
6546
|
registerModelCommands();
|
|
5877
6547
|
const { registerDisplayModeCommands, registerDisplayModeCallbacks, } = (0, displayModes_1.createDisplayModes)(botCore);
|
|
@@ -5909,6 +6579,44 @@ const { applyAutoContinueLimits, consumeAutoContinueLimitsPicker, registerAutoCo
|
|
|
5909
6579
|
getRequestWakeUpEngine: () => requestWakeUpEngine,
|
|
5910
6580
|
});
|
|
5911
6581
|
registerAutoContinueLimitsCommands();
|
|
6582
|
+
// ── «Bot skills» switch (`/bot_skills`): `connectors/telegram/commands/botSkills.ts`, `utils/botSkills.ts` ──
|
|
6583
|
+
const { registerBotSkillsCommands, registerBotSkillsCallbacks } = (0, botSkills_2.createBotSkills)({
|
|
6584
|
+
...botCore,
|
|
6585
|
+
readBotSkills: botSkillsReader,
|
|
6586
|
+
readBotSkillsInstanceValue: () => (0, botSkills_1.readBotSkillsInstanceValue)(botSkillsSources),
|
|
6587
|
+
checkIsOpenCodeTopic: (key) => ((0, createAdapter_1.getThreadAdapterNameRaw)(key) ?? state.getAgent(key)?.name) === adapterNames_1.openCodeAdapterName,
|
|
6588
|
+
});
|
|
6589
|
+
registerBotSkillsCommands();
|
|
6590
|
+
// ── The cap on tracker task agents (`/task_agents`): `connectors/telegram/commands/taskAgents.ts`, `trackers/` ──
|
|
6591
|
+
const { registerTaskAgentsCommands, registerTaskAgentsCallbacks } = (0, taskAgents_1.createTaskAgents)({
|
|
6592
|
+
...botCore,
|
|
6593
|
+
readTaskAgentLimit: () => (0, taskAgentLimit_1.readTaskAgentLimit)(taskAgentLimitSources),
|
|
6594
|
+
// A General value just written may raise the cap: the waiting issues start at once.
|
|
6595
|
+
noteTaskAgentLimitChanged: () => taskAgentSlots?.noteLimitMayHaveChanged(),
|
|
6596
|
+
// The slots decide nothing on an instance that serves no tracker: its screen says nothing of them.
|
|
6597
|
+
readTaskAgentStatus: () => ([...ENV.servedPlatforms].some((platform) => platform !== 'telegram') ? taskAgentSlots?.getStatus() ?? null : null),
|
|
6598
|
+
});
|
|
6599
|
+
registerTaskAgentsCommands();
|
|
6600
|
+
// ── Paired runners (`/runners`): `connectors/telegram/commands/runners.ts`, the hub in `runners/` ──
|
|
6601
|
+
const runnersHub = (0, hubPaths_1.createHubContext)();
|
|
6602
|
+
const runnersCacheDir = (0, ssh_1.getRemoteRunCacheDir)();
|
|
6603
|
+
const { registerRunnersCommands, registerRunnersCallbacks, handleRunnersAddressText, cancelRunnersAddressWait, startRunnersWatcher } = (0, runners_1.createRunners)({
|
|
6604
|
+
...botCore,
|
|
6605
|
+
hub: runnersHub,
|
|
6606
|
+
// The pairing messages this instance sent: its own record (the hub registry is shared by every instance of the OS user).
|
|
6607
|
+
pairingMessagesPath: path.join((0, state_1.resolveDataDir)(), 'runner-pairing-messages.json'),
|
|
6608
|
+
sweepExpiredCodes: async (nowMs) => { await (0, hubPairing_1.sweepExpiredPairings)(runnersHub, nowMs); },
|
|
6609
|
+
readRunnerHost: (0, instanceSettings_1.createRunnerHostReader)(() => (0, instanceSettings_1.getInstanceSettingsPath)(), (line) => console.error(line)),
|
|
6610
|
+
saveRunnerHost: (address) => (0, instanceSettings_1.saveInstanceSettingsChange)((0, instanceSettings_1.getInstanceSettingsPath)(), { runnerHost: address }),
|
|
6611
|
+
getBotLabel: () => (bot.botInfo?.username === undefined ? os.hostname() : `@${bot.botInfo.username}`),
|
|
6612
|
+
readOverview: (nowMs) => (0, runnersOverview_1.readRunnersOverview)(runnersHub, runnersCacheDir, nowMs),
|
|
6613
|
+
probeOverview: (overview, nowMs) => (0, runnersOverview_1.probeRunnersOverview)(runnersHub, overview, runnersCacheDir, nowMs),
|
|
6614
|
+
listAddressCandidates: () => (0, hubAddressCandidates_1.listPublicAddressCandidates)(),
|
|
6615
|
+
scanHostKey: hubAddress_1.scanHostKey,
|
|
6616
|
+
ensureGateWrapper: () => { (0, gateWrapper_1.ensureGateWrapper)(runnersHub); },
|
|
6617
|
+
now: Date.now,
|
|
6618
|
+
});
|
|
6619
|
+
registerRunnersCommands();
|
|
5912
6620
|
// ── Claude account profiles (`/account`): `utils/claudeProfiles.ts`, `commands/claudeAccounts.ts` ──
|
|
5913
6621
|
/**
|
|
5914
6622
|
* @description The Claude config directory a conversation's NEXT process starts with: its resolved account
|
|
@@ -5941,7 +6649,13 @@ async function readClaudeAuthStatusUnder(configDir) {
|
|
|
5941
6649
|
}
|
|
5942
6650
|
catch (e) {
|
|
5943
6651
|
// A logged-out directory makes the CLI exit non-zero WITH its JSON on stdout: that is the answer.
|
|
5944
|
-
|
|
6652
|
+
const status = (0, claudeProfiles_1.parseClaudeAuthStatus)((0, claudeProfiles_1.readProcessErrorStdout)(e));
|
|
6653
|
+
// No answer at all (a probe killed by its timeout on a loaded host, a CLI that did not start): one line, so a
|
|
6654
|
+
// missing status can be told from a signed-out one.
|
|
6655
|
+
if (status === null) {
|
|
6656
|
+
console.log(`[accounts] the auth status probe under ${configDir ?? 'the default directory'} gave no status: ${(0, claudeProfiles_1.getProcessFailureReason)(e, claudeAuthStatusProbeTimeoutMs)}`);
|
|
6657
|
+
}
|
|
6658
|
+
return status;
|
|
5945
6659
|
}
|
|
5946
6660
|
}
|
|
5947
6661
|
const claudeAuthStatusProbeTimeoutMs = 5000;
|
|
@@ -6153,12 +6867,39 @@ function getTopicFallbackPosition(key) {
|
|
|
6153
6867
|
const parsed = label ? (0, fallbackChain_1.parseOpenCodeModelLabel)(label) : null;
|
|
6154
6868
|
return parsed ? { backend: 'opencode', ...parsed } : null;
|
|
6155
6869
|
}
|
|
6870
|
+
/**
|
|
6871
|
+
* @description Whether the account or model `key` runs on is marked limited now, and whether it is billed to API
|
|
6872
|
+
* credits — what the todo list's standalone reminder never fires into (D12).
|
|
6873
|
+
*/
|
|
6874
|
+
function getConversationAccountState(key) {
|
|
6875
|
+
const position = getTopicFallbackPosition(key);
|
|
6876
|
+
if (position === null)
|
|
6877
|
+
return { isLimited: false, isApiBilled: false };
|
|
6878
|
+
const nowMs = Date.now();
|
|
6879
|
+
const checkIsFuture = (instant) => instant !== undefined && Date.parse(instant) > nowMs;
|
|
6880
|
+
if (position.backend === 'opencode')
|
|
6881
|
+
return { isLimited: checkIsFuture(state.getOpenCodeModelLimitedUntil((0, fallbackChain_1.buildOpenCodeModelLabel)(position))), isApiBilled: false };
|
|
6882
|
+
const info = state.getClaudeProfileInfo(position.profile);
|
|
6883
|
+
return { isLimited: checkIsFuture(info?.limitedUntil) || info?.creditExhaustedAt !== undefined, isApiBilled: info?.billing === 'api' };
|
|
6884
|
+
}
|
|
6885
|
+
/**
|
|
6886
|
+
* @description A person wrote in the conversation (a message, voice, file, album; an issue's request): the
|
|
6887
|
+
* compact-on-idle latch clears (D2), the todo reminder's silence after ignored reminders ends (D13), and a question the
|
|
6888
|
+
* idle compaction re-asked is closed as unanswered — they wrote something else instead of tapping it, like a live
|
|
6889
|
+
* question a message cancels, and its buttons must not be tapped later onto whatever the agent asks next. A tap on the
|
|
6890
|
+
* re-asked question takes its record before it gets here, so it is not closed by its own answer.
|
|
6891
|
+
*/
|
|
6892
|
+
function notePersonActivity(key) {
|
|
6893
|
+
noteThreadUserActivity(key);
|
|
6894
|
+
getTodoRegistry().notePersonActivity(key);
|
|
6895
|
+
closeReAskedQuestion(key);
|
|
6896
|
+
}
|
|
6156
6897
|
/**
|
|
6157
6898
|
* @description What a usage limit on `key` leads to (`utils/fallbackChain.ts`): the rung the topic is on is
|
|
6158
6899
|
* marked limited until the reset the error named (a Claude profile: else its session window, else the retry's
|
|
6159
6900
|
* blind delay; an OpenCode model: else the blind delay), the usage snapshots are brought up to date, and the
|
|
6160
6901
|
* chain's AVAILABLE rungs are listed in priority order. Without a configured chain a Claude topic falls back to
|
|
6161
|
-
* the within-Claude
|
|
6902
|
+
* the within-Claude ranking by the soonest weekly reset (`utils/claudeLimitSwitch.ts`) — the behaviour before the chain existed. An
|
|
6162
6903
|
* offer is remembered per thread under its episode so only ITS buttons can act on it. `none` for a non-Telegram
|
|
6163
6904
|
* conversation, a topic with no account to switch from, or when no rung can take over.
|
|
6164
6905
|
*/
|
|
@@ -6261,35 +7002,58 @@ function getBalanceSpentLine(from) {
|
|
|
6261
7002
|
function readRunningModelBeforeMove(key, from, to) {
|
|
6262
7003
|
return from?.backend === 'claude' && to.backend === 'claude' ? getThreadClaudeModel(key) : null;
|
|
6263
7004
|
}
|
|
7005
|
+
/** Where the topic's agent keeps its conversation: its adapter, the bound folder (`null` when unbound) and the persisted session id (`null` when it holds none yet). */
|
|
7006
|
+
function getConversationSource(key) {
|
|
7007
|
+
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
7008
|
+
const decision = getWorkDirStartDecision(key);
|
|
7009
|
+
const sessionId = (0, ensureSessionPlan_1.getPersistedSessionIdForAdapter)(state.getAgent(key) ?? null, adapter.name, (0, createAdapter_1.checkIsClaudeBackend)(adapter.name));
|
|
7010
|
+
return { adapter, workDir: decision.ok ? decision.workDir : null, sessionId };
|
|
7011
|
+
}
|
|
6264
7012
|
/**
|
|
6265
7013
|
* @description The last `limit` turns (oldest first) of the conversation the topic's agent holds, off its own
|
|
6266
7014
|
* transcript. `[]` when it has none yet (no session id persisted); `null` when they cannot be read (no bound
|
|
6267
7015
|
* folder, an adapter without a transcript reader, a read that failed).
|
|
6268
7016
|
*/
|
|
6269
7017
|
async function readConversationTurns(key, limit) {
|
|
6270
|
-
const adapter = (
|
|
6271
|
-
const decision = getWorkDirStartDecision(key);
|
|
6272
|
-
const sessionId = (0, ensureSessionPlan_1.getPersistedSessionIdForAdapter)(state.getAgent(key) ?? null, adapter.name, (0, createAdapter_1.checkIsClaudeBackend)(adapter.name));
|
|
7018
|
+
const { adapter, workDir, sessionId } = getConversationSource(key);
|
|
6273
7019
|
if (!sessionId)
|
|
6274
7020
|
return [];
|
|
6275
|
-
if (
|
|
7021
|
+
if (workDir === null || !adapter.getRecentTurns)
|
|
6276
7022
|
return null;
|
|
6277
7023
|
try {
|
|
6278
|
-
return await adapter.getRecentTurns(key,
|
|
7024
|
+
return await adapter.getRecentTurns(key, workDir, sessionId, limit);
|
|
6279
7025
|
}
|
|
6280
7026
|
catch (e) {
|
|
6281
7027
|
console.warn(`[fallback] ${(0, sessionKey_1.keyToString)(key)}: the previous agent's turns are unreadable:`, e instanceof Error ? e.message : e);
|
|
6282
7028
|
return null;
|
|
6283
7029
|
}
|
|
6284
7030
|
}
|
|
7031
|
+
/**
|
|
7032
|
+
* @description The summary the topic's agent's own compaction left in its conversation (`AgentAdapter.getSessionCompactionSummary`),
|
|
7033
|
+
* for the context bridge to hand on beside the turns; `null` when it has none, the backend keeps none, or it cannot be read.
|
|
7034
|
+
*/
|
|
7035
|
+
async function readConversationCompactionSummary(key) {
|
|
7036
|
+
const { adapter, workDir, sessionId } = getConversationSource(key);
|
|
7037
|
+
if (!sessionId || workDir === null || !adapter.getSessionCompactionSummary)
|
|
7038
|
+
return null;
|
|
7039
|
+
try {
|
|
7040
|
+
return await adapter.getSessionCompactionSummary(key, workDir, sessionId);
|
|
7041
|
+
}
|
|
7042
|
+
catch (e) {
|
|
7043
|
+
console.warn(`[fallback] ${(0, sessionKey_1.keyToString)(key)}: the previous agent's compaction summary is unreadable:`, e instanceof Error ? e.message : e);
|
|
7044
|
+
return null;
|
|
7045
|
+
}
|
|
7046
|
+
}
|
|
6285
7047
|
/**
|
|
6286
7048
|
* @description The context bridge for a cross-backend switch (`utils/fallbackBridge.ts`), built BEFORE the old
|
|
6287
|
-
* adapter is stopped: its last turns off its own transcript (an unreadable one yields a bridge without turns)
|
|
7049
|
+
* adapter is stopped: its last turns off its own transcript (an unreadable one yields a bridge without turns) and the
|
|
7050
|
+
* summary of its own earlier compaction, if any.
|
|
6288
7051
|
* `reason` is why the agent changes — a usage limit or the operator's own switch.
|
|
6289
7052
|
*/
|
|
6290
7053
|
async function buildFallbackBridge(key, input) {
|
|
6291
7054
|
const turns = await readConversationTurns(key, fallbackBridge_1.fallbackBridgeTurnLimit) ?? [];
|
|
6292
|
-
|
|
7055
|
+
const compactionSummary = await readConversationCompactionSummary(key);
|
|
7056
|
+
return (0, fallbackBridge_1.buildFallbackBridgePrompt)({ ...input, turns, compactionSummary, replyLanguage: (0, i18n_1.getLocaleEndonym)((0, i18n_1.getActiveLang)()) });
|
|
6293
7057
|
}
|
|
6294
7058
|
/** The backend the topic's agent runs on, by name (`Claude` / `OpenCode`), for a label whose rung is unknown. */
|
|
6295
7059
|
function formatThreadBackend(key) {
|
|
@@ -6372,19 +7136,31 @@ async function switchTopicToFallbackEntry(key, from, to, options) {
|
|
|
6372
7136
|
}
|
|
6373
7137
|
firstPrompt = { text: bridge, isRequestPrompt: false };
|
|
6374
7138
|
}
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
7139
|
+
// From the end of the wait until the continue prompt is forwarded, the idle the limit left behind is not a turn end (the
|
|
7140
|
+
// same hold a retry kick has): the model pin below re-spawns the session for seconds, and a wake-up engine that polled
|
|
7141
|
+
// then would remind the agent on top of the prompt this move is about to send. A kick that moved the topic holds it already.
|
|
7142
|
+
const isHoldingTurnEnd = !apiRetryKicksInFlight.has(k);
|
|
7143
|
+
if (isHoldingTurnEnd)
|
|
7144
|
+
apiRetryKicksInFlight.add(k);
|
|
7145
|
+
try {
|
|
7146
|
+
cancelApiRetry(key); // retires the offer too: the switch consumed it
|
|
7147
|
+
const notice = `${getFallbackSwitchedNotice(from, to, runningModel)}${isStoppingBackground ? `\n${(0, i18n_1.t)('fallback.backgroundStopped')}` : ''}`;
|
|
7148
|
+
console.log(`[fallback] ${k}: switched from ${fromLabel} to ${toLabel} after a usage limit${firstPrompt ? ' (cross-backend, bridged)' : ''}; resuming`);
|
|
7149
|
+
await say(notice);
|
|
7150
|
+
await resumeTopicOnNewSession(key, {
|
|
7151
|
+
modelToPin: (0, fallbackChain_1.getFallbackModelToPin)(to, runningModel),
|
|
7152
|
+
// Read after the session is up: the open requests may have changed while it started.
|
|
7153
|
+
getFirstPrompt: () => firstPrompt ?? (0, limitHeldPrompts_1.getLimitResumeMessage)({
|
|
7154
|
+
continueNudge: (0, i18n_1.t)('apiRetry.continueNudge'),
|
|
7155
|
+
untakenRequestPrompt: (0, requestHeader_1.joinPromptsNotTakenIn)(requestLimitWaitAnswerDeps?.ledger.listOpenRequestsOf(key) ?? []),
|
|
7156
|
+
}),
|
|
7157
|
+
});
|
|
7158
|
+
return { isSwitched: true, notice };
|
|
7159
|
+
}
|
|
7160
|
+
finally {
|
|
7161
|
+
if (isHoldingTurnEnd)
|
|
7162
|
+
apiRetryKicksInFlight.delete(k);
|
|
7163
|
+
}
|
|
6388
7164
|
}
|
|
6389
7165
|
/**
|
|
6390
7166
|
* @description The move ACROSS backends both the usage-limit switch and the operator's manual switch make, first
|
|
@@ -6611,10 +7387,14 @@ async function takeFallbackSwitchOffer(key, episodeId, index) {
|
|
|
6611
7387
|
const { notice } = await switchTopicToFallbackEntry(key, offer.from, decision.to, { isAnnounced: false });
|
|
6612
7388
|
return notice;
|
|
6613
7389
|
}
|
|
6614
|
-
/**
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
7390
|
+
/**
|
|
7391
|
+
* The `claude_profiles` MCP tool's answer: snapshots refreshed on demand (a minute's cache; the account a resumed session
|
|
7392
|
+
* was stopped on is re-read at once), then the launch pick — a new agent, or the session's own account when it is warm.
|
|
7393
|
+
* The accounts the operator switched off in the `/limits` list are never picked for a new agent.
|
|
7394
|
+
*/
|
|
7395
|
+
async function listClaudeProfilesForAgent(query) {
|
|
7396
|
+
await (0, claudeUsageRefresher_1.refreshClaudeUsageForLaunch)(claudeUsageRefresher, { currentProfile: query.currentProfile, isStoppedByLimit: query.isStoppedByLimit, maxAgeMs: claudeUsageRefresher_1.claudeUsageOnDemandMaxAgeMs });
|
|
7397
|
+
return (0, claudeUsage_1.buildClaudeProfilesReport)((0, claudeUsage_1.buildClaudeProfileUsageViews)({ dataDir: (0, state_1.resolveDataDir)(), records: state.getAllClaudeProfileInfos() }), query.model, Date.now(), { currentProfile: query.currentProfile, isStoppedByLimit: query.isStoppedByLimit, excludedNames: (0, fallbackChain_1.getDisabledClaudeProfileNames)(state.getFallbackChain()) });
|
|
6618
7398
|
}
|
|
6619
7399
|
const { registerFallbackChainCommands, registerFallbackChainCallbacks, openAutoSwitchView, runAutoSwitchCommand, applyAutoSwitch } = (0, fallbackChain_2.createFallbackChain)({
|
|
6620
7400
|
...botCore,
|
|
@@ -6626,6 +7406,15 @@ const { registerFallbackChainCommands, registerFallbackChainCallbacks, openAutoS
|
|
|
6626
7406
|
getTopicPosition: getTopicFallbackPosition,
|
|
6627
7407
|
takeSwitchOffer: takeFallbackSwitchOffer,
|
|
6628
7408
|
});
|
|
7409
|
+
const { registerTodoCommands, registerTodoCallbacks } = (0, todo_1.createTodo)({
|
|
7410
|
+
...botCore,
|
|
7411
|
+
getTodoRegistry,
|
|
7412
|
+
readTodoSettings,
|
|
7413
|
+
getSettingsPath: () => (0, instanceSettings_1.getInstanceSettingsPath)(),
|
|
7414
|
+
listTodoProjects,
|
|
7415
|
+
getConversationLabel: getTodoConversationLabel,
|
|
7416
|
+
checkIsConversationAsleep: (conversationKey) => getTodoSessionActivity(conversationKey) === 'asleep',
|
|
7417
|
+
});
|
|
6629
7418
|
const { registerLimitsBoardCommands, registerLimitsBoardCallbacks } = (0, limitsBoard_1.createLimitsBoard)({
|
|
6630
7419
|
...botCore,
|
|
6631
7420
|
getInstanceTimezone: getCurrentTimezone,
|
|
@@ -6685,6 +7474,7 @@ registerClaudeAccountCommands();
|
|
|
6685
7474
|
registerTopicCommand();
|
|
6686
7475
|
registerFallbackChainCommands();
|
|
6687
7476
|
registerLimitsBoardCommands();
|
|
7477
|
+
registerTodoCommands();
|
|
6688
7478
|
registerSttCommands();
|
|
6689
7479
|
const { registerSelfUpdateCommands, registerSelfUpdateCallbacks } = (0, selfUpdate_1.createSelfUpdate)({
|
|
6690
7480
|
...botCore,
|
|
@@ -6801,8 +7591,10 @@ async function resumeSessionByIndex(key, idx, onExpired) {
|
|
|
6801
7591
|
// The ONLY resume path that posts the "last N messages" context block —
|
|
6802
7592
|
// silent re-attach (bot restart) and crash recovery must stay quiet.
|
|
6803
7593
|
await adapter.resumeSession(key, workDir, sessionId, { isWithRecentContext: true });
|
|
6804
|
-
// Another session took over this topic; it does not know the open request
|
|
7594
|
+
// Another session took over this topic; it does not know the open request, and the question the idle compaction re-asked
|
|
7595
|
+
// for the conversation it replaces would send its label into the picked one.
|
|
6805
7596
|
cancelConversationRequest(key);
|
|
7597
|
+
closeReAskedQuestion(key);
|
|
6806
7598
|
// Persist the PICKED id — without this the next restart re-attaches to
|
|
6807
7599
|
// whatever id the last fresh start wrote, silently dropping the user's
|
|
6808
7600
|
// pick (live incident 2026-06-10).
|
|
@@ -6819,7 +7611,7 @@ async function resumeSessionByIndex(key, idx, onExpired) {
|
|
|
6819
7611
|
}
|
|
6820
7612
|
command(['sessions', 'resume'], (_ctx, key) => handleSessionsList(key));
|
|
6821
7613
|
/**
|
|
6822
|
-
* @description `/
|
|
7614
|
+
* @description `/quitall` — kill every active agent across every thread.
|
|
6823
7615
|
*
|
|
6824
7616
|
* General-only because it's a workspace-wide operation that's easy to
|
|
6825
7617
|
* fire by mistake; restricting to General + needing an explicit command
|
|
@@ -6832,7 +7624,7 @@ command(['sessions', 'resume'], (_ctx, key) => handleSessionsList(key));
|
|
|
6832
7624
|
* adapter that thread is configured for and stop only if it's active —
|
|
6833
7625
|
* idempotent, so re-running is safe.
|
|
6834
7626
|
*/
|
|
6835
|
-
command(
|
|
7627
|
+
command('quitall', async (_ctx, key) => {
|
|
6836
7628
|
if (!checkIsGeneral(key)) {
|
|
6837
7629
|
await replyToThread(key, (0, i18n_1.t)('quit_all.general_only'));
|
|
6838
7630
|
return;
|
|
@@ -6852,7 +7644,7 @@ command(['quit-all', 'quitall'], async (_ctx, key) => {
|
|
|
6852
7644
|
active += result.attempted;
|
|
6853
7645
|
stopped += result.stopped.length;
|
|
6854
7646
|
// Same release-for-good semantics as `/quit`, per swept thread.
|
|
6855
|
-
await
|
|
7647
|
+
await releaseConversationIds(bKey);
|
|
6856
7648
|
}
|
|
6857
7649
|
if (active === 0) {
|
|
6858
7650
|
await replyToThread(key, (0, i18n_1.t)('quit_all.none_active'));
|
|
@@ -6907,7 +7699,7 @@ command(['quit', 'q'], async (_ctx, key) => {
|
|
|
6907
7699
|
if (!primaryActive) {
|
|
6908
7700
|
// A sleeping conversation is released for good: the next message starts a fresh one.
|
|
6909
7701
|
if (getResumableSessionId(key) !== null) {
|
|
6910
|
-
await
|
|
7702
|
+
await releaseConversationIds(key);
|
|
6911
7703
|
await replyToThread(key, (0, i18n_1.t)('agent.sleeping_released'));
|
|
6912
7704
|
return;
|
|
6913
7705
|
}
|
|
@@ -6928,8 +7720,14 @@ command(['quit', 'q'], async (_ctx, key) => {
|
|
|
6928
7720
|
await replyToThread(key, (0, i18n_1.t)('agent.exit_signal_sent', { label: adapter.label }));
|
|
6929
7721
|
return;
|
|
6930
7722
|
}
|
|
6931
|
-
// OpenCode, terminal, any future adapter: tear down via stopSession.
|
|
6932
|
-
|
|
7723
|
+
// OpenCode, terminal, any future adapter: tear down via stopSession. «Stopped» is said once it is: a `/claude` sent on the
|
|
7724
|
+
// reply must find the session gone, not «already running» — as one transition, so a start or resume waits behind it.
|
|
7725
|
+
await sessionTransitions.run((0, sessionKey_1.keyToString)(key), async () => {
|
|
7726
|
+
if (adapter.stopSessionAndWait)
|
|
7727
|
+
await adapter.stopSessionAndWait(key);
|
|
7728
|
+
else
|
|
7729
|
+
adapter.stopSession(key);
|
|
7730
|
+
});
|
|
6933
7731
|
// Explicit quit releases the session for good — no auto-reattach later.
|
|
6934
7732
|
await state.clearAgentSessionIds(key);
|
|
6935
7733
|
await replyToThread(key, (0, i18n_1.t)('agent.stopped', { label: adapter.label }));
|
|
@@ -7198,8 +7996,8 @@ command('schedule', async (ctx, key, parsed) => {
|
|
|
7198
7996
|
await replyToThread(key, result.message);
|
|
7199
7997
|
const isStarting = startupPromptBuffer.checkIsStarting((0, sessionKey_1.keyToString)(key));
|
|
7200
7998
|
const opening = await openTopicRequest(key, wrappedPrompt, { source: 'schedule', requesterId: (0, topicRequest_1.getTopicRequesterId)(ctx.from) });
|
|
7201
|
-
await deliverPromptOrBuffer(key, `${opening.header}${wrappedPrompt}`, isStarting);
|
|
7202
|
-
|
|
7999
|
+
const delivery = await deliverPromptOrBuffer(key, `${opening.header}${wrappedPrompt}`, isStarting);
|
|
8000
|
+
await settleTopicRequestAfterDelivery(key, opening, delivery);
|
|
7203
8001
|
});
|
|
7204
8002
|
const reminderWizards = new Map();
|
|
7205
8003
|
const { cancelReminderWizard, claimReminderTextCapture, expireReminderWizard, finishReminderWizard, registerReminderCommands, registerReminderCallbacks, } = (0, reminders_1.createReminders)({
|
|
@@ -7278,16 +8076,18 @@ command('clear_messages', async (ctx, key) => {
|
|
|
7278
8076
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
7279
8077
|
// Slash tokens the text handler must NOT forward to the agent. Covers every
|
|
7280
8078
|
// bot-owned command (incl. the raw-key controls c/y/n/enter/up/down/tab/esc)
|
|
7281
|
-
// PLUS the retired commands (agent/cancel/unbind/where/stop/stopall/stop-all)
|
|
7282
|
-
// their handlers were removed
|
|
7283
|
-
//
|
|
7284
|
-
//
|
|
8079
|
+
// PLUS the retired commands (agent/cancel/unbind/where/stop/stopall/stop-all)
|
|
8080
|
+
// and the spelling `quit-all`: their handlers were removed (`quit-all` never
|
|
8081
|
+
// worked — Telegram ends a command at the hyphen, so a typed `/quit-all` is
|
|
8082
|
+
// routed as `/quit`), but keeping the tokens here makes a stray `/where` (or
|
|
8083
|
+
// `/stop`, or an entity-less `/quit-all`) inert instead of typing a meaningless
|
|
8084
|
+
// prompt into the agent.
|
|
7285
8085
|
const botCommands = new Set([
|
|
7286
8086
|
'start', 'claude', 'opencode', 'oc', 'terminal', 'agent', 'sessions', 'resume', 'cancel', 'model', 'connect',
|
|
7287
|
-
'disconnect', 'stop', 'stopall', 'stop-all', 'status', 'c', 'y', 'n', 'enter', 'up', 'down', 'tab', 'esc', 'escape', 'interrupt', 'output', 'clear_messages',
|
|
7288
|
-
'bind', 'topic', 'unbind', 'where', 'ls', 'list', 'new', '
|
|
7289
|
-
'doctor', 'mcp', 'rename_session', 'compact', 'compact_on_idle', 'compact_summary', 'auto_continue_limits', 'limits', 'limits_autoswitch', 'trace', 'timestamps', 'timezone', 'schedule', 'reminders', 'thinking', 'tool_results',
|
|
7290
|
-
'subagent', 'claude_mode', 'effort', 'verbosity', 'quit', 'q', '
|
|
8087
|
+
'disconnect', 'stop', 'stopall', 'stop-all', 'quit-all', 'status', 'c', 'y', 'n', 'enter', 'up', 'down', 'tab', 'esc', 'escape', 'interrupt', 'output', 'clear_messages',
|
|
8088
|
+
'bind', 'topic', 'unbind', 'where', 'ls', 'list', 'new', 'whoami', 'version', 'help', 'language', 'lang',
|
|
8089
|
+
'doctor', 'mcp', 'rename_session', 'compact', 'compact_on_idle', 'compact_summary', 'bot_skills', 'task_agents', 'runners', 'auto_continue_limits', 'limits', 'limits_autoswitch', 'trace', 'timestamps', 'timezone', 'schedule', 'reminders', 'todo', 'thinking', 'tool_results',
|
|
8090
|
+
'subagent', 'claude_mode', 'effort', 'verbosity', 'quit', 'q', 'quitall', 'pair', 'account', 'accounts', 'fallback', 'stt', 'voice', 'speech', 'update',
|
|
7291
8091
|
]);
|
|
7292
8092
|
/**
|
|
7293
8093
|
* @description The bare `/clear` is no longer bot-owned (it was renamed to
|
|
@@ -7323,6 +8123,8 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7323
8123
|
cancelTopicInput(key);
|
|
7324
8124
|
// ... and an armed `/stt` address or key step.
|
|
7325
8125
|
await cancelSttInput(key);
|
|
8126
|
+
// ... and the `/runners` address step.
|
|
8127
|
+
cancelRunnersAddressWait(key);
|
|
7326
8128
|
if (botCommands.has(cmd))
|
|
7327
8129
|
return;
|
|
7328
8130
|
// `/login` on a json-stream Claude thread (idle or per-turn lifecycle) has no
|
|
@@ -7426,12 +8228,18 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7426
8228
|
return;
|
|
7427
8229
|
}
|
|
7428
8230
|
}
|
|
8231
|
+
// `/runners` → «Enter another address»: this General message is the address runners dial. A slash command wins over the wait.
|
|
8232
|
+
if (await handleRunnersAddressText(key, text))
|
|
8233
|
+
return;
|
|
7429
8234
|
// Session is mid-startup → buffer the prompt and replay it once the agent is
|
|
7430
8235
|
// ready, instead of dropping it into the "no agent running" guidance below.
|
|
7431
8236
|
if (startupPromptBuffer.checkIsStarting(kStr)) {
|
|
7432
8237
|
await bufferPromptDuringStartup(key, text, { source: 'text', requesterId: (0, topicRequest_1.getTopicRequesterId)(ctx.from) }, getReplyQuoteBlock(key, ctx.message));
|
|
7433
8238
|
return;
|
|
7434
8239
|
}
|
|
8240
|
+
// Set when this very message bound the topic: its welcome is already the answer, so the idle-thread hint at the end
|
|
8241
|
+
// stays out — but the message itself is still read as what it is (a start phrase starts the agent).
|
|
8242
|
+
let isJustAutoBound = false;
|
|
7435
8243
|
// Audit S2 / #5: if `forum_topic_created` was emitted by a non-allowed
|
|
7436
8244
|
// admin, we stashed the topic name in `pendingTopicNames` without
|
|
7437
8245
|
// binding. Now that an allowed user is engaging this thread, retry the
|
|
@@ -7441,29 +8249,28 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7441
8249
|
// explicitly chose to create a new folder, so this message is the name —
|
|
7442
8250
|
// don't let a stale topic-name cache hijack it into a different bind.
|
|
7443
8251
|
if (!checkIsGeneral(key) && !state.getBinding(key) && !awaitingFolderName.has(kStr)) {
|
|
7444
|
-
const
|
|
7445
|
-
|
|
8252
|
+
const pendingEntry = pendingTopicNames.get(kStr);
|
|
8253
|
+
const pending = (0, pendingTopicName_1.getFreshPendingTopicName)(pendingEntry, Date.now(), PENDING_TOPIC_NAME_TTL_MS);
|
|
8254
|
+
if (pending) {
|
|
7446
8255
|
const match = (0, validation_1.findAutobindSubdir)(pending.name, listAvailableSubdirs(ENV.workRoot));
|
|
7447
8256
|
if (match) {
|
|
7448
8257
|
try {
|
|
7449
|
-
const subdir = (0, validation_1.validateSubdir)(ENV.workRoot, match);
|
|
7450
|
-
// The pending entry carries the topic name (cached at creation) —
|
|
7451
|
-
// copy it onto the binding for the thread-context preamble (S1).
|
|
7452
|
-
await state.setBinding(key, subdir, pending.name ? { topicName: pending.name } : {});
|
|
7453
8258
|
pendingTopicNames.delete(kStr);
|
|
7454
|
-
|
|
7455
|
-
|
|
8259
|
+
// The name rides on the binding for the thread-context preamble (S1): an authorised user's as given, a member's
|
|
8260
|
+
// replaced by the folder's own name — the match is all the bot vets of it.
|
|
8261
|
+
isJustAutoBound = await autoBindTopicByName(key, match, (0, pendingTopicName_1.getAutoBindTopicName)(pending, match));
|
|
7456
8262
|
}
|
|
7457
8263
|
catch (e) {
|
|
7458
8264
|
console.warn(`[deferred-autobind] failed for "${match}":`, e);
|
|
7459
|
-
pendingTopicNames.delete(kStr);
|
|
7460
8265
|
}
|
|
7461
8266
|
}
|
|
7462
|
-
else {
|
|
8267
|
+
else if (!pending.isFromAdmin) {
|
|
8268
|
+
// No folder matches: a name only a non-admin gave has nothing else to do. An authorised user's stays for the
|
|
8269
|
+
// bind the picker leads to, which keeps it (until it is bound or the TTL runs out).
|
|
7463
8270
|
pendingTopicNames.delete(kStr);
|
|
7464
8271
|
}
|
|
7465
8272
|
}
|
|
7466
|
-
else if (
|
|
8273
|
+
else if (pendingEntry) {
|
|
7467
8274
|
// TTL expired — clean the entry.
|
|
7468
8275
|
pendingTopicNames.delete(kStr);
|
|
7469
8276
|
}
|
|
@@ -7533,12 +8340,13 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7533
8340
|
if (requestedFamily !== null && state.getBinding(key) && await askBeforeCrossBackendSwitch(key, requestedFamily, startMatch.args))
|
|
7534
8341
|
return;
|
|
7535
8342
|
// The shared `ensureAgentSession` does the bind-check + switch + start.
|
|
7536
|
-
// The explicit matched adapter wins (`preferredAdapterName`)
|
|
7537
|
-
//
|
|
7538
|
-
//
|
|
7539
|
-
//
|
|
8343
|
+
// The explicit matched adapter wins (`preferredAdapterName`), named the way
|
|
8344
|
+
// `/claude` names it: `claude` is the topic's Claude backend, never the bare
|
|
8345
|
+
// tmux one. The phrase's trailing args ride along. On `unbound` we keep this
|
|
8346
|
+
// path's own reply: the folder picker (plan §11 Этап 4 — starting without a
|
|
8347
|
+
// binding would silently launch against WORK_ROOT, which is almost never wanted).
|
|
7540
8348
|
const result = await ensureAgentSession(key, {
|
|
7541
|
-
preferredAdapterName: startMatch.adapterName,
|
|
8349
|
+
preferredAdapterName: (0, createAdapter_1.resolveStartAdapterName)(key, startMatch.adapterName),
|
|
7542
8350
|
args: startMatch.args,
|
|
7543
8351
|
});
|
|
7544
8352
|
if (!result.ok && result.reason === 'unbound') {
|
|
@@ -7628,6 +8436,8 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7628
8436
|
await replyToThread(key, (0, i18n_1.t)('thread.no_binding'), extra);
|
|
7629
8437
|
return;
|
|
7630
8438
|
}
|
|
8439
|
+
if (isJustAutoBound)
|
|
8440
|
+
return;
|
|
7631
8441
|
await replyToThread(key, (0, i18n_1.t)('thread.no_agent_with_binding', { subdir: binding.subdir }));
|
|
7632
8442
|
});
|
|
7633
8443
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -7750,7 +8560,7 @@ async function processVoiceJob(key, fileId, durationSeconds, requesterId, sentAt
|
|
|
7750
8560
|
// Same choke point as the text trigger (L-D4): a sleeping conversation is
|
|
7751
8561
|
// woken, never replaced by a fresh start; `unbound` keeps the folder picker.
|
|
7752
8562
|
const result = await ensureAgentSession(key, {
|
|
7753
|
-
preferredAdapterName: startMatch.adapterName,
|
|
8563
|
+
preferredAdapterName: (0, createAdapter_1.resolveStartAdapterName)(key, startMatch.adapterName),
|
|
7754
8564
|
args: startMatch.args,
|
|
7755
8565
|
});
|
|
7756
8566
|
if (!result.ok && result.reason === 'unbound') {
|
|
@@ -7946,12 +8756,12 @@ const albumCollector = (0, mediaGroupCollector_1.createMediaGroupCollector)({
|
|
|
7946
8756
|
// cancel it silently, like the text/voice handlers.
|
|
7947
8757
|
cancelApiRetry(key);
|
|
7948
8758
|
// A user album is genuine user activity → clear the compact-on-idle latch (D2).
|
|
7949
|
-
|
|
8759
|
+
notePersonActivity(key);
|
|
7950
8760
|
try {
|
|
7951
8761
|
const opening = await openTopicRequest(key, promptText, { source: 'album', requesterId: orderedItems[0].requesterId });
|
|
7952
8762
|
const quotedPrompt = getPromptWithThreadReplyQuote(key, promptText, replyContext);
|
|
7953
|
-
await deliverPromptOrBuffer(key, `${opening.header}${quotedPrompt}`, isStarting);
|
|
7954
|
-
|
|
8763
|
+
const delivery = await deliverPromptOrBuffer(key, `${opening.header}${quotedPrompt}`, isStarting);
|
|
8764
|
+
await settleTopicRequestAfterDelivery(key, opening, delivery);
|
|
7955
8765
|
}
|
|
7956
8766
|
catch (err) {
|
|
7957
8767
|
console.error('[Bot] album flush failed:', err);
|
|
@@ -8030,12 +8840,12 @@ async function handleIncomingFile(ctx, key) {
|
|
|
8030
8840
|
// cancel it silently, like the text/voice handlers.
|
|
8031
8841
|
cancelApiRetry(key);
|
|
8032
8842
|
// A user file is genuine user activity → clear the compact-on-idle latch (D2).
|
|
8033
|
-
|
|
8843
|
+
notePersonActivity(key);
|
|
8034
8844
|
const opening = await openTopicRequest(key, promptText, { source: 'file', requesterId: (0, topicRequest_1.getTopicRequesterId)(ctx.from) });
|
|
8035
8845
|
// A file sent as a REPLY carries the message it answers, as a typed or spoken reply does.
|
|
8036
8846
|
const quotedPrompt = getPromptWithThreadReplyQuote(key, promptText, getReplyQuoteBlock(key, ctx.message));
|
|
8037
|
-
await deliverPromptOrBuffer(key, `${opening.header}${quotedPrompt}`, isStarting);
|
|
8038
|
-
|
|
8847
|
+
const delivery = await deliverPromptOrBuffer(key, `${opening.header}${quotedPrompt}`, isStarting);
|
|
8848
|
+
await settleTopicRequestAfterDelivery(key, opening, delivery);
|
|
8039
8849
|
}
|
|
8040
8850
|
bot.on(
|
|
8041
8851
|
// anyOf-composed per-kind filter — `message('photo', 'document', …)` would
|
|
@@ -8139,7 +8949,7 @@ bot.on((0, filters_1.message)('forum_topic_created'), async (ctx) => {
|
|
|
8139
8949
|
const existing = state.getBinding(key);
|
|
8140
8950
|
// Already bound — service event arrived after the user's first message
|
|
8141
8951
|
// (or on restart). Don't overwrite; quietly skip the welcome too, the
|
|
8142
|
-
// user has already seen the
|
|
8952
|
+
// user has already seen the welcome.
|
|
8143
8953
|
if (existing)
|
|
8144
8954
|
return;
|
|
8145
8955
|
// Audit S2 / #5: topic creation may be open to all members (group setting),
|
|
@@ -8156,7 +8966,7 @@ bot.on((0, filters_1.message)('forum_topic_created'), async (ctx) => {
|
|
|
8156
8966
|
return;
|
|
8157
8967
|
if (action === 'cacheNameOnly') {
|
|
8158
8968
|
if (topicName) {
|
|
8159
|
-
pendingTopicNames.set((0, sessionKey_1.keyToString)(key), { name: topicName, ts: Date.now() });
|
|
8969
|
+
pendingTopicNames.set((0, sessionKey_1.keyToString)(key), { name: topicName, ts: Date.now(), isFromAdmin: false });
|
|
8160
8970
|
}
|
|
8161
8971
|
console.warn(`[security] forum_topic_created in chat ${ctx.chat.id} by user ${userId ?? '?'} (not a group admin) — name cached, no auto-bind`);
|
|
8162
8972
|
return;
|
|
@@ -8169,19 +8979,20 @@ bot.on((0, filters_1.message)('forum_topic_created'), async (ctx) => {
|
|
|
8169
8979
|
const match = (0, validation_1.findAutobindSubdir)(topicName, subdirs);
|
|
8170
8980
|
if (match) {
|
|
8171
8981
|
try {
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
await state.setBinding(key, subdir, topicName ? { topicName } : {});
|
|
8176
|
-
await replyToThread(key, (0, i18n_1.t)('thread.welcome_bound', { subdir }));
|
|
8177
|
-
await sendBindingWelcome(key, subdir);
|
|
8178
|
-
return;
|
|
8982
|
+
// The creation event carries the topic name — it rides on the binding for the thread-context preamble (S1).
|
|
8983
|
+
if (await autoBindTopicByName(key, match, topicName))
|
|
8984
|
+
return;
|
|
8179
8985
|
}
|
|
8180
8986
|
catch (e) {
|
|
8181
8987
|
console.warn(`[forum_topic_created] auto-bind failed for "${match}":`, e);
|
|
8182
8988
|
// Fall through to picker.
|
|
8183
8989
|
}
|
|
8184
8990
|
}
|
|
8991
|
+
// The picker is the way on, and the bind through it is the topic's first: set the name an authorised user gave aside, so
|
|
8992
|
+
// that bind keeps it (the shared-folder row of a later topic names this one by it).
|
|
8993
|
+
if (topicName && !state.getBinding(key)) {
|
|
8994
|
+
pendingTopicNames.set((0, sessionKey_1.keyToString)(key), { name: topicName, ts: Date.now(), isFromAdmin: true });
|
|
8995
|
+
}
|
|
8185
8996
|
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
8186
8997
|
await replyToThread(key, (0, i18n_1.t)('thread.welcome_pick'), extra);
|
|
8187
8998
|
});
|
|
@@ -8213,7 +9024,7 @@ bot.on((0, filters_1.message)('forum_topic_edited'), async (ctx) => {
|
|
|
8213
9024
|
}
|
|
8214
9025
|
else {
|
|
8215
9026
|
// No binding yet — keep the freshest name for a later fuzzy auto-bind.
|
|
8216
|
-
pendingTopicNames.set((0, sessionKey_1.keyToString)(key), { name: newName, ts: Date.now() });
|
|
9027
|
+
pendingTopicNames.set((0, sessionKey_1.keyToString)(key), { name: newName, ts: Date.now(), isFromAdmin: true });
|
|
8217
9028
|
}
|
|
8218
9029
|
});
|
|
8219
9030
|
// Audit S2 / #5: closed/reopened events can come from a member who isn't an
|
|
@@ -8297,16 +9108,11 @@ bot.action(/^bind_page_(\d+)$/, async (ctx) => {
|
|
|
8297
9108
|
const page = parseInt(ctx.match[1], 10);
|
|
8298
9109
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
8299
9110
|
const keyboard = (0, bindPicker_1.buildBindKeyboard)(subdirs, page, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
8300
|
-
|
|
8301
|
-
await ctx.editMessageReplyMarkup(keyboard.reply_markup);
|
|
8302
|
-
}
|
|
8303
|
-
catch (e) {
|
|
8304
|
-
const desc = (0, sendErrorClassifier_1.checkIsApiError)(e) ? (0, sendErrorClassifier_1.getErrorDescription)(e) : '';
|
|
8305
|
-
if (!/message is not modified/i.test(desc)) {
|
|
8306
|
-
console.warn(`[bind_page] edit failed:`, desc || e);
|
|
8307
|
-
}
|
|
8308
|
-
}
|
|
9111
|
+
// Answered BEFORE the edit: the edit waits in the paced send queue, which can outlive the callback.
|
|
8309
9112
|
await ctx.answerCbQuery();
|
|
9113
|
+
const pickerMessageId = (0, callbackMessage_1.getCallbackMessageId)(ctx);
|
|
9114
|
+
if (pickerMessageId !== null)
|
|
9115
|
+
await editThreadKeyboard(key, pickerMessageId, keyboard.reply_markup, 'bind_page');
|
|
8310
9116
|
});
|
|
8311
9117
|
// Middle "N/M" pill in the nav row — pure UI, no state change.
|
|
8312
9118
|
bot.action(bindPicker_1.bindPageNoopCallback, async (ctx) => {
|
|
@@ -8326,9 +9132,18 @@ bot.action(bindPicker_1.bindCreateFolderCallback, async (ctx) => {
|
|
|
8326
9132
|
await ctx.answerCbQuery((0, i18n_1.t)('cb.bind_only_topical'));
|
|
8327
9133
|
return;
|
|
8328
9134
|
}
|
|
9135
|
+
const pickerMessageId = (0, callbackMessage_1.getCallbackMessageId)(ctx);
|
|
9136
|
+
// A picker a bind (or a leave) holds is spent: a name asked for now would bind a second time.
|
|
9137
|
+
if (pickerMessageId !== null && bindPickerClaims.has(key, pickerMessageId)) {
|
|
9138
|
+
await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.bind_picker_used'));
|
|
9139
|
+
return;
|
|
9140
|
+
}
|
|
8329
9141
|
await ctx.answerCbQuery((0, i18n_1.t)('bind.create_cb'));
|
|
8330
|
-
armFolderCreation(key);
|
|
8331
|
-
|
|
9142
|
+
armFolderCreation(key, pickerMessageId);
|
|
9143
|
+
// Remembered like a picker: the bind this question leads to deletes it (a refused name keeps it, the question stays open).
|
|
9144
|
+
const promptMessageId = await replyToThread(key, (0, i18n_1.t)('bind.create_prompt'));
|
|
9145
|
+
if (promptMessageId !== null)
|
|
9146
|
+
openBindMessages.remember((0, sessionKey_1.keyToString)(key), promptMessageId, Date.now());
|
|
8332
9147
|
});
|
|
8333
9148
|
// «Leave current dir» — the folded-in `/unbind`, shown only in a bound topic.
|
|
8334
9149
|
// Callback id deliberately avoids the `bind_` prefix so it can't be matched by
|
|
@@ -8343,8 +9158,20 @@ bot.action(bindPicker_1.bindLeaveCurrentCallback, async (ctx) => {
|
|
|
8343
9158
|
await ctx.answerCbQuery((0, i18n_1.t)('cb.bind_only_topical'));
|
|
8344
9159
|
return;
|
|
8345
9160
|
}
|
|
8346
|
-
|
|
8347
|
-
|
|
9161
|
+
// Claimed like a bind: two taps in a row unbind once, and a picker a bind holds is no more a place to leave from.
|
|
9162
|
+
const pickerMessageId = (0, callbackMessage_1.getCallbackMessageId)(ctx);
|
|
9163
|
+
const run = await bindPickerClaims.runClaimed(key, pickerMessageId, async () => {
|
|
9164
|
+
await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
|
|
9165
|
+
await unbindThread(key);
|
|
9166
|
+
if (pickerMessageId !== null)
|
|
9167
|
+
await removeBindPicker(key, pickerMessageId);
|
|
9168
|
+
await closeOpenBindMessages(key);
|
|
9169
|
+
// The welcome's buttons have no folder to act on now.
|
|
9170
|
+
await stripWelcomeKeyboards(key, openWelcomeMessages.take((0, sessionKey_1.keyToString)(key)).map((entry) => entry.messageId));
|
|
9171
|
+
return true;
|
|
9172
|
+
});
|
|
9173
|
+
if (run === 'busy')
|
|
9174
|
+
await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.bind_picker_used'));
|
|
8348
9175
|
});
|
|
8349
9176
|
// `/topic`'s buttons: patterns of their own (`topic_…`), disjoint from the `bind_<subdir>` regex below.
|
|
8350
9177
|
registerTopicCallbacks();
|
|
@@ -8358,12 +9185,26 @@ bot.action(/^bind_(.+)$/, async (ctx) => {
|
|
|
8358
9185
|
await ctx.answerCbQuery((0, i18n_1.t)('cb.bind_only_topical'));
|
|
8359
9186
|
return;
|
|
8360
9187
|
}
|
|
9188
|
+
// The picker is claimed before any await: a bind takes seconds (the folder's facts are probed, the sends are paced), and a
|
|
9189
|
+
// second tap landing meanwhile must not bind — or welcome — a second time. A bind that does not happen (a refused folder,
|
|
9190
|
+
// a failure on the way) gives the picker back.
|
|
8361
9191
|
const subdir = ctx.match[1];
|
|
8362
|
-
|
|
8363
|
-
const
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
await
|
|
9192
|
+
const pickerMessageId = (0, callbackMessage_1.getCallbackMessageId)(ctx);
|
|
9193
|
+
const run = await bindPickerClaims.runClaimed(key, pickerMessageId, async () => {
|
|
9194
|
+
// A toast that comes too late (the query expired) changes nothing about the bind the person asked for.
|
|
9195
|
+
await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.binding_to', { subdir }));
|
|
9196
|
+
const result = await applyBinding(key, subdir);
|
|
9197
|
+
if (!result.ok) {
|
|
9198
|
+
await replyToBindResult(key, result);
|
|
9199
|
+
return false;
|
|
9200
|
+
}
|
|
9201
|
+
if (pickerMessageId !== null)
|
|
9202
|
+
await removeBindPicker(key, pickerMessageId);
|
|
9203
|
+
await replyToBindResult(key, result);
|
|
9204
|
+
return true;
|
|
9205
|
+
});
|
|
9206
|
+
if (run === 'busy')
|
|
9207
|
+
await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.bind_picker_used'));
|
|
8367
9208
|
});
|
|
8368
9209
|
// ─── /language inline picker callbacks ───────────────────────────────────────
|
|
8369
9210
|
//
|
|
@@ -8501,10 +9342,14 @@ bot.action(timezonePicker_1.zonePickCallbackRe, async (ctx) => {
|
|
|
8501
9342
|
registerModelCallbacks();
|
|
8502
9343
|
registerCompactionCallbacks();
|
|
8503
9344
|
registerAutoContinueLimitsCallbacks();
|
|
9345
|
+
registerBotSkillsCallbacks();
|
|
9346
|
+
registerTaskAgentsCallbacks();
|
|
9347
|
+
registerRunnersCallbacks();
|
|
8504
9348
|
registerBackendSwitchCallbacks();
|
|
8505
9349
|
registerClaudeAccountCallbacks();
|
|
8506
9350
|
registerFallbackChainCallbacks();
|
|
8507
9351
|
registerLimitsBoardCallbacks();
|
|
9352
|
+
registerTodoCallbacks();
|
|
8508
9353
|
registerSttCallbacks();
|
|
8509
9354
|
registerSelfUpdateCallbacks();
|
|
8510
9355
|
// «⏭ Skip once» on the `/limits` board and the `/limits_autoswitch` screen: drop THIS armed resume and
|
|
@@ -8535,43 +9380,6 @@ bot.action(new RegExp(`^${autoContinueOnLimit_1.skipArmedRetryCallbackPrefix}(\\
|
|
|
8535
9380
|
await consumeAutoContinueLimitsPicker(key, ctx.callbackQuery?.message, (0, i18n_1.t)('autoContinueLimits.skippedNotice'));
|
|
8536
9381
|
});
|
|
8537
9382
|
});
|
|
8538
|
-
// D1: a tap on an idle-compaction RE-ASKED question button. The original request
|
|
8539
|
-
// was rejected server-side during the compaction, so the chosen option label is
|
|
8540
|
-
// delivered to the (now compacted) session as a FRESH prompt, not answered.
|
|
8541
|
-
bot.action(/^reask_(\d+)$/, async (ctx) => {
|
|
8542
|
-
const key = await authoriseContext(ctx);
|
|
8543
|
-
if (!key) {
|
|
8544
|
-
await ctx.answerCbQuery((0, i18n_1.t)('cb.access_denied'));
|
|
8545
|
-
return;
|
|
8546
|
-
}
|
|
8547
|
-
const optIdx = parseInt(ctx.match[1], 10);
|
|
8548
|
-
const labels = reAskedQuestionOptions.get((0, sessionKey_1.keyToString)(key));
|
|
8549
|
-
const label = labels?.[optIdx];
|
|
8550
|
-
if (!label) {
|
|
8551
|
-
await ctx.answerCbQuery((0, i18n_1.t)('cb.no_pending_question'));
|
|
8552
|
-
return;
|
|
8553
|
-
}
|
|
8554
|
-
// The idle fire posts the re-asked question just BEFORE it stops the process, and a stopping process still reads as
|
|
8555
|
-
// active: a tap inside that window (or inside a start) is buffered like a typed prompt and replayed once a session is up.
|
|
8556
|
-
const isStarting = startupPromptBuffer.checkIsStarting((0, sessionKey_1.keyToString)(key));
|
|
8557
|
-
// The answer to a re-asked question wakes a sleeping conversation (L-D4) — the question is its own.
|
|
8558
|
-
if (!isStarting && !(0, createAdapter_1.getThreadAdapter)(key).checkIsActive(key) && !(await resumeSleepingSessionForPrompt(key))) {
|
|
8559
|
-
await ctx.answerCbQuery((0, i18n_1.t)('agent.no_session'));
|
|
8560
|
-
return;
|
|
8561
|
-
}
|
|
8562
|
-
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
8563
|
-
reAskedQuestionOptions.delete((0, sessionKey_1.keyToString)(key));
|
|
8564
|
-
// The label goes to the agent as a prompt and nothing else in the topic shows it whole (the buttons clip long
|
|
8565
|
-
// labels): a label too long for the toast is posted in full, as the record of the answer that was sent.
|
|
8566
|
-
await (0, callbackToast_1.answerCallbackWithWholeText)(ctx, label, (wholeLabel) => replyToThread(key, wholeLabel));
|
|
8567
|
-
// An answer to the agent's own question is never a request (S7), even delivered as a prompt. Routed at delivery,
|
|
8568
|
-
// not on the `isStarting` read above: the stop may have ended during the toast, and a label buffered into its closed
|
|
8569
|
-
// window was never replayed.
|
|
8570
|
-
const outcome = await withThreadLocale(key, () => deliverActivePrompt(key, adapter, label, null));
|
|
8571
|
-
// The label reached no session (the topic was told why): the question is still open, so are its buttons.
|
|
8572
|
-
if (outcome === 'noSession' && labels && !reAskedQuestionOptions.has((0, sessionKey_1.keyToString)(key)))
|
|
8573
|
-
reAskedQuestionOptions.set((0, sessionKey_1.keyToString)(key), labels);
|
|
8574
|
-
});
|
|
8575
9383
|
registerReminderCallbacks();
|
|
8576
9384
|
bot.action(/^ccmode_(.+)$/, async (ctx) => {
|
|
8577
9385
|
const key = await authoriseContext(ctx);
|
|
@@ -8593,18 +9401,11 @@ bot.action(/^ccmode_(.+)$/, async (ctx) => {
|
|
|
8593
9401
|
return;
|
|
8594
9402
|
}
|
|
8595
9403
|
await ctx.answerCbQuery((0, i18n_1.t)('cb.claudeMode_switching'));
|
|
8596
|
-
await replyToThread(key, await applyClaudeBackendSwitch(key, target));
|
|
9404
|
+
await replyToThread(key, appendTmuxDeprecatedHint(target, await applyClaudeBackendSwitch(key, target)));
|
|
8597
9405
|
// Re-render the picker so the `✓` follows the new backend (mirrors effort_cb).
|
|
8598
|
-
const
|
|
8599
|
-
if (
|
|
8600
|
-
|
|
8601
|
-
await (0, rateLimiter_1.enqueueSend)(key, () => bot.telegram.editMessageReplyMarkup((0, sessionKeyCodec_1.getTelegramChatId)(key), cbMsg.message_id, undefined, buildClaudeModeKeyboard((0, createAdapter_1.resolveClaudeBackendName)(key)).reply_markup));
|
|
8602
|
-
}
|
|
8603
|
-
catch (e) {
|
|
8604
|
-
const desc = (0, sendErrorClassifier_1.checkIsApiError)(e) ? (0, sendErrorClassifier_1.getErrorDescription)(e) : '';
|
|
8605
|
-
if (!/message is not modified/i.test(desc))
|
|
8606
|
-
console.warn('[ccmode_cb] re-render:', desc || e);
|
|
8607
|
-
}
|
|
9406
|
+
const pickerMessageId = (0, callbackMessage_1.getCallbackMessageId)(ctx);
|
|
9407
|
+
if (pickerMessageId !== null) {
|
|
9408
|
+
await editThreadKeyboard(key, pickerMessageId, buildClaudeModeKeyboard((0, createAdapter_1.resolveClaudeBackendName)(key)).reply_markup, 'ccmode_cb');
|
|
8608
9409
|
}
|
|
8609
9410
|
});
|
|
8610
9411
|
registerDisplayModeCallbacks();
|
|
@@ -8632,18 +9433,9 @@ bot.action(/^agent_(.+)$/, async (ctx) => {
|
|
|
8632
9433
|
// instead of staying stuck on the previous one (B16, mirrors B12). The
|
|
8633
9434
|
// start keeps the picker message visible (it is neither edited nor deleted),
|
|
8634
9435
|
// so the stale marker would otherwise persist.
|
|
8635
|
-
const
|
|
8636
|
-
if (
|
|
8637
|
-
|
|
8638
|
-
try {
|
|
8639
|
-
await (0, rateLimiter_1.enqueueSend)(key, () => bot.telegram.editMessageReplyMarkup((0, sessionKeyCodec_1.getTelegramChatId)(key), cbMsg.message_id, undefined, keyboard.reply_markup));
|
|
8640
|
-
}
|
|
8641
|
-
catch (e) {
|
|
8642
|
-
const desc = (0, sendErrorClassifier_1.checkIsApiError)(e) ? (0, sendErrorClassifier_1.getErrorDescription)(e) : '';
|
|
8643
|
-
if (!/message is not modified/i.test(desc)) {
|
|
8644
|
-
console.warn('[agent_cb] keyboard re-render failed:', desc || e);
|
|
8645
|
-
}
|
|
8646
|
-
}
|
|
9436
|
+
const pickerMessageId = (0, callbackMessage_1.getCallbackMessageId)(ctx);
|
|
9437
|
+
if (pickerMessageId !== null) {
|
|
9438
|
+
await editThreadKeyboard(key, pickerMessageId, buildAgentKeyboard((0, createAdapter_1.getAvailableAdapters)(), adapterName).reply_markup, 'agent_cb');
|
|
8647
9439
|
}
|
|
8648
9440
|
}
|
|
8649
9441
|
catch {
|
|
@@ -8716,14 +9508,14 @@ bot.action(/^qa_(\d+)_(\d+)$/, async (ctx) => {
|
|
|
8716
9508
|
const selectedLabel = question.options[optIdx].label;
|
|
8717
9509
|
const isAnswerShown = await applyQuestionAnswer(key, [selectedLabel]);
|
|
8718
9510
|
if (isAnswerShown) {
|
|
8719
|
-
// The question message was just edited to
|
|
9511
|
+
// The question message was just edited to carry the whole answer under the question: the toast only
|
|
8720
9512
|
// confirms the tap, so a label too long for it is cut there and not posted a second time.
|
|
8721
|
-
await (0,
|
|
9513
|
+
await (0, callbackToast_2.answerCallbackWithFittedText)(ctx, selectedLabel);
|
|
8722
9514
|
}
|
|
8723
9515
|
else {
|
|
8724
9516
|
// The message could not be edited (it is gone, or the edit failed): nothing in the topic shows the answer, so a
|
|
8725
9517
|
// label too long for the toast is posted in full.
|
|
8726
|
-
await (0,
|
|
9518
|
+
await (0, callbackToast_2.answerCallbackWithWholeText)(ctx, selectedLabel, (wholeLabel) => replyToThread(key, wholeLabel));
|
|
8727
9519
|
}
|
|
8728
9520
|
});
|
|
8729
9521
|
/**
|
|
@@ -8735,6 +9527,13 @@ bot.action(/^qa_(\d+)_(\d+)$/, async (ctx) => {
|
|
|
8735
9527
|
* the agent without the message it points at.
|
|
8736
9528
|
*/
|
|
8737
9529
|
async function bufferPromptDuringStartup(key, text, intake, replyContext) {
|
|
9530
|
+
// The same for a bare option number typed in that window: it answers the question asked again.
|
|
9531
|
+
if (await answerReAskedQuestionByNumber(key, text))
|
|
9532
|
+
return;
|
|
9533
|
+
// A person wrote, in a window where the session is starting or being stopped: it counts like a message that reaches
|
|
9534
|
+
// `deliverActivePrompt` — the idle latch clears, and a question the idle compaction re-asked is closed (the idle fire
|
|
9535
|
+
// posts it just before the stop, so this window is where a message instead of a tap most often lands).
|
|
9536
|
+
notePersonActivity(key);
|
|
8738
9537
|
const opening = await openTopicRequest(key, text, intake);
|
|
8739
9538
|
const quotedText = getPromptWithThreadReplyQuote(key, text, replyContext);
|
|
8740
9539
|
await deliverPromptOrBuffer(key, `${opening.header}${quotedText}`, true);
|
|
@@ -8753,15 +9552,18 @@ async function bufferPromptDuringStartup(key, text, intake, replyContext) {
|
|
|
8753
9552
|
* user before forwarding so the prompt doesn't queue behind it forever.
|
|
8754
9553
|
* 3. Forward the prompt (a bare `/clear` first purges thread context + files).
|
|
8755
9554
|
*
|
|
8756
|
-
*
|
|
8757
|
-
*
|
|
9555
|
+
* Step 1 is the OpenCode AND the Claude stream-json backends (both raise the `question` event and implement
|
|
9556
|
+
* `answerQuestion`). The tmux-scrape Claude and the terminal never reach it (no `pendingQuestions` entry): for them
|
|
8758
9557
|
* `forwardPromptToAgent` interrupts the TUI selector itself.
|
|
8759
9558
|
*/
|
|
8760
9559
|
async function deliverActivePrompt(key, adapter, text, intake, sentAtMs, replyContext) {
|
|
9560
|
+
// A bare option number answers the question the idle compaction asked again (a tap's own label comes in with no intake).
|
|
9561
|
+
if (intake !== null && await answerReAskedQuestionByNumber(key, text))
|
|
9562
|
+
return null;
|
|
8761
9563
|
// A genuine USER message (text or voice) — clear the compact-on-idle latch so
|
|
8762
9564
|
// the next idle period can compact again (D2), whether it answers a pending
|
|
8763
9565
|
// question or is a fresh prompt.
|
|
8764
|
-
|
|
9566
|
+
notePersonActivity(key);
|
|
8765
9567
|
const pending = pendingQuestions.get((0, sessionKey_1.keyToString)(key));
|
|
8766
9568
|
if (pending && adapter.answerQuestion) {
|
|
8767
9569
|
const currentQuestion = pending.data.questions[pending.currentIndex];
|
|
@@ -8775,8 +9577,8 @@ async function deliverActivePrompt(key, adapter, text, intake, sentAtMs, replyCo
|
|
|
8775
9577
|
}
|
|
8776
9578
|
// Free-form text cancels the question and is a fresh prompt — a request (S7).
|
|
8777
9579
|
const opening = await openTopicRequest(key, text, intake);
|
|
8778
|
-
await cancelPendingQuestionAndForward(key, adapter, text, sentAtMs, replyContext, opening.header);
|
|
8779
|
-
|
|
9580
|
+
const delivery = await cancelPendingQuestionAndForward(key, adapter, text, sentAtMs, replyContext, opening.header);
|
|
9581
|
+
await settleTopicRequestAfterDelivery(key, opening, delivery);
|
|
8780
9582
|
return null;
|
|
8781
9583
|
}
|
|
8782
9584
|
// A bare `/clear` forwarded to the agent wipes its context (Claude TUI), so
|
|
@@ -8805,6 +9607,8 @@ async function deliverActivePrompt(key, adapter, text, intake, sentAtMs, replyCo
|
|
|
8805
9607
|
// durable by the time the agent reads it.
|
|
8806
9608
|
const opening = await openTopicRequest(key, text, intake);
|
|
8807
9609
|
// Routed now, not when the message arrived: the idle stop may have begun or ended during the awaits since.
|
|
9610
|
+
// The forward can itself end in no session — a prompt that waited for an adopt replay found the session gone and nothing to wake;
|
|
9611
|
+
// it has told the person already (`forwardPromptToAgent`), so what is left here is the request this prompt opened.
|
|
8808
9612
|
const outcome = await (0, promptDelivery_1.deliverLatePrompt)({
|
|
8809
9613
|
checkIsWindowOpen: () => startupPromptBuffer.checkIsStarting((0, sessionKey_1.keyToString)(key)),
|
|
8810
9614
|
checkIsSessionActive: () => adapter.checkIsActive(key),
|
|
@@ -8814,36 +9618,44 @@ async function deliverActivePrompt(key, adapter, text, intake, sentAtMs, replyCo
|
|
|
8814
9618
|
wake: () => wakeSleepingSessionForPrompt(key),
|
|
8815
9619
|
forward: () => forwardPromptToAgent(key, (0, createAdapter_1.getThreadAdapter)(key), text, sentAtMs, { replyContext, requestHeader: opening.header }),
|
|
8816
9620
|
reportNoSession: async (isAlreadyTold) => {
|
|
8817
|
-
// The prompt reaches no agent: the request it opened would only wait for a turn that never comes.
|
|
8818
|
-
if (opening.requestId !== null)
|
|
8819
|
-
await topicRequestLedger?.closeRequest(opening.requestId, 'cancelled');
|
|
8820
9621
|
if (!isAlreadyTold)
|
|
8821
9622
|
await replyToThread(key, (0, i18n_1.t)('agent.no_session'));
|
|
8822
9623
|
},
|
|
8823
9624
|
});
|
|
8824
|
-
|
|
8825
|
-
trackTopicRequestTurn(key, opening);
|
|
9625
|
+
await settleTopicRequestAfterDelivery(key, opening, outcome);
|
|
8826
9626
|
return outcome;
|
|
8827
9627
|
}
|
|
8828
9628
|
/**
|
|
8829
|
-
* @description
|
|
8830
|
-
* pending-question state and
|
|
8831
|
-
*
|
|
8832
|
-
*
|
|
9629
|
+
* @description Close the on-screen state of a pending question that ends without an answer: clear the bot's
|
|
9630
|
+
* pending-question state (which unpins it) and edit the question's message so its buttons are gone and a note says that
|
|
9631
|
+
* nobody answered it. The message is never deleted — the question and its options
|
|
9632
|
+
* stay readable. Closing the question on the agent is the caller's, before it stops or aborts the turn.
|
|
9633
|
+
* Returns whether the message itself was edited — it IS the notice then.
|
|
8833
9634
|
*
|
|
8834
9635
|
* The edit runs through `runQuestionLifecycleOp` (same serializer as the answer path) so it can't race a concurrent
|
|
8835
|
-
* re-post-to-bottom. The `messageId` +
|
|
9636
|
+
* re-post-to-bottom. The `messageId` + question are captured BEFORE clearing, since `clearPendingQuestion` drops the state.
|
|
8836
9637
|
*/
|
|
8837
9638
|
async function retirePendingQuestion(key) {
|
|
8838
9639
|
const pending = pendingQuestions.get((0, sessionKey_1.keyToString)(key));
|
|
8839
|
-
const cancelledMessageId = pending?.messageId ?? null;
|
|
8840
|
-
const cancelledQuestion = pending?.data.questions[pending.currentIndex];
|
|
8841
9640
|
clearPendingQuestion(key);
|
|
8842
|
-
|
|
9641
|
+
return closeCapturedQuestionMessage(key, pending?.messageId ?? null, pending?.data.questions[pending.currentIndex]);
|
|
9642
|
+
}
|
|
9643
|
+
/**
|
|
9644
|
+
* @description The edit half of {@link retirePendingQuestion}, for a question whose id and text were captured before its
|
|
9645
|
+
* state was cleared (also: read from the saved state at boot, where no memory entry ever existed). `false` when there is no
|
|
9646
|
+
* message to edit, else whether the edit landed.
|
|
9647
|
+
*/
|
|
9648
|
+
async function closeCapturedQuestionMessage(key, messageId, question) {
|
|
9649
|
+
if (messageId === null || !question)
|
|
8843
9650
|
return false;
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
9651
|
+
return runQuestionLifecycleOp(key, () => closeQuestionMessage(key, messageId, question, 'unanswered'));
|
|
9652
|
+
}
|
|
9653
|
+
/**
|
|
9654
|
+
* @description {@link retirePendingQuestion} for the teardown and boot paths that do not wait for the edit (it goes through
|
|
9655
|
+
* the send pacer): a failure is logged, never an unhandled rejection.
|
|
9656
|
+
*/
|
|
9657
|
+
function retirePendingQuestionInBackground(key) {
|
|
9658
|
+
retirePendingQuestion(key).catch((e) => console.error('[pendingQuestion] retire failed:', e));
|
|
8847
9659
|
}
|
|
8848
9660
|
/**
|
|
8849
9661
|
* @description Cancel a pending OpenCode question because the user sent free-form
|
|
@@ -8853,7 +9665,7 @@ async function retirePendingQuestion(key) {
|
|
|
8853
9665
|
* abort`), post the "previous question cancelled" notice, then forward.
|
|
8854
9666
|
*/
|
|
8855
9667
|
async function cancelPendingQuestionAndForward(key, adapter, text, sentAtMs, replyContext, requestHeader) {
|
|
8856
|
-
// When the question bubble itself was
|
|
9668
|
+
// When the question bubble itself was closed with its «not answered» note, that IS the
|
|
8857
9669
|
// cancellation notice — a second standalone "previous question cancelled" line is
|
|
8858
9670
|
// pure noise (the reported triple message). The standalone notice is kept only
|
|
8859
9671
|
// for the no-bubble case.
|
|
@@ -8871,7 +9683,7 @@ async function cancelPendingQuestionAndForward(key, adapter, text, sentAtMs, rep
|
|
|
8871
9683
|
if (!didLabelQuestionMessage) {
|
|
8872
9684
|
await replyToThread(key, (0, i18n_1.t)('agent.question_cancelled_for_prompt'));
|
|
8873
9685
|
}
|
|
8874
|
-
|
|
9686
|
+
return forwardPromptToAgent(key, adapter, text, sentAtMs, { replyContext, requestHeader });
|
|
8875
9687
|
}
|
|
8876
9688
|
/**
|
|
8877
9689
|
* @description Record one answer for the CURRENTLY shown OpenCode question and
|
|
@@ -8895,8 +9707,9 @@ async function applyQuestionAnswerInner(key, answerForCurrent) {
|
|
|
8895
9707
|
return false;
|
|
8896
9708
|
const answeredQuestion = pending.data.questions[pending.currentIndex];
|
|
8897
9709
|
const { nextState, action } = (0, openCodeQuestionFlow_1.recordAnswerAndAdvance)(pending, answerForCurrent);
|
|
8898
|
-
// Mark the answered question's message as done
|
|
8899
|
-
|
|
9710
|
+
// Mark the answered question's message as done: the question stays as asked, the chosen answer takes the place of
|
|
9711
|
+
// its buttons.
|
|
9712
|
+
const isAnswerShown = pending.messageId !== null && answeredQuestion !== undefined && await editQuestionMessageToClosed(key, pending.messageId, answeredQuestion, (0, i18n_1.t)('agent.question_answered_note', { answer: answerForCurrent.join(', ') }));
|
|
8900
9713
|
if (action.kind === 'submit') {
|
|
8901
9714
|
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
8902
9715
|
clearPendingQuestion(key);
|
|
@@ -8910,7 +9723,8 @@ async function applyQuestionAnswerInner(key, answerForCurrent) {
|
|
|
8910
9723
|
}
|
|
8911
9724
|
// More questions remain: persist the advanced state, then post the next one
|
|
8912
9725
|
// (which re-owns `messageId`). DO NOT reply to the agent yet.
|
|
8913
|
-
|
|
9726
|
+
// The next question is a new message of its own: it has no original yet, the one of the question answered is not its record.
|
|
9727
|
+
setPendingQuestion(key, { ...nextState, originalMessageId: null });
|
|
8914
9728
|
await postPendingQuestionAt(key);
|
|
8915
9729
|
return isAnswerShown;
|
|
8916
9730
|
}
|
|
@@ -9922,6 +10736,9 @@ function handleAgentQuestion(key, questionData) {
|
|
|
9922
10736
|
if (persistedQuestion && persistedQuestion.data.requestId === questionData.requestId) {
|
|
9923
10737
|
return;
|
|
9924
10738
|
}
|
|
10739
|
+
// The agent asks anew: a question the idle compaction re-asked earlier is superseded, and its buttons would now answer
|
|
10740
|
+
// the wrong question — it is closed as unanswered.
|
|
10741
|
+
closeReAskedQuestion(key);
|
|
9925
10742
|
// A pending status frame is now stale — the question UI replaces it. Stop the
|
|
9926
10743
|
// liveness loop first so it can't recreate a frame under the question prompt
|
|
9927
10744
|
// (Claude-only; a no-op for OpenCode threads, which never arm it).
|
|
@@ -10037,6 +10854,7 @@ async function postPendingQuestionAt(key) {
|
|
|
10037
10854
|
const existing = pendingQuestions.get(kStr);
|
|
10038
10855
|
if (existing && existing.data === pending.data && existing.currentIndex === qIdx) {
|
|
10039
10856
|
existing.messageId = messageId;
|
|
10857
|
+
existing.originalMessageId ??= messageId;
|
|
10040
10858
|
setPendingQuestion(key, existing);
|
|
10041
10859
|
markQuestionMessageSent(key, messageId);
|
|
10042
10860
|
// Pin the question so the muted topic fires a notification (S2). The
|
|
@@ -10048,11 +10866,12 @@ async function postPendingQuestionAt(key) {
|
|
|
10048
10866
|
void pinThreadQuestion(key, messageId);
|
|
10049
10867
|
}
|
|
10050
10868
|
else {
|
|
10051
|
-
// The question advanced / was answered while our send sat in the
|
|
10869
|
+
// The question advanced / was answered / was closed while our send sat in the
|
|
10052
10870
|
// queue (a fast digit reply can beat the post — seen live 2026-06-10:
|
|
10053
10871
|
// a stale unanswered-looking "❓" landed and lingered). The message we
|
|
10054
|
-
// just sent describes a question that no longer exists
|
|
10055
|
-
|
|
10872
|
+
// just sent describes a question that no longer exists: it is closed as
|
|
10873
|
+
// unanswered (its buttons would be dead), never deleted.
|
|
10874
|
+
await closeQuestionMessage(key, messageId, question, 'unanswered');
|
|
10056
10875
|
}
|
|
10057
10876
|
}
|
|
10058
10877
|
}
|
|
@@ -10090,7 +10909,7 @@ function handleAgentClosed(key) {
|
|
|
10090
10909
|
clearThinkingMessage(key);
|
|
10091
10910
|
// A closed session has no running delegation — remove the sub-agent status.
|
|
10092
10911
|
clearSubagentStatus(key);
|
|
10093
|
-
|
|
10912
|
+
retirePendingQuestionInBackground(key);
|
|
10094
10913
|
// A closed session has nothing to resume — drop any armed retry silently.
|
|
10095
10914
|
cancelApiRetry(key);
|
|
10096
10915
|
clearAuthNotice(key); // session closed → retire any pinned logged-out notice
|
|
@@ -10197,7 +11016,7 @@ function handleAgentError(key, error) {
|
|
|
10197
11016
|
deleteStatusMessage(key).catch(() => { });
|
|
10198
11017
|
clearThinkingMessage(key);
|
|
10199
11018
|
clearSubagentStatus(key);
|
|
10200
|
-
|
|
11019
|
+
retirePendingQuestionInBackground(key);
|
|
10201
11020
|
replyToThread(key, `Error: ${error.message}`).catch(() => { });
|
|
10202
11021
|
}
|
|
10203
11022
|
/**
|
|
@@ -10245,8 +11064,8 @@ function handleAgentStarted(key) {
|
|
|
10245
11064
|
* the delete); the "thinking …" frame and the sub-agent status for the same reason;
|
|
10246
11065
|
* and the pending question (F1): no stop path fires the adapter's `questionGone`
|
|
10247
11066
|
* (Claude's pane is killed; OpenCode emits no resolve), so a pinned question would
|
|
10248
|
-
* leak forever —
|
|
10249
|
-
* `handleAgentClosed` / `handleAgentError`.
|
|
11067
|
+
* leak forever — retiring it chains `unpinThreadQuestion` and closes its message as unanswered (the question stays
|
|
11068
|
+
* in the topic, its buttons would be dead), matching `handleAgentClosed` / `handleAgentError`.
|
|
10250
11069
|
*/
|
|
10251
11070
|
function clearGoneProcessFrames(key) {
|
|
10252
11071
|
clearThreadQueues(key);
|
|
@@ -10254,11 +11073,11 @@ function clearGoneProcessFrames(key) {
|
|
|
10254
11073
|
deleteStatusMessage(key).catch(() => { });
|
|
10255
11074
|
clearThinkingMessage(key);
|
|
10256
11075
|
clearSubagentStatus(key);
|
|
10257
|
-
|
|
11076
|
+
retirePendingQuestionInBackground(key);
|
|
10258
11077
|
}
|
|
10259
11078
|
function handleAgentStopped(key) {
|
|
10260
11079
|
// Single convergence point for every `stopSession`-driven stop path —
|
|
10261
|
-
// `/quit` (OpenCode/terminal), `/
|
|
11080
|
+
// `/quit` (OpenCode/terminal), `/quitall`, `/new`, `/unbind`, and adapter
|
|
10262
11081
|
// switch all emit `stopped`.
|
|
10263
11082
|
clearGoneProcessFrames(key);
|
|
10264
11083
|
// Session ended — a future session starts with empty context, so forget the
|
|
@@ -10321,9 +11140,8 @@ exports.COMMANDS_MENU = [
|
|
|
10321
11140
|
{ command: 'claude', description: '▶️ Start Claude Code' },
|
|
10322
11141
|
{ command: 'opencode', description: '▶️ Start OpenCode' },
|
|
10323
11142
|
{ command: 'terminal', description: '🖥 Open a raw shell in the bound folder' },
|
|
10324
|
-
{ command: 'claude_mode', description: '🔀 Claude backend: json-stream ⇄ per-turn ⇄ tmux
|
|
10325
|
-
{ command: 'new', description: '🆕 Restart session
|
|
10326
|
-
{ command: 'clear_session', description: '🆕 Restart session (alias /new)' },
|
|
11143
|
+
{ command: 'claude_mode', description: '🔀 Claude backend: json-stream ⇄ per-turn ⇄ tmux (deprecated)' },
|
|
11144
|
+
{ command: 'new', description: '🆕 Restart session' },
|
|
10327
11145
|
{ command: 'model', description: '🧠 Switch model' },
|
|
10328
11146
|
{ command: 'effort', description: '⚙️ Reasoning effort' },
|
|
10329
11147
|
{ command: 'verbosity', description: '🔊 Output verbosity (thinking+tools+sub-agents)' },
|
|
@@ -10338,6 +11156,9 @@ exports.COMMANDS_MENU = [
|
|
|
10338
11156
|
{ command: 'compact', description: '🧹 Compact agent context' },
|
|
10339
11157
|
{ command: 'compact_on_idle', description: '🧹 Auto-compact after idle (toggle)' },
|
|
10340
11158
|
{ command: 'compact_summary', description: '📄 Post the compaction summary (toggle)' },
|
|
11159
|
+
{ command: 'bot_skills', description: '🧰 The bot\'s own skills for agents (on/off)' },
|
|
11160
|
+
{ command: 'task_agents', description: '🧵 How many tracker task agents work at once' },
|
|
11161
|
+
{ command: 'runners', description: '🖥 Runners — machines for the agents\' tests and builds' },
|
|
10341
11162
|
{ command: 'accounts', description: '👤 Claude accounts and OpenCode providers (switch, add, re-login, remove, connect, disconnect)' },
|
|
10342
11163
|
{ command: 'limits', description: '🚧 Usage limits — switch this topic' },
|
|
10343
11164
|
{ command: 'limits_autoswitch', description: '🔁 Automatic switching on a usage limit' },
|
|
@@ -10345,6 +11166,7 @@ exports.COMMANDS_MENU = [
|
|
|
10345
11166
|
{ command: 'update', description: '🔄 Bot updates: automatic, notify only, off' },
|
|
10346
11167
|
{ command: 'schedule', description: '⏰ Schedule a prompt (agent does the work)' },
|
|
10347
11168
|
{ command: 'reminders', description: '🔔 Reminders — add, list, delete (no agent)' },
|
|
11169
|
+
{ command: 'todo', description: '📋 Agents\' published todo lists' },
|
|
10348
11170
|
{ command: 'status', description: '📊 Show status' },
|
|
10349
11171
|
{ command: 'output', description: '📜 Last 500 lines' },
|
|
10350
11172
|
{ command: 'trace', description: '🛰 Output-trace recorder on/off' },
|
|
@@ -10650,7 +11472,7 @@ async function reattachExistingSessions(opts = { quietReattach: false }) {
|
|
|
10650
11472
|
// A per-turn process that outlived the bot is stopped as soon as its turn (if any) ends (L5).
|
|
10651
11473
|
stopAdoptedPerTurnSessionWhenIdle(key, ownerAdapter);
|
|
10652
11474
|
}
|
|
10653
|
-
if ((0, adoptedToolList_1.checkIsAdoptedToolListStale)({ persistedDigest: agent.mcpToolDigest, currentDigest: botMcpToolDigestReader?.(key
|
|
11475
|
+
if ((0, adoptedToolList_1.checkIsAdoptedToolListStale)({ persistedDigest: agent.mcpToolDigest, currentDigest: botMcpToolDigestReader?.(key) ?? '' })) {
|
|
10654
11476
|
console.log(`[reattach] ${(0, sessionKey_1.keyToString)(key)}: adopted with a stale tool list; it gets the current tools when it resumes after its idle stop`);
|
|
10655
11477
|
}
|
|
10656
11478
|
// The surviving process keeps the account it was spawned with. A switch the user picked while it worked is
|
|
@@ -10797,7 +11619,8 @@ async function reattachExistingSessions(opts = { quietReattach: false }) {
|
|
|
10797
11619
|
* A persisted question is restored ONLY when its thread's session reattached
|
|
10798
11620
|
* and is currently active (`adapter.checkIsActive`). Otherwise the question is
|
|
10799
11621
|
* unreachable — the agent that asked it is gone — so it is dropped from the
|
|
10800
|
-
* store, preventing a stale entry from lingering across boots
|
|
11622
|
+
* store, preventing a stale entry from lingering across boots; its message is
|
|
11623
|
+
* kept, closed as unanswered, and its pin released. The persisted
|
|
10801
11624
|
* `messageId` is preserved, so the OLD buttons resolve correctly; the question
|
|
10802
11625
|
* is never re-posted.
|
|
10803
11626
|
*
|
|
@@ -10830,12 +11653,62 @@ function restorePendingQuestions() {
|
|
|
10830
11653
|
restored += 1;
|
|
10831
11654
|
}
|
|
10832
11655
|
else {
|
|
11656
|
+
// The agent that asked it is gone, so nobody can answer it: it is closed like any question that ends unanswered —
|
|
11657
|
+
// its message stays with the «not answered» note in place of the buttons and its pin is released. The pin map is
|
|
11658
|
+
// lost with the restart, so it is seeded from the saved message id for the clear to unpin that message.
|
|
11659
|
+
const orphaned = (0, openCodeQuestionFlow_1.migratePendingQuestionState)(value);
|
|
11660
|
+
if (orphaned.messageId !== null)
|
|
11661
|
+
questionPinnedMessageId.set(keyStr, orphaned.messageId);
|
|
10833
11662
|
clearPendingQuestion(key);
|
|
11663
|
+
closeCapturedQuestionMessage(key, orphaned.messageId, orphaned.data.questions[orphaned.currentIndex])
|
|
11664
|
+
.catch((e) => console.error('[pendingQuestion] closing the dropped question failed:', e));
|
|
10834
11665
|
dropped += 1;
|
|
10835
11666
|
}
|
|
10836
11667
|
}
|
|
10837
11668
|
console.log(`[reattach] pending questions: restored ${restored}, dropped ${dropped}`);
|
|
10838
11669
|
}
|
|
11670
|
+
/**
|
|
11671
|
+
* @description Bring back the questions the idle compaction re-asked, at boot. One whose conversation can still be
|
|
11672
|
+
* resumed (a live adopted process, or a sleeping conversation whose id is kept) gets its buttons working again — a tap
|
|
11673
|
+
* wakes it, as before the restart. One whose conversation is gone (released while the bot was down), or beside which a newer
|
|
11674
|
+
* question waits already, cannot be answered / must not be: its message is closed as unanswered, never left with buttons that
|
|
11675
|
+
* do nothing or that would answer the wrong question, and the saved record is dropped.
|
|
11676
|
+
* MUST run AFTER {@link reattachExistingSessions} so session activity is known. A platform this instance does not serve
|
|
11677
|
+
* is left alone, like {@link restorePendingQuestions}. A record saved BEFORE its re-ask was posted (`isPosted: false`: the bot
|
|
11678
|
+
* stopped during the idle compaction) names the message the question waited in: when the conversation can be resumed the
|
|
11679
|
+
* question is asked again now (nobody has had the chance to answer it) and that message says so; otherwise it is closed as
|
|
11680
|
+
* unanswered like any other.
|
|
11681
|
+
*/
|
|
11682
|
+
function restoreReAskedQuestions() {
|
|
11683
|
+
for (const [keyStr, saved] of Object.entries(state.getReAskedQuestions())) {
|
|
11684
|
+
let key;
|
|
11685
|
+
try {
|
|
11686
|
+
key = (0, sessionKey_1.keyFromString)(keyStr);
|
|
11687
|
+
}
|
|
11688
|
+
catch {
|
|
11689
|
+
continue;
|
|
11690
|
+
}
|
|
11691
|
+
if (!(0, connectorSet_1.checkIsServedConversation)(key, ENV.servedPlatforms))
|
|
11692
|
+
continue;
|
|
11693
|
+
// A question waits already (restored a moment ago, or asked anew by the agent that reattached): the copy's buttons would
|
|
11694
|
+
// deliver their label into IT, and a question asked again now would sit beside it. Nor is there anything to deliver a label
|
|
11695
|
+
// to once the conversation is gone.
|
|
11696
|
+
const isConversationResumable = !pendingQuestions.has(keyStr) && ((0, createAdapter_1.getThreadAdapter)(key).checkIsActive(key) || getResumableSessionId(key) !== null);
|
|
11697
|
+
if (isConversationResumable) {
|
|
11698
|
+
if (saved.isPosted === false) {
|
|
11699
|
+
// The bot stopped between the start of an idle compaction and its re-ask: the question was rejected on the agent's side
|
|
11700
|
+
// and nobody has answered it — it is asked again now, and the message it waited in (what is saved) says so.
|
|
11701
|
+
reAskInterruptedQuestion(key, saved).catch((e) => console.error('[reAskedQuestion] asking the interrupted question again failed:', e));
|
|
11702
|
+
}
|
|
11703
|
+
else {
|
|
11704
|
+
restoreReAskedQuestion(key, saved);
|
|
11705
|
+
}
|
|
11706
|
+
continue;
|
|
11707
|
+
}
|
|
11708
|
+
// The copy is closed, never brought back; an interrupted question's message ends as unanswered, the one thing known of it.
|
|
11709
|
+
closeSavedReAskedQuestion(key, saved);
|
|
11710
|
+
}
|
|
11711
|
+
}
|
|
10839
11712
|
/**
|
|
10840
11713
|
* @description Re-arm persisted API-error retries (S6) so a pending kick —
|
|
10841
11714
|
* especially a multi-hour usage-limit wait — survives a bot restart. MUST run
|
|
@@ -10905,23 +11778,20 @@ function markLimitEpisodeSettled(key) {
|
|
|
10905
11778
|
}
|
|
10906
11779
|
/**
|
|
10907
11780
|
* @description When the parked `errorText` of a json-stream conversation was written, from its Claude transcript's
|
|
10908
|
-
* API-error entry ({@link
|
|
11781
|
+
* API-error entry ({@link readTranscriptErrorWrittenAtMs}); `null` when the transcript tail cannot be read or holds no such
|
|
10909
11782
|
* entry — the recovery then goes by the stdout log's mtime.
|
|
10910
11783
|
*/
|
|
10911
11784
|
function readParkedErrorWrittenAtMs(key, claudeSessionId, errorText) {
|
|
10912
11785
|
const workDirDecision = getWorkDirStartDecision(key);
|
|
10913
11786
|
if (!workDirDecision.ok)
|
|
10914
11787
|
return null;
|
|
10915
|
-
|
|
10916
|
-
try {
|
|
10917
|
-
const sizeBytes = fs.statSync(transcriptPath).size;
|
|
10918
|
-
const tail = (0, jsonStreamHost_1.readFileByteRange)(transcriptPath, Math.max(0, sizeBytes - limitEpisodeRecovery_1.limitEpisodeTailMaxBytes), sizeBytes);
|
|
10919
|
-
return (0, limitEpisodeRecovery_1.getParkedErrorWrittenAtMs)(tail.toString('utf8'), errorText);
|
|
10920
|
-
}
|
|
10921
|
-
catch {
|
|
10922
|
-
return null; // no transcript on disk — the mtime decides
|
|
10923
|
-
}
|
|
11788
|
+
return (0, transcriptErrorTime_1.readTranscriptErrorWrittenAtMs)((0, claudeCliAdapter_1.getClaudeTranscriptPath)(workDirDecision.workDir, claudeSessionId), errorText);
|
|
10924
11789
|
}
|
|
11790
|
+
/**
|
|
11791
|
+
* @description The threads whose limit episode the boot recovery armed and whose adopt is still replaying the downtime tail
|
|
11792
|
+
* ({@link handleApiError} leaves the replay's copy of that error alone). An entry lives until the replay is over.
|
|
11793
|
+
*/
|
|
11794
|
+
const limitEpisodesOwnedByRecovery = new Set();
|
|
10925
11795
|
/**
|
|
10926
11796
|
* @description Recover a usage/session-limit episode that ENDED BEFORE this boot
|
|
10927
11797
|
* and left no armed retry behind — the case {@link restoreApiRetries} cannot help
|
|
@@ -11005,6 +11875,10 @@ function recoverLimitEpisodesFromDisk() {
|
|
|
11005
11875
|
void state
|
|
11006
11876
|
.setLimitEpisodeRecovered(key, log)
|
|
11007
11877
|
.catch(e => console.error('[limitRecovery] marker persist failed:', e));
|
|
11878
|
+
// This episode is the recovery's alone: the adopt replays the same error a moment later (the tail was read from the
|
|
11879
|
+
// saved offset), and `handleApiError` leaves that copy out until the replay is over.
|
|
11880
|
+
limitEpisodesOwnedByRecovery.add(keyStr);
|
|
11881
|
+
void ((0, createAdapter_1.getAdapter)(agent.name).whenAdoptReplayed?.(key) ?? Promise.resolve()).then(() => limitEpisodesOwnedByRecovery.delete(keyStr));
|
|
11008
11882
|
void withThreadLocale(key, () => handleApiError(key, decision.cls, { isRecoveredEpisode: true }));
|
|
11009
11883
|
recovered += 1;
|
|
11010
11884
|
}
|
|
@@ -11204,6 +12078,8 @@ let connectorConversationHooks = new Map();
|
|
|
11204
12078
|
* {@link clearThreadContextMarker}, which a stop clears too.
|
|
11205
12079
|
*/
|
|
11206
12080
|
function noteConversationContextReset(key, reason) {
|
|
12081
|
+
// A completed compaction or a fresh session: the agent forgot the board too (D20).
|
|
12082
|
+
noteTodoFreshContext(key);
|
|
11207
12083
|
connectorConversationHooks.get(key.platform)?.onContextReset(key, reason);
|
|
11208
12084
|
}
|
|
11209
12085
|
/**
|
|
@@ -11267,6 +12143,36 @@ async function remindAnswerTail(key, text) {
|
|
|
11267
12143
|
await forwardPromptToAgent(key, (0, createAdapter_1.getThreadAdapter)(key), text, undefined, { isRequestReminder: true });
|
|
11268
12144
|
return true;
|
|
11269
12145
|
}
|
|
12146
|
+
/**
|
|
12147
|
+
* @description How a conversation's session stands right now (`requests/sessionTurnProbe.ts`) — the one probe the
|
|
12148
|
+
* wake-up engine and the todo reminder both poll.
|
|
12149
|
+
*/
|
|
12150
|
+
const probeThreadTurn = (0, sessionTurnProbe_1.createSessionTurnProbe)({
|
|
12151
|
+
getAdapter: createAdapter_1.getThreadAdapter,
|
|
12152
|
+
checkHasPendingQuestion: (keyString) => pendingQuestions.has(keyString),
|
|
12153
|
+
checkHasQuestionPin: (keyString) => questionPinnedMessageId.has(keyString),
|
|
12154
|
+
checkIsCompacting: (keyString) => threadsCompacting.has(keyString),
|
|
12155
|
+
getApiRetryTimer: (keyString) => apiRetryTimers.get(keyString)?.timer,
|
|
12156
|
+
checkIsWedgeRecoveryInFlight: (keyString) => wedgeRecoveriesInFlight.has(keyString),
|
|
12157
|
+
checkIsRetryKickInFlight: (keyString) => apiRetryKicksInFlight.has(keyString),
|
|
12158
|
+
checkIsSessionStarting: (keyString) => startupPromptBuffer.checkIsStarting(keyString),
|
|
12159
|
+
serializeKey: sessionKey_1.keyToString,
|
|
12160
|
+
});
|
|
12161
|
+
/**
|
|
12162
|
+
* @description The todo list's standalone reminder (D11 form b): a technical prompt, posted only into a live session
|
|
12163
|
+
* that is idle right now — never a resume, never an interrupt (`postToIdleSession`). It opens no request and is never
|
|
12164
|
+
* replayed; in a Jira conversation it is no hand-over and nothing of its turn reaches the issue (D58).
|
|
12165
|
+
*/
|
|
12166
|
+
async function postTodoReminder(key, text) {
|
|
12167
|
+
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
12168
|
+
// A reminder dropped after an adopt replay (the session was no longer live and idle) was not posted.
|
|
12169
|
+
let isDropped = false;
|
|
12170
|
+
const isPosted = await (0, postToSession_1.postToIdleSession)({
|
|
12171
|
+
checkIsIdle: () => adapter.checkIsActive(key) && !(adapter.checkIsBusy?.(key) ?? false),
|
|
12172
|
+
forward: async () => { isDropped = (await forwardPromptToAgent(key, adapter, text, undefined, { isTechnicalPrompt: true })) === 'dropped'; },
|
|
12173
|
+
});
|
|
12174
|
+
return isPosted && !isDropped;
|
|
12175
|
+
}
|
|
11270
12176
|
/**
|
|
11271
12177
|
* @description The person took over the conversation (interrupt, quit, restart,
|
|
11272
12178
|
* switch or resume of the session, a switch to another agent or a shell, leaving
|
|
@@ -11302,8 +12208,8 @@ function createSessionPostDeps() {
|
|
|
11302
12208
|
// A scheduled run in a topic with requests on is a request (S7); a tracker's
|
|
11303
12209
|
// post arrives with its request already open and its header in the text.
|
|
11304
12210
|
const opening = await openTopicRequest(key, text, { source: 'scheduledRun', requesterId: topicRequest_1.scheduledRunRequester });
|
|
11305
|
-
await deliverPromptOrBuffer(key, `${opening.header}${text}`, startupPromptBuffer.checkIsStarting(conversationKey));
|
|
11306
|
-
|
|
12211
|
+
const delivery = await deliverPromptOrBuffer(key, `${opening.header}${text}`, startupPromptBuffer.checkIsStarting(conversationKey));
|
|
12212
|
+
await settleTopicRequestAfterDelivery(key, opening, delivery);
|
|
11307
12213
|
},
|
|
11308
12214
|
now: () => Date.now(),
|
|
11309
12215
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
@@ -11342,6 +12248,15 @@ function wireScheduler(wiring) {
|
|
|
11342
12248
|
await state.upsertSchedule({ ...record, isCheckFailing: isFailing, updatedAt: new Date().toISOString() });
|
|
11343
12249
|
await state.flush();
|
|
11344
12250
|
},
|
|
12251
|
+
recordAlertPin: async (jobId, messageId) => {
|
|
12252
|
+
const record = state.getSchedules()[jobId];
|
|
12253
|
+
if (!record)
|
|
12254
|
+
return;
|
|
12255
|
+
const alertPinMessageIds = [...(record.alertPinMessageIds ?? []), messageId];
|
|
12256
|
+
await state.upsertSchedule({ ...record, alertPinMessageIds, updatedAt: new Date().toISOString() });
|
|
12257
|
+
// Saved at once, like the failing flag: an id lost in a crash would leave its alert pinned for good.
|
|
12258
|
+
await state.flush();
|
|
12259
|
+
},
|
|
11345
12260
|
});
|
|
11346
12261
|
/**
|
|
11347
12262
|
* The engine's deliver callback: the S4 pipeline plus the S8 unbound-pause
|
|
@@ -11377,6 +12292,11 @@ function wireScheduler(wiring) {
|
|
|
11377
12292
|
const key = (0, sessionKey_1.keyFromString)(threadKeyStr);
|
|
11378
12293
|
return (0, createAdapter_1.getThreadAdapterNameRaw)(key) ?? state.getAgent(key)?.name;
|
|
11379
12294
|
},
|
|
12295
|
+
unpinMessages: async (threadKeyStr, messageIds) => {
|
|
12296
|
+
const key = (0, sessionKey_1.keyFromString)(threadKeyStr);
|
|
12297
|
+
for (const messageId of messageIds)
|
|
12298
|
+
await unpinMessageQuiet(key, messageId);
|
|
12299
|
+
},
|
|
11380
12300
|
// Routed through the outbound seam: the agent-facing file tool is a
|
|
11381
12301
|
// connector delivery, not a direct call into the Telegram send pipeline.
|
|
11382
12302
|
// The MCP surface is string-keyed (its `threadKey` arrives as text), so the
|
|
@@ -11391,13 +12311,26 @@ function wireScheduler(wiring) {
|
|
|
11391
12311
|
sendMessagesToThread,
|
|
11392
12312
|
compactConversation: (threadKeyStr) => armDeferredCompaction((0, sessionKey_1.keyFromString)(threadKeyStr)),
|
|
11393
12313
|
listClaudeProfiles: listClaudeProfilesForAgent,
|
|
12314
|
+
getRemoteRunRunnerLines: () => (0, runnerSummary_1.getRunnerSummaryLines)(),
|
|
12315
|
+
// A refused tool call leaves one line in the bounded diagnostic log (agent-diag.log): the SDK refuses before the bot sees it.
|
|
12316
|
+
logDiagnostic: diagLog_1.appendDiagLog,
|
|
11394
12317
|
...(jiraConnector
|
|
11395
12318
|
? { fetchJiraAttachment: (threadKeyStr, attachmentId) => jiraConnector.fetchAttachment((0, sessionKey_1.keyFromString)(threadKeyStr), attachmentId) }
|
|
11396
12319
|
: {}),
|
|
12320
|
+
// Read per request: General can turn the shared lists off, and the tools go with them.
|
|
12321
|
+
get todos() {
|
|
12322
|
+
return readTodoSettings().isEnabled
|
|
12323
|
+
? {
|
|
12324
|
+
update: (threadKeyStr, args) => getTodoTools().update((0, sessionKey_1.keyFromString)(threadKeyStr), args),
|
|
12325
|
+
read: (threadKeyStr, args) => getTodoTools().read((0, sessionKey_1.keyFromString)(threadKeyStr), args),
|
|
12326
|
+
}
|
|
12327
|
+
: undefined;
|
|
12328
|
+
},
|
|
11397
12329
|
answerRequest: (args) => (0, answerRequest_1.answerRequest)({
|
|
11398
12330
|
ledger: wiring.requestLedger,
|
|
11399
12331
|
answerSinks: wiring.answerSinks,
|
|
11400
12332
|
onAnswerDelivered: (key, requestId, kind) => requestWakeUpEngine?.noteAnswerDelivered(key, requestId, kind),
|
|
12333
|
+
getFinalAnswerNote: (key, requestCreatedAt) => (readTodoSettings().isEnabled ? getTodoRegistry().getFinalAnswerNote(key, requestCreatedAt) : null),
|
|
11401
12334
|
}, args),
|
|
11402
12335
|
whenSessionsRestored: wiring.whenSessionsRestored,
|
|
11403
12336
|
getSecret: () => state.getSchedulerMcpSecret(),
|
|
@@ -11616,10 +12549,11 @@ async function prepareJiraConnectorOrExit() {
|
|
|
11616
12549
|
* J5): the issue's binding, the request ledger, and the shared session post —
|
|
11617
12550
|
* after which the wake-up engine watches the turn the request started.
|
|
11618
12551
|
*/
|
|
11619
|
-
function createJiraSessionDeps(requestLedger, adapterName) {
|
|
12552
|
+
function createJiraSessionDeps(requestLedger, slots, adapterName) {
|
|
11620
12553
|
// D5: one conversation per issue — a session that sleeps between requests is resumed by the ensure (L-D4).
|
|
11621
12554
|
const sessionPostDeps = createSessionPostDeps();
|
|
11622
12555
|
return {
|
|
12556
|
+
slots,
|
|
11623
12557
|
bindConversation: async (key, folder) => {
|
|
11624
12558
|
if (state.getBinding(key)?.subdir !== folder)
|
|
11625
12559
|
await state.setBinding(key, folder);
|
|
@@ -11629,7 +12563,7 @@ function createJiraSessionDeps(requestLedger, adapterName) {
|
|
|
11629
12563
|
postRequest: async (key, requestId, prompt) => {
|
|
11630
12564
|
// A request is the issue's "user message" (L-D5): it clears the spent idle-compaction
|
|
11631
12565
|
// latch like a topic's message does, or the issue would never compact again.
|
|
11632
|
-
|
|
12566
|
+
notePersonActivity(key);
|
|
11633
12567
|
const posted = await (0, postToSession_1.postToSession)(sessionPostDeps, (0, sessionKey_1.keyToString)(key), prompt, adapterName);
|
|
11634
12568
|
if (!posted.ok) {
|
|
11635
12569
|
// R28: retried at about 1, 5 and 15 min, outside the wake-up cap.
|
|
@@ -11652,7 +12586,7 @@ async function startBot() {
|
|
|
11652
12586
|
console.log(`Default Claude backend: ${(0, createAdapter_1.getDefaultClaudeBackendName)()}`);
|
|
11653
12587
|
console.log(`Available agents: ${(0, createAdapter_1.getAvailableAdapters)().map(a => a.name).join(', ')}`);
|
|
11654
12588
|
// 1. State store. The `[startup] DATA_DIR=` shape is load-bearing: the
|
|
11655
|
-
//
|
|
12589
|
+
// docs/troubleshooting.md's two-instances section tells the operator to compare this
|
|
11656
12590
|
// exact line across instances.
|
|
11657
12591
|
state = await (0, state_1.getStateStore)({ saveDebounceMs: ENV.testTimings.stateSaveDebounceMs });
|
|
11658
12592
|
console.log(`[startup] DATA_DIR=${path.dirname(state.stateFilePath)}`);
|
|
@@ -11682,6 +12616,8 @@ async function startBot() {
|
|
|
11682
12616
|
// It starts polling once the sessions are restored.
|
|
11683
12617
|
// Every Claude spawn from here on reads the conversation's account profile (`/account`).
|
|
11684
12618
|
(0, createAdapter_1.registerClaudeConfigDirReader)(resolveThreadClaudeConfigDir);
|
|
12619
|
+
// The «Bot skills» switch (`/bot_skills`): every agent start, the OpenCode server's start and the bot MCP's instructions read it.
|
|
12620
|
+
(0, botSkills_1.setBotSkillsReader)(botSkillsReader);
|
|
11685
12621
|
(0, createAdapter_1.registerClaudeRateLimitSink)(onClaudeRateLimitEvent);
|
|
11686
12622
|
(0, createAdapter_1.registerClaudeTurnCostSink)(onClaudeTurnCost);
|
|
11687
12623
|
if (ENV.testTimings.openCodeServerUpBudgetMs !== undefined)
|
|
@@ -11690,6 +12626,7 @@ async function startBot() {
|
|
|
11690
12626
|
claudeUsageRefresher.start();
|
|
11691
12627
|
void claudeUsageRefresher.refreshAll();
|
|
11692
12628
|
const jiraConnector = ENV.servedPlatforms.has('jira') ? await prepareJiraConnectorOrExit() : null;
|
|
12629
|
+
todoJiraConversations = jiraConnector;
|
|
11693
12630
|
if (jiraConnector) {
|
|
11694
12631
|
const { launchDefaults } = jiraConnector;
|
|
11695
12632
|
(0, createAdapter_1.registerSessionLaunchDefaultsReader)((key) => (key.platform === 'jira' ? launchDefaults : null));
|
|
@@ -11709,24 +12646,26 @@ async function startBot() {
|
|
|
11709
12646
|
onRequestCreated: (key) => announceLimitWaitToNewRequest(key),
|
|
11710
12647
|
// C4: a connector counts what a prompt carried as sent only once its agent took it in.
|
|
11711
12648
|
onPromptSettled: (key, requestId, outcome) => connectorConversationHooks.get(key.platform)?.onPromptSettled(key, requestId, outcome),
|
|
12649
|
+
// D10: a closed hand-over may free a task-agent slot for a waiting issue.
|
|
12650
|
+
onRequestClosed: (record) => taskAgentSlots?.noteRequestClosed(record),
|
|
12651
|
+
// D10: a request the wake-up engine gave up on no longer holds a slot either.
|
|
12652
|
+
onRequestWakeStopped: (_key, request) => taskAgentSlots?.noteRequestWakeStopped(request),
|
|
11712
12653
|
});
|
|
11713
12654
|
await requestLedger.load();
|
|
12655
|
+
const slots = new taskAgentSlots_1.TaskAgentSlots({
|
|
12656
|
+
getMaxConcurrent: () => (0, taskAgentLimit_1.readTaskAgentLimit)(taskAgentLimitSources).maxConcurrent,
|
|
12657
|
+
ledger: requestLedger,
|
|
12658
|
+
store: state,
|
|
12659
|
+
});
|
|
12660
|
+
// A place held for a connector this process no longer serves could never start, and would hold up the others.
|
|
12661
|
+
slots.dropUnserved((key) => (0, connectorSet_1.checkIsServedConversation)(key, ENV.servedPlatforms));
|
|
12662
|
+
taskAgentSlots = slots;
|
|
11714
12663
|
topicRequestLedger = requestLedger;
|
|
11715
12664
|
// The wake-up engine (S4) watches the turns requests start. It is created now
|
|
11716
12665
|
// but started only once the sessions are restored (its sweep reads them).
|
|
11717
12666
|
requestWakeUpEngine = new wakeUpEngine_1.RequestWakeUpEngine({
|
|
11718
12667
|
ledger: requestLedger,
|
|
11719
|
-
probeTurn:
|
|
11720
|
-
getAdapter: createAdapter_1.getThreadAdapter,
|
|
11721
|
-
checkHasPendingQuestion: (keyString) => pendingQuestions.has(keyString),
|
|
11722
|
-
checkHasQuestionPin: (keyString) => questionPinnedMessageId.has(keyString),
|
|
11723
|
-
checkIsCompacting: (keyString) => threadsCompacting.has(keyString),
|
|
11724
|
-
getApiRetryTimer: (keyString) => apiRetryTimers.get(keyString)?.timer,
|
|
11725
|
-
checkIsWedgeRecoveryInFlight: (keyString) => wedgeRecoveriesInFlight.has(keyString),
|
|
11726
|
-
checkIsRetryKickInFlight: (keyString) => apiRetryKicksInFlight.has(keyString),
|
|
11727
|
-
checkIsSessionStarting: (keyString) => startupPromptBuffer.checkIsStarting(keyString),
|
|
11728
|
-
serializeKey: sessionKey_1.keyToString,
|
|
11729
|
-
}),
|
|
12668
|
+
probeTurn: probeThreadTurn,
|
|
11730
12669
|
prepareWakeUpSession: (key, message) => prepareRequestWakeUpSession(key, message, key.platform === 'jira' ? jiraConnector?.adapterName : undefined),
|
|
11731
12670
|
forwardWakeUp: forwardRequestWakeUp,
|
|
11732
12671
|
deliverAlert: async (key, request, reason) => {
|
|
@@ -11739,6 +12678,7 @@ async function startBot() {
|
|
|
11739
12678
|
return result.ok ? (result.alertRef ?? null) : null;
|
|
11740
12679
|
},
|
|
11741
12680
|
backstopMs: (0, wakeUpRules_1.getRequestBackstopMs)(process.env.REQUEST_BACKSTOP_MINUTES),
|
|
12681
|
+
pollIntervalMs: ENV.testTimings.requestTurnPollMs,
|
|
11742
12682
|
sweepIntervalMs: ENV.testTimings.requestSweepIntervalMs,
|
|
11743
12683
|
firstPostRetryDelayMs: ENV.testTimings.requestFirstPostRetryDelayMs,
|
|
11744
12684
|
// Only a Telegram topic in the answers-only view hides the stream from someone who expects to see the work.
|
|
@@ -11746,6 +12686,25 @@ async function startBot() {
|
|
|
11746
12686
|
remindAnswerTail,
|
|
11747
12687
|
});
|
|
11748
12688
|
requestLimitWaitAnswerDeps = { ledger: requestLedger, engine: requestWakeUpEngine, answerSinks };
|
|
12689
|
+
// The todo list's staleness reminder (D10–D12) polls the same turns; started once the sessions are restored.
|
|
12690
|
+
todoReminderEngine = new reminderEngine_1.TodoReminderEngine({
|
|
12691
|
+
registry: getTodoRegistry(),
|
|
12692
|
+
listConversations: () => state.listBindings().map(({ key }) => key),
|
|
12693
|
+
probeTurn: probeThreadTurn,
|
|
12694
|
+
getGate: (key) => ({
|
|
12695
|
+
hasOpenRequest: requestLedger.listOpenRequestsOf(key).length > 0,
|
|
12696
|
+
isStoppingAfterTurn: (0, createAdapter_1.getThreadAdapter)(key).name === adapterNames_1.claudePerTurnAdapterName,
|
|
12697
|
+
isAnswerTailFollowed: requestWakeUpEngine?.checkIsAnswerTailFollowed(key) ?? false,
|
|
12698
|
+
...getConversationAccountState(key),
|
|
12699
|
+
}),
|
|
12700
|
+
getIntervalMs: getTodoReminderIntervalMsNow,
|
|
12701
|
+
checkIsEnabled: () => readTodoSettings().isEnabled,
|
|
12702
|
+
postReminder: postTodoReminder,
|
|
12703
|
+
midTurn: getTodoMidTurn(),
|
|
12704
|
+
pollIntervalMs: ENV.testTimings.todoReminderPollMs,
|
|
12705
|
+
});
|
|
12706
|
+
getTodoMidTurn().clearInboxes();
|
|
12707
|
+
warmTodoProjects();
|
|
11749
12708
|
// Snapshot the persisted transient status-frame ids (S2) NOW, before reattach
|
|
11750
12709
|
// can run any frame-id setter. A reattached session's first frame lifecycle
|
|
11751
12710
|
// call persists the FRESH (all-null) in-memory state and would otherwise
|
|
@@ -11801,8 +12760,9 @@ async function startBot() {
|
|
|
11801
12760
|
// that gives a conversation of another platform only its request-side handling
|
|
11802
12761
|
// (Jira connector plan J2b, R2 — the routing table is unit-tested per event).
|
|
11803
12762
|
const noteTurnOutput = (key) => {
|
|
11804
|
-
// Output means a turn is under way — the wake-up engine's busy-onset signal.
|
|
12763
|
+
// Output means a turn is under way — the wake-up engine's busy-onset signal, and the todo reminder's.
|
|
11805
12764
|
requestWakeUpEngine?.noteAgentOutput(key);
|
|
12765
|
+
todoReminderEngine?.noteAgentOutput(key);
|
|
11806
12766
|
};
|
|
11807
12767
|
(0, createAdapter_1.registerAdapterEventHandlers)({
|
|
11808
12768
|
onOutput: (key, output, meta) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'output', () => {
|
|
@@ -11841,7 +12801,12 @@ async function startBot() {
|
|
|
11841
12801
|
}),
|
|
11842
12802
|
onStopped: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'stopped', () => withThreadLocale(key, () => handleAgentStopped(key)), () => clearForeignSessionState(key)),
|
|
11843
12803
|
onSuspended: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'suspended', () => withThreadLocale(key, () => handleAgentSuspended(key)), () => scheduleLostWorkNote(key, takeLostWorkOf(key), 'holdExpired')),
|
|
11844
|
-
onTurnEnded: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'turnEnded', () =>
|
|
12804
|
+
onTurnEnded: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'turnEnded', () => {
|
|
12805
|
+
// Every platform's (the route is `all`): the todo reminder's turn end, which the per-turn stop below would
|
|
12806
|
+
// otherwise hide from its poll.
|
|
12807
|
+
void todoReminderEngine?.noteAdapterTurnEnd(key).catch((e) => console.warn(`[todos] the turn end of ${(0, sessionKey_1.keyToString)(key)} was not noted:`, e instanceof Error ? e.message : e));
|
|
12808
|
+
handleAgentTurnEnded(key);
|
|
12809
|
+
}),
|
|
11845
12810
|
onContextCompacted: (key, trigger) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'contextCompacted', () => noteConversationContextReset(key, `compaction (${trigger ?? 'bot'})`)),
|
|
11846
12811
|
onConversationLost: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'conversationLost', () => withThreadLocale(key, () => handleConversationLost(key)), () => { dropLostConversationWatermark(key); noteConversationContextReset(key, 'its transcript was lost'); }),
|
|
11847
12812
|
onError: (key, error) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'error', () => withThreadLocale(key, () => handleAgentError(key, error)), () => {
|
|
@@ -11943,7 +12908,7 @@ async function startBot() {
|
|
|
11943
12908
|
});
|
|
11944
12909
|
// Known before any session starts or is adopted: every json-stream start persists the digest of the
|
|
11945
12910
|
// tools it connected to, and the reattach compares an adopted process's against it (L4).
|
|
11946
|
-
botMcpToolDigestReader = (
|
|
12911
|
+
botMcpToolDigestReader = (key) => schedulerMcpHandle.getToolDigest(key.platform, (0, botSkills_1.checkIsBotSkillsEnabled)(key));
|
|
11947
12912
|
const isSchedulerMcpStarted = await (0, mcpBoot_1.runSessionBootPhase)({
|
|
11948
12913
|
startBotMcp: () => (0, mcpBoot_1.startSchedulerMcpForBoot)({
|
|
11949
12914
|
handle: schedulerMcpHandle,
|
|
@@ -11964,6 +12929,9 @@ async function startBot() {
|
|
|
11964
12929
|
// existing option buttons keep working. A question is restored only for a
|
|
11965
12930
|
// thread whose session came back; otherwise it is unreachable, dropped.
|
|
11966
12931
|
restorePendingQuestions();
|
|
12932
|
+
// The questions the idle compaction re-asked: their buttons come back while the conversation can be resumed,
|
|
12933
|
+
// else the message is closed.
|
|
12934
|
+
restoreReAskedQuestions();
|
|
11967
12935
|
// Re-arm persisted API-error retries (S6) so a pending kick survives the
|
|
11968
12936
|
// restart. AFTER restorePendingQuestions so the kick lands in a live
|
|
11969
12937
|
// session; a past fireAt fires one delayed catch-up.
|
|
@@ -11986,7 +12954,8 @@ async function startBot() {
|
|
|
11986
12954
|
markSessionsRestored();
|
|
11987
12955
|
// From here the engine can tell a live turn from a dead session.
|
|
11988
12956
|
requestWakeUpEngine?.start();
|
|
11989
|
-
|
|
12957
|
+
todoReminderEngine?.start();
|
|
12958
|
+
jiraConnector?.start(createJiraSessionDeps(requestLedger, slots, jiraConnector.adapterName));
|
|
11990
12959
|
},
|
|
11991
12960
|
healActiveSessions: () => {
|
|
11992
12961
|
// When this boot ADOPTED an already-running opencode, that server may
|
|
@@ -11994,11 +12963,13 @@ async function startBot() {
|
|
|
11994
12963
|
// generation pointing at a now-dead port. Reconcile against the live MCP
|
|
11995
12964
|
// status and force-refresh any directory that is not `connected`.
|
|
11996
12965
|
// Fire-and-forget: boot must not block on opencode HTTP round-trips
|
|
11997
|
-
// before `bot.launch()`. The
|
|
11998
|
-
//
|
|
12966
|
+
// before `bot.launch()`. The skills step (a server started without the
|
|
12967
|
+
// bot's skills folder is replaced) and the compaction-plugin step go FIRST:
|
|
12968
|
+
// a replaced server or a recreated directory instance drops the MCP
|
|
11999
12969
|
// registration, which this same reconcile then restores.
|
|
12000
12970
|
const openCodeAdapter = (0, createAdapter_1.getAdapter)('opencode');
|
|
12001
12971
|
void (async () => {
|
|
12972
|
+
await openCodeAdapter.activateAgentSkillsForServer?.();
|
|
12002
12973
|
await openCodeAdapter.activateCompactionPluginForActiveSessions?.();
|
|
12003
12974
|
await openCodeAdapter.reconcileSchedulerMcpForActiveSessions?.();
|
|
12004
12975
|
})().catch((e) => console.warn('[scheduler] MCP reconcile failed:', e instanceof Error ? e.message : e));
|
|
@@ -12127,12 +13098,10 @@ async function startBot() {
|
|
|
12127
13098
|
// thread that no longer owns a json-stream session (the normal stop /
|
|
12128
13099
|
// release paths delete the dir themselves; reattach's adopt/orphan-kill
|
|
12129
13100
|
// covers live tmux sessions — this covers their dirs).
|
|
12130
|
-
void (0, jsonStreamHost_1.sweepOrphanJsonStreamDirs)((0, jsonStreamHost_1.resolveJsonStreamRoot)(getDataDir()), (key) => {
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
return agent?.name === adapterNames_1.claudeJsonStreamAdapterName && !!agent.claudeSessionId;
|
|
12135
|
-
})
|
|
13101
|
+
void (0, jsonStreamHost_1.sweepOrphanJsonStreamDirs)((0, jsonStreamHost_1.resolveJsonStreamRoot)(getDataDir()), (key) => (0, jsonStreamHost_1.checkIsJsonStreamHostOwned)({
|
|
13102
|
+
isSessionActive: [adapterNames_1.claudeJsonStreamAdapterName, adapterNames_1.claudePerTurnAdapterName].some((name) => (0, createAdapter_1.getAdapter)(name).checkIsActive(key)),
|
|
13103
|
+
agent: state.getAgent(key),
|
|
13104
|
+
}))
|
|
12136
13105
|
.then((removedCount) => {
|
|
12137
13106
|
if (removedCount > 0)
|
|
12138
13107
|
console.log(`[fileSweep] removed ${removedCount} orphan jsonstream dirs`);
|
|
@@ -12175,7 +13144,7 @@ async function startBot() {
|
|
|
12175
13144
|
console.error('[bot.catch] unhandled error:', err, 'update:', ctx.updateType);
|
|
12176
13145
|
});
|
|
12177
13146
|
// 7. Shutdown — preserve active agents for restart/reattach. Use /quit
|
|
12178
|
-
// (or /
|
|
13147
|
+
// (or /quitall) for an intentional agent stop; process signals only
|
|
12179
13148
|
// stop the bot itself. Ordering (bot.stop → state.flush →
|
|
12180
13149
|
// releaseLock → exit) is enforced by `gracefulShutdown` so the
|
|
12181
13150
|
// previous race against `lock.ts`'s synchronous `process.exit(0)`
|
|
@@ -12206,6 +13175,7 @@ async function startBot() {
|
|
|
12206
13175
|
// means the next boot's rearmAll picks them back up (catch-up replay).
|
|
12207
13176
|
schedulerEngine?.shutdown();
|
|
12208
13177
|
requestWakeUpEngine?.stop();
|
|
13178
|
+
todoReminderEngine?.stop();
|
|
12209
13179
|
jiraConnector?.stop();
|
|
12210
13180
|
if (isSchedulerMcpStarted)
|
|
12211
13181
|
void schedulerMcpHandle.stop().catch(() => { });
|
|
@@ -12291,6 +13261,9 @@ async function startBot() {
|
|
|
12291
13261
|
console.warn('[platform] native Windows notice failed:', e instanceof Error ? e.message : e);
|
|
12292
13262
|
}
|
|
12293
13263
|
await startUpdates();
|
|
13264
|
+
// Pairing messages of `/runners` turn into «paired» / «expired» (and survive this restart through the registry's notes).
|
|
13265
|
+
if (ENV.servedPlatforms.has('telegram'))
|
|
13266
|
+
startRunnersWatcher();
|
|
12294
13267
|
// The local speech engine: clean up after an earlier run, re-attach to / start its install. Starts no process.
|
|
12295
13268
|
void bootSttLocalEngine().catch((e) => console.warn('[STT] boot step failed:', e instanceof Error ? e.message : e));
|
|
12296
13269
|
console.log(`Launching Telegraf bot (long polling, dropPendingUpdates=${bootMode.dropPendingUpdates})...`);
|