@nordbyte/nordrelay 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/.env.example +9 -0
  2. package/README.md +81 -1197
  3. package/dist/{access-control.js → access/access-control.js} +1 -1
  4. package/dist/{audit-log.js → access/audit-log.js} +2 -2
  5. package/dist/{session-locks.js → access/session-locks.js} +1 -1
  6. package/dist/{user-management.js → access/user-management.js} +1 -1
  7. package/dist/{claude-code-cli.js → agents/claude-code/claude-code-cli.js} +2 -2
  8. package/dist/{claude-code-session.js → agents/claude-code/claude-code-session.js} +1 -1
  9. package/dist/{codex-cli.js → agents/codex/codex-cli.js} +14 -5
  10. package/dist/{codex-session.js → agents/codex/codex-session.js} +2 -4
  11. package/dist/{hermes-cli.js → agents/hermes/hermes-cli.js} +2 -2
  12. package/dist/{hermes-launch.js → agents/hermes/hermes-launch.js} +1 -1
  13. package/dist/{hermes-session.js → agents/hermes/hermes-session.js} +1 -1
  14. package/dist/{openclaw-cli.js → agents/openclaw/openclaw-cli.js} +2 -2
  15. package/dist/{openclaw-launch.js → agents/openclaw/openclaw-launch.js} +1 -1
  16. package/dist/{openclaw-session.js → agents/openclaw/openclaw-session.js} +1 -1
  17. package/dist/{pi-cli.js → agents/pi/pi-cli.js} +2 -2
  18. package/dist/{pi-launch.js → agents/pi/pi-launch.js} +1 -1
  19. package/dist/{pi-session.js → agents/pi/pi-session.js} +1 -1
  20. package/dist/{adapter-conformance.js → agents/shared/adapter-conformance.js} +2 -2
  21. package/dist/{agent-activity.js → agents/shared/agent-activity.js} +5 -5
  22. package/dist/agents/shared/agent-auth-commands.js +30 -0
  23. package/dist/{agent-factory.js → agents/shared/agent-factory.js} +5 -5
  24. package/dist/{agent-feature-matrix.js → agents/shared/agent-feature-matrix.js} +2 -2
  25. package/dist/{agent-updates.js → agents/shared/agent-updates.js} +7 -7
  26. package/dist/{discord-artifacts.js → channels/discord/discord-artifacts.js} +4 -4
  27. package/dist/{discord-bot.js → channels/discord/discord-bot.js} +164 -424
  28. package/dist/{discord-channel-runtime.js → channels/discord/discord-channel-runtime.js} +2 -2
  29. package/dist/{discord-command-surface.js → channels/discord/discord-command-surface.js} +3 -3
  30. package/dist/{bot-rendering.js → channels/shared/bot-rendering.js} +6 -6
  31. package/dist/{channel-actions.js → channels/shared/channel-actions.js} +4 -4
  32. package/dist/channels/shared/channel-bridge-controller.js +69 -0
  33. package/dist/channels/shared/channel-cli-artifacts.js +51 -0
  34. package/dist/{channel-command-service.js → channels/shared/channel-command-service.js} +51 -28
  35. package/dist/channels/shared/channel-external-mirror-controller.js +193 -0
  36. package/dist/channels/shared/channel-external-monitor.js +52 -0
  37. package/dist/{channel-mirror-registry.js → channels/shared/channel-mirror-registry.js} +14 -6
  38. package/dist/{channel-peer-prompt.js → channels/shared/channel-peer-prompt.js} +3 -3
  39. package/dist/{channel-turn-service.js → channels/shared/channel-turn-service.js} +2 -2
  40. package/dist/{context-key.js → channels/shared/context-key.js} +1 -1
  41. package/dist/{session-format.js → channels/shared/session-format.js} +2 -2
  42. package/dist/{slack-artifacts.js → channels/slack/slack-artifacts.js} +4 -4
  43. package/dist/{slack-bot.js → channels/slack/slack-bot.js} +159 -294
  44. package/dist/{slack-channel-runtime.js → channels/slack/slack-channel-runtime.js} +2 -2
  45. package/dist/{slack-command-surface.js → channels/slack/slack-command-surface.js} +2 -2
  46. package/dist/{slack-diagnostics.js → channels/slack/slack-diagnostics.js} +2 -2
  47. package/dist/{bot-ui.js → channels/telegram/bot-ui.js} +1 -1
  48. package/dist/{bot.js → channels/telegram/bot.js} +178 -427
  49. package/dist/{telegram-access-commands.js → channels/telegram/telegram-access-commands.js} +3 -3
  50. package/dist/{telegram-access-middleware.js → channels/telegram/telegram-access-middleware.js} +4 -4
  51. package/dist/{telegram-agent-commands.js → channels/telegram/telegram-agent-commands.js} +9 -9
  52. package/dist/{telegram-artifact-commands.js → channels/telegram/telegram-artifact-commands.js} +4 -4
  53. package/dist/{telegram-channel-runtime.js → channels/telegram/telegram-channel-runtime.js} +2 -2
  54. package/dist/{telegram-command-menu.js → channels/telegram/telegram-command-menu.js} +1 -1
  55. package/dist/{telegram-diagnostics-command.js → channels/telegram/telegram-diagnostics-command.js} +7 -7
  56. package/dist/{telegram-general-commands.js → channels/telegram/telegram-general-commands.js} +4 -4
  57. package/dist/{telegram-operational-commands.js → channels/telegram/telegram-operational-commands.js} +5 -5
  58. package/dist/{telegram-output.js → channels/telegram/telegram-output.js} +2 -2
  59. package/dist/{telegram-preference-commands.js → channels/telegram/telegram-preference-commands.js} +3 -3
  60. package/dist/{telegram-queue-commands.js → channels/telegram/telegram-queue-commands.js} +6 -6
  61. package/dist/{telegram-support-command.js → channels/telegram/telegram-support-command.js} +4 -4
  62. package/dist/{telegram-update-commands.js → channels/telegram/telegram-update-commands.js} +5 -5
  63. package/dist/{config-metadata.js → core/config-metadata.js} +8 -0
  64. package/dist/{config.js → core/config.js} +11 -3
  65. package/dist/index.js +27 -23
  66. package/dist/{peer-client.js → peers/peer-client.js} +57 -1
  67. package/dist/peers/peer-discovery-jobs.js +206 -0
  68. package/dist/peers/peer-discovery.js +223 -0
  69. package/dist/peers/peer-health-monitor.js +49 -0
  70. package/dist/{peer-identity.js → peers/peer-identity.js} +50 -1
  71. package/dist/{peer-runtime-service.js → peers/peer-runtime-service.js} +29 -7
  72. package/dist/{peer-server.js → peers/peer-server.js} +23 -6
  73. package/dist/{peer-store.js → peers/peer-store.js} +84 -11
  74. package/dist/{peer-types.js → peers/peer-types.js} +9 -0
  75. package/dist/peers/peer-web-proxy-contract.js +127 -0
  76. package/dist/{metrics.js → runtime/metrics.js} +5 -3
  77. package/dist/{relay-artifact-service.js → runtime/relay-artifact-service.js} +1 -1
  78. package/dist/runtime/relay-auth-service.js +63 -0
  79. package/dist/runtime/relay-dashboard-service.js +139 -0
  80. package/dist/{relay-external-activity-monitor.js → runtime/relay-external-activity-monitor.js} +140 -53
  81. package/dist/runtime/relay-runtime-active-sessions.js +387 -0
  82. package/dist/runtime/relay-runtime-dashboard.js +201 -0
  83. package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +307 -0
  84. package/dist/runtime/relay-runtime-sessions.js +623 -0
  85. package/dist/runtime/relay-runtime-types.js +1 -0
  86. package/dist/runtime/relay-runtime-updates-jobs.js +360 -0
  87. package/dist/runtime/relay-runtime.js +451 -0
  88. package/dist/runtime/runtime-cache.js +117 -0
  89. package/dist/{session-registry.js → state/session-registry.js} +3 -3
  90. package/dist/{operations.js → support/operations.js} +7 -7
  91. package/dist/{support-bundle.js → support/support-bundle.js} +1 -1
  92. package/dist/{web-api-contract.js → web/web-api-contract.js} +17 -3
  93. package/dist/web/web-api-types.js +1 -0
  94. package/dist/{web-dashboard-access-routes.js → web/web-dashboard-access-routes.js} +2 -2
  95. package/dist/{web-dashboard-assets.js → web/web-dashboard-assets.js} +24 -2
  96. package/dist/{web-dashboard-http.js → web/web-dashboard-http.js} +41 -5
  97. package/dist/{web-dashboard-pages.js → web/web-dashboard-pages.js} +37 -10
  98. package/dist/{web-dashboard-peer-routes.js → web/web-dashboard-peer-routes.js} +102 -7
  99. package/dist/web/web-dashboard-security.js +14 -0
  100. package/dist/{web-dashboard-session-routes.js → web/web-dashboard-session-routes.js} +12 -1
  101. package/dist/{web-dashboard.js → web/web-dashboard.js} +132 -48
  102. package/dist/web/web-performance.js +60 -0
  103. package/dist/web/web-rate-limit.js +19 -0
  104. package/dist/{web-state.js → web/web-state.js} +74 -5
  105. package/dist/webui-assets/dashboard.css +171 -10
  106. package/dist/webui-assets/dashboard.js +515 -48
  107. package/dist/webui-assets/favicon.ico +0 -0
  108. package/dist/webui-assets/favicon.png +0 -0
  109. package/dist/webui-assets/logo.png +0 -0
  110. package/package.json +4 -3
  111. package/plugins/nordrelay/scripts/nordrelay.mjs +17 -5
  112. package/{launchd/start.sh → scripts/launchd-start.sh} +1 -1
  113. package/dist/relay-runtime.js +0 -1916
  114. package/dist/runtime-cache.js +0 -57
  115. /package/dist/{user-management-crypto.js → access/user-management-crypto.js} +0 -0
  116. /package/dist/{user-management-normalize.js → access/user-management-normalize.js} +0 -0
  117. /package/dist/{user-management-types.js → access/user-management-types.js} +0 -0
  118. /package/dist/{claude-code-auth.js → agents/claude-code/claude-code-auth.js} +0 -0
  119. /package/dist/{claude-code-launch.js → agents/claude-code/claude-code-launch.js} +0 -0
  120. /package/dist/{claude-code-state.js → agents/claude-code/claude-code-state.js} +0 -0
  121. /package/dist/{codex-auth.js → agents/codex/codex-auth.js} +0 -0
  122. /package/dist/{codex-config.js → agents/codex/codex-config.js} +0 -0
  123. /package/dist/{codex-launch.js → agents/codex/codex-launch.js} +0 -0
  124. /package/dist/{codex-state.js → agents/codex/codex-state.js} +0 -0
  125. /package/dist/{hermes-api.js → agents/hermes/hermes-api.js} +0 -0
  126. /package/dist/{hermes-auth.js → agents/hermes/hermes-auth.js} +0 -0
  127. /package/dist/{hermes-state.js → agents/hermes/hermes-state.js} +0 -0
  128. /package/dist/{openclaw-auth.js → agents/openclaw/openclaw-auth.js} +0 -0
  129. /package/dist/{openclaw-gateway.js → agents/openclaw/openclaw-gateway.js} +0 -0
  130. /package/dist/{openclaw-state.js → agents/openclaw/openclaw-state.js} +0 -0
  131. /package/dist/{pi-auth.js → agents/pi/pi-auth.js} +0 -0
  132. /package/dist/{pi-rpc.js → agents/pi/pi-rpc.js} +0 -0
  133. /package/dist/{pi-state.js → agents/pi/pi-state.js} +0 -0
  134. /package/dist/{agent-adapter.js → agents/shared/agent-adapter.js} +0 -0
  135. /package/dist/{agent.js → agents/shared/agent.js} +0 -0
  136. /package/dist/{artifacts.js → artifacts/artifacts.js} +0 -0
  137. /package/dist/{attachments.js → artifacts/attachments.js} +0 -0
  138. /package/dist/{voice.js → artifacts/voice.js} +0 -0
  139. /package/dist/{discord-rate-limit.js → channels/discord/discord-rate-limit.js} +0 -0
  140. /package/dist/{channel-adapter.js → channels/shared/channel-adapter.js} +0 -0
  141. /package/dist/{relay-runtime-types.js → channels/shared/channel-bridge-state.js} +0 -0
  142. /package/dist/{channel-command-catalog.js → channels/shared/channel-command-catalog.js} +0 -0
  143. /package/dist/{channel-command-core.js → channels/shared/channel-command-core.js} +0 -0
  144. /package/dist/{channel-prompt-engine.js → channels/shared/channel-prompt-engine.js} +0 -0
  145. /package/dist/{channel-runtime.js → channels/shared/channel-runtime.js} +0 -0
  146. /package/dist/{channel-turn-lifecycle.js → channels/shared/channel-turn-lifecycle.js} +0 -0
  147. /package/dist/{slack-rate-limit.js → channels/slack/slack-rate-limit.js} +0 -0
  148. /package/dist/{telegram-command-types.js → channels/telegram/telegram-command-types.js} +0 -0
  149. /package/dist/{telegram-rate-limit.js → channels/telegram/telegram-rate-limit.js} +0 -0
  150. /package/dist/{activity-events.js → core/activity-events.js} +0 -0
  151. /package/dist/{error-messages.js → core/error-messages.js} +0 -0
  152. /package/dist/{format.js → core/format.js} +0 -0
  153. /package/dist/{logger.js → core/logger.js} +0 -0
  154. /package/dist/{redaction.js → core/redaction.js} +0 -0
  155. /package/dist/{settings-service.js → core/settings-service.js} +0 -0
  156. /package/dist/{settings-wizard-test.js → core/settings-wizard-test.js} +0 -0
  157. /package/dist/{workspace-policy.js → core/workspace-policy.js} +0 -0
  158. /package/dist/{peer-auth.js → peers/peer-auth.js} +0 -0
  159. /package/dist/{peer-context.js → peers/peer-context.js} +0 -0
  160. /package/dist/{peer-readiness.js → peers/peer-readiness.js} +0 -0
  161. /package/dist/{relay-queue-service.js → runtime/relay-queue-service.js} +0 -0
  162. /package/dist/{web-api-types.js → runtime/relay-runtime-delegate.js} +0 -0
  163. /package/dist/{relay-runtime-helpers.js → runtime/relay-runtime-helpers.js} +0 -0
  164. /package/dist/{remote-prompt.js → runtime/remote-prompt.js} +0 -0
  165. /package/dist/{bot-preferences.js → state/bot-preferences.js} +0 -0
  166. /package/dist/{job-store.js → state/job-store.js} +0 -0
  167. /package/dist/{persistence.js → state/persistence.js} +0 -0
  168. /package/dist/{prompt-store.js → state/prompt-store.js} +0 -0
  169. /package/dist/{state-backend.js → state/state-backend.js} +0 -0
  170. /package/dist/{zip-writer.js → support/zip-writer.js} +0 -0
  171. /package/dist/{web-dashboard-artifact-routes.js → web/web-dashboard-artifact-routes.js} +0 -0
  172. /package/dist/{web-dashboard-runtime-routes.js → web/web-dashboard-runtime-routes.js} +0 -0
  173. /package/dist/{web-dashboard-ui.js → web/web-dashboard-ui.js} +0 -0
@@ -3,36 +3,38 @@ import { readFile, unlink } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { autoRetry } from "@grammyjs/auto-retry";
5
5
  import { Bot, InlineKeyboard, InputFile } from "grammy";
6
- import { ADMIN_GROUP_ID } from "./access-control.js";
7
- import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
8
- import { collectArtifactReport, collectRecentWorkspaceArtifacts, createArtifactZipBundle, ensureOutDir, formatArtifactSummary, isTelegramImagePreview, persistWorkspaceArtifactReport, pruneConnectorTurnDirs, telegramArtifactFilename, totalArtifactSize, } from "./artifacts.js";
9
- import { AgentUpdateManager } from "./agent-updates.js";
10
- import { AuditLogStore } from "./audit-log.js";
6
+ import { ADMIN_GROUP_ID } from "../../access/access-control.js";
7
+ import { buildFileInstructions, outboxPath, stageFile, } from "../../artifacts/attachments.js";
8
+ import { collectArtifactReport, createArtifactZipBundle, ensureOutDir, formatArtifactSummary, isTelegramImagePreview, pruneConnectorTurnDirs, telegramArtifactFilename, totalArtifactSize, } from "../../artifacts/artifacts.js";
9
+ import { AgentUpdateManager } from "../../agents/shared/agent-updates.js";
10
+ import { AuditLogStore } from "../../access/audit-log.js";
11
11
  import { formatSessionLabel } from "./bot-ui.js";
12
- import { BotPreferencesStore, isQuietNow, } from "./bot-preferences.js";
13
- import { renderAgentUpdateJobAction } from "./channel-actions.js";
14
- import { ChannelCommandService } from "./channel-command-service.js";
15
- import { runChannelPeerPrompt } from "./channel-peer-prompt.js";
16
- import { deliverChannelAction } from "./channel-runtime.js";
17
- import { createChannelTurnLifecycle, createChannelTypingLoop } from "./channel-turn-lifecycle.js";
18
- import { agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
19
- import { getExternalActivityForSession, getExternalSnapshotForSession, } from "./agent-activity.js";
20
- import { checkAuthStatus, clearAuthCache, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
21
- import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
22
- import { formatLaunchProfileBehavior } from "./codex-launch.js";
23
- import { contextKeyFromCtx, isTelegramContextKey, isTopicContextKey, parseContextKey } from "./context-key.js";
24
- import { friendlyErrorText } from "./error-messages.js";
25
- import { escapeHTML } from "./format.js";
26
- import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
27
- import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
28
- import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
29
- import { RemoteRelayClient } from "./peer-client.js";
30
- import { checkPiAuthStatus } from "./pi-auth.js";
31
- import { configureRedaction, redactText } from "./redaction.js";
32
- import { canWriteWithLock, SessionLockStore } from "./session-locks.js";
33
- import { renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
34
- import { SessionRegistry } from "./session-registry.js";
35
- import { transcribeAudio } from "./voice.js";
12
+ import { BotPreferencesStore, isQuietNow, } from "../../state/bot-preferences.js";
13
+ import { renderAgentUpdateJobAction } from "../shared/channel-actions.js";
14
+ import { createChannelBusyStore } from "../shared/channel-bridge-controller.js";
15
+ import { ChannelCommandService } from "../shared/channel-command-service.js";
16
+ import { runChannelPeerPrompt } from "../shared/channel-peer-prompt.js";
17
+ import { deliverChannelAction } from "../shared/channel-runtime.js";
18
+ import { deliverChannelCliArtifacts } from "../shared/channel-cli-artifacts.js";
19
+ import { createChannelExternalMirrorController } from "../shared/channel-external-mirror-controller.js";
20
+ import { monitorChannelExternalContexts } from "../shared/channel-external-monitor.js";
21
+ import { createChannelTurnLifecycle, createChannelTypingLoop } from "../shared/channel-turn-lifecycle.js";
22
+ import { agentLabel, agentReasoningLabel, agentReasoningOptions, } from "../../agents/shared/agent.js";
23
+ import { agentIdForAuth as resolveAgentIdForAuth, agentLabelForAuth, hostAgentLoginCommand, hostAgentLogoutCommand, } from "../../agents/shared/agent-auth-commands.js";
24
+ import { getExternalActivityForSession, } from "../../agents/shared/agent-activity.js";
25
+ import { checkAuthStatus, clearAuthCache, startLogin as startCodexLogin, startLogout as startCodexLogout } from "../../agents/codex/codex-auth.js";
26
+ import { formatLaunchProfileBehavior } from "../../agents/codex/codex-launch.js";
27
+ import { contextKeyFromCtx, isTelegramContextKey, isTopicContextKey, parseContextKey } from "../shared/context-key.js";
28
+ import { friendlyErrorText } from "../../core/error-messages.js";
29
+ import { escapeHTML } from "../../core/format.js";
30
+ import { PromptStore, toPromptEnvelope } from "../../state/prompt-store.js";
31
+ import { RemoteRelayClient } from "../../peers/peer-client.js";
32
+ import { RelayAuthService } from "../../runtime/relay-auth-service.js";
33
+ import { configureRedaction, redactText } from "../../core/redaction.js";
34
+ import { canWriteWithLock, SessionLockStore } from "../../access/session-locks.js";
35
+ import { renderSessionInfoHTML, renderSessionInfoPlain, } from "../shared/session-format.js";
36
+ import { SessionRegistry } from "../../state/session-registry.js";
37
+ import { transcribeAudio } from "../../artifacts/voice.js";
36
38
  import { telegramRateLimiter } from "./telegram-rate-limit.js";
37
39
  import { chatBucket, downloadTelegramFile, isMessageNotModifiedError, renderMarkdownChunkWithinLimit, safeEditMessage, safeEditReplyMarkup, safeReply, sendChatActionSafe, sendTextMessage, splitMarkdownForTelegram, } from "./telegram-output.js";
38
40
  import { NOOP_PAGE_CALLBACK_DATA, TelegramBotChannelRuntime, paginateKeyboard, telegramChannelContextFromCtx, } from "./telegram-channel-runtime.js";
@@ -47,11 +49,11 @@ import { registerTelegramPreferenceCommands } from "./telegram-preference-comman
47
49
  import { createQueuedPromptCancelKeyboard, registerTelegramQueueCommands, } from "./telegram-queue-commands.js";
48
50
  import { registerTelegramSupportCommands } from "./telegram-support-command.js";
49
51
  import { registerTelegramUpdateCommands } from "./telegram-update-commands.js";
50
- import { appendWithCap, authHelpText, buildStreamingPreview, capabilitiesOf, filterSessions, formatAgentLaunchProfileLabel, formatAgentSettingScope, formatDurationSeconds, formatError, formatLocalDateTime, formatLockOwner, formatModelButtonLabel, formatRelativeTime, formatTelegramName, formatToolSummaryLine, formatTurnUsageLine, getWorkspaceShortName, idOf, isEmptyArtifactReport, isPromptEnvelopeLike, isQueuedPromptLike, labelOf, orderPinnedSessions, parseFastModeArgument, renderExternalMirrorEvent, renderExternalMirrorStatus, renderPromptFailure, renderTodoList, renderToolEndMessage, renderToolStartMessage, requiresTurnApproval, trimLine, } from "./bot-rendering.js";
51
- import { UserStore } from "./user-management.js";
52
- import { WebActivityStore } from "./web-state.js";
53
- import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "./workspace-policy.js";
54
- export { formatToolSummaryLine, formatTurnUsageLine, summarizeToolName } from "./bot-rendering.js";
52
+ import { appendWithCap, authHelpText, buildStreamingPreview, capabilitiesOf, filterSessions, formatAgentLaunchProfileLabel, formatAgentSettingScope, formatDurationSeconds, formatError, formatLocalDateTime, formatLockOwner, formatModelButtonLabel, formatRelativeTime, formatTelegramName, formatToolSummaryLine, formatTurnUsageLine, getWorkspaceShortName, idOf, isEmptyArtifactReport, isPromptEnvelopeLike, isQueuedPromptLike, labelOf, orderPinnedSessions, parseFastModeArgument, renderPromptFailure, renderTodoList, renderToolEndMessage, renderToolStartMessage, requiresTurnApproval, trimLine, } from "../shared/bot-rendering.js";
53
+ import { UserStore } from "../../access/user-management.js";
54
+ import { WebActivityStore } from "../../web/web-state.js";
55
+ import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "../../core/workspace-policy.js";
56
+ export { formatToolSummaryLine, formatTurnUsageLine, summarizeToolName } from "../shared/bot-rendering.js";
55
57
  export { registerCommands } from "./telegram-command-menu.js";
56
58
  const EDIT_DEBOUNCE_MS = 1500;
57
59
  const TYPING_INTERVAL_MS = 4500;
@@ -73,7 +75,12 @@ export function createBot(config, registry) {
73
75
  const bot = new Bot(config.telegramBotToken);
74
76
  bot.api.config.use(autoRetry({ maxRetryAttempts: 3, maxDelaySeconds: 10 }));
75
77
  const telegramChannelRuntime = new TelegramBotChannelRuntime(bot);
76
- const contextBusy = new Map();
78
+ const contextBusy = createChannelBusyStore(() => ({
79
+ processing: false,
80
+ switching: false,
81
+ transcribing: false,
82
+ approving: false,
83
+ }));
77
84
  const pendingApprovals = new Map();
78
85
  const pendingSessionPicks = new Map();
79
86
  const pendingWorkspacePicks = new Map();
@@ -93,6 +100,7 @@ export function createBot(config, registry) {
93
100
  const auditLog = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
94
101
  const lockStore = new SessionLockStore(config.workspace, config.stateBackend);
95
102
  const userStore = new UserStore();
103
+ const authService = new RelayAuthService(config);
96
104
  const contextUsers = new WeakMap();
97
105
  const agentUpdateActors = new Map();
98
106
  const agentUpdateStates = new Map();
@@ -155,17 +163,10 @@ export function createBot(config, registry) {
155
163
  }
156
164
  }
157
165
  });
158
- const getBusyState = (contextKey) => {
159
- let state = contextBusy.get(contextKey);
160
- if (!state) {
161
- state = { processing: false, switching: false, transcribing: false, approving: false };
162
- contextBusy.set(contextKey, state);
163
- }
164
- return state;
165
- };
166
+ const getBusyState = (contextKey) => contextBusy.get(contextKey);
166
167
  const getExternalActivity = (session) => getExternalActivityForSession(session, config);
167
168
  const getBusyReason = (contextKey) => {
168
- const state = contextBusy.get(contextKey);
169
+ const state = contextBusy.peek(contextKey);
169
170
  const session = registry.get(contextKey);
170
171
  if (state?.processing || state?.switching || state?.transcribing || state?.approving || session?.isProcessing()) {
171
172
  return { busy: true, kind: "connector", state: state ?? getBusyState(contextKey) };
@@ -191,41 +192,14 @@ export function createBot(config, registry) {
191
192
  registry.updateMetadata(contextKey, session);
192
193
  };
193
194
  const checkAgentAuthStatus = async (info) => {
194
- if (idOf(info) === "pi") {
195
- return checkPiAuthStatus(info.model);
196
- }
197
- if (idOf(info) === "hermes") {
198
- return checkHermesAuthStatus({
199
- baseUrl: config.hermesApiBaseUrl,
200
- apiKey: config.hermesApiKey,
201
- });
202
- }
203
- if (idOf(info) === "openclaw") {
204
- return checkOpenClawAuthStatus({
205
- gatewayUrl: config.openClawGatewayUrl,
206
- token: config.openClawGatewayToken,
207
- password: config.openClawGatewayPassword,
208
- });
209
- }
210
- if (idOf(info) === "claude-code") {
211
- return checkClaudeCodeAuthStatus(config.claudeCodeCliPath);
212
- }
213
- return checkAuthStatus(config.codexApiKey);
195
+ const status = await authService.check(info);
196
+ return { ...status, method: status.method ?? "unknown" };
214
197
  };
215
- const agentIdForAuth = (info) => info ? idOf(info) : "codex";
216
- const labelForAuth = (info) => info ? labelOf(info) : "Codex";
198
+ const agentIdForAuth = resolveAgentIdForAuth;
199
+ const labelForAuth = agentLabelForAuth;
217
200
  const checkLoginAuthStatus = async (info) => {
218
- const agentId = agentIdForAuth(info);
219
- if (agentId === "hermes") {
220
- return checkHermesAuthStatus({
221
- baseUrl: config.hermesApiBaseUrl,
222
- apiKey: config.hermesApiKey,
223
- });
224
- }
225
- if (agentId === "claude-code") {
226
- return checkClaudeCodeAuthStatus(config.claudeCodeCliPath);
227
- }
228
- return checkAuthStatus(config.codexApiKey);
201
+ const status = info ? await authService.check(info) : await checkAuthStatus(config.codexApiKey);
202
+ return { ...status, method: status.method ?? "unknown" };
229
203
  };
230
204
  const replyChannelAction = async (ctx, rendered) => {
231
205
  const channelContext = telegramChannelContextFromCtx(ctx);
@@ -280,44 +254,16 @@ export function createBot(config, registry) {
280
254
  }
281
255
  };
282
256
  const startAgentLogin = (info) => {
283
- const agentId = agentIdForAuth(info);
284
- if (agentId === "hermes") {
285
- return startHermesLogin(config.hermesCliPath);
286
- }
287
- if (agentId === "claude-code") {
288
- return startClaudeCodeLogin(config.claudeCodeCliPath);
289
- }
290
- return startCodexLogin();
257
+ return info ? authService.startLogin(info) : startCodexLogin();
291
258
  };
292
259
  const startAgentLogout = (info) => {
293
- const agentId = agentIdForAuth(info);
294
- if (agentId === "hermes") {
295
- return startHermesLogout(config.hermesCliPath);
296
- }
297
- if (agentId === "claude-code") {
298
- return startClaudeCodeLogout(config.claudeCodeCliPath);
299
- }
300
- return startCodexLogout();
260
+ return info ? authService.startLogout(info) : startCodexLogout();
301
261
  };
302
262
  const hostLoginCommand = (info) => {
303
- const agentId = agentIdForAuth(info);
304
- if (agentId === "hermes") {
305
- return `${config.hermesCliPath ?? "hermes"} login --no-browser`;
306
- }
307
- if (agentId === "claude-code") {
308
- return `${config.claudeCodeCliPath ?? "claude"} auth login`;
309
- }
310
- return "codex login --device-auth";
263
+ return hostAgentLoginCommand(config, info);
311
264
  };
312
265
  const hostLogoutCommand = (info) => {
313
- const agentId = agentIdForAuth(info);
314
- if (agentId === "hermes") {
315
- return `${config.hermesCliPath ?? "hermes"} logout`;
316
- }
317
- if (agentId === "claude-code") {
318
- return `${config.claudeCodeCliPath ?? "claude"} auth logout`;
319
- }
320
- return "codex logout";
266
+ return hostAgentLogoutCommand(config, info);
321
267
  };
322
268
  const isTopicContext = (contextKey) => isTopicContextKey(contextKey);
323
269
  const getPreferences = (contextKey) => preferencesStore.get(contextKey);
@@ -421,276 +367,24 @@ export function createBot(config, registry) {
421
367
  return item;
422
368
  };
423
369
  const monitorExternalContexts = async () => {
424
- const contextKeys = new Set([
425
- ...registry.listContexts().map((context) => context.contextKey),
426
- ...promptStore.listContextKeys(),
427
- ].filter(isTelegramContextKey));
428
- for (const contextKey of contextKeys) {
429
- await monitorExternalContext(contextKey);
430
- }
431
- };
432
- const monitorExternalContext = async (contextKey) => {
433
- if (!isTelegramContextKey(contextKey)) {
434
- return;
435
- }
436
- if (!canSendSystemMessagesToContext(contextKey)) {
437
- return;
438
- }
439
- const session = await registry.getOrCreate(contextKey, { deferThreadStart: true }).catch(() => null);
440
- if (!session) {
441
- return;
442
- }
443
- const info = session.getInfo();
444
- if (!capabilitiesOf(info).externalActivity) {
445
- const parsed = parseContextKey(contextKey);
446
- const queueLength = promptStore.list(contextKey).length;
447
- if (queueLength > 0 && !promptStore.isPaused(contextKey) && !session.isProcessing()) {
448
- await drainQueuedPrompts(createSystemContext(contextKey), contextKey, parsed.chatId, session);
449
- }
450
- return;
451
- }
452
- const threadId = session.getActiveThreadId();
453
- const parsed = parseContextKey(contextKey);
454
- const queueLength = promptStore.list(contextKey).length;
455
- const paused = promptStore.isPaused(contextKey);
456
- if (!threadId) {
457
- if (queueLength > 0 && !paused && !session.isProcessing()) {
458
- await drainQueuedPrompts(createSystemContext(contextKey), contextKey, parsed.chatId, session);
459
- }
460
- return;
461
- }
462
- const previous = externalMirrors.get(contextKey);
463
- const snapshot = getExternalSnapshotForSession(session, config, {
464
- afterLine: previous?.lastLine ?? Number.MAX_SAFE_INTEGER,
465
- }) ?? getExternalSnapshotForSession(session, config, {
466
- maxEvents: 0,
467
- });
468
- if (!snapshot) {
469
- if (queueLength > 0 && !paused && !session.isProcessing()) {
370
+ await monitorChannelExternalContexts({
371
+ config,
372
+ registry,
373
+ promptStore,
374
+ isContextKey: isTelegramContextKey,
375
+ canSendSystemMessages: canSendSystemMessagesToContext,
376
+ contextForKey: channelContextFromTelegramKey,
377
+ previousLastLine: (contextKey) => externalMirrors.get(contextKey)?.lastLine,
378
+ mirrorSnapshot: async (contextKey, _context, session, snapshot) => {
379
+ const parsed = parseContextKey(contextKey);
380
+ await mirrorExternalSnapshot(contextKey, parsed.chatId, session, snapshot);
381
+ },
382
+ updateQueueStatus: (contextKey, _context, text) => updateQueueStatusMessage(contextKey, text),
383
+ drainQueue: async (contextKey, _context, session) => {
384
+ const parsed = parseContextKey(contextKey);
470
385
  await drainQueuedPrompts(createSystemContext(contextKey), contextKey, parsed.chatId, session);
471
- }
472
- return;
473
- }
474
- if (!session.isProcessing()) {
475
- await mirrorExternalSnapshot(contextKey, parsed.chatId, session, snapshot);
476
- }
477
- const activity = snapshot.activity;
478
- if (activity.active && queueLength > 0) {
479
- await updateQueueStatusMessage(contextKey, `Waiting for ${info.agentLabel} CLI task... ${queueLength} queued${paused ? " (paused)" : ""}.`);
480
- return;
481
- }
482
- if (!activity.active && queueLength > 0 && !paused && !session.isProcessing()) {
483
- await updateQueueStatusMessage(contextKey, `CLI task finished, running queued prompt 1/${queueLength}.`);
484
- await drainQueuedPrompts(createSystemContext(contextKey), contextKey, parsed.chatId, session);
485
- }
486
- };
487
- const sendExternalMirrorTyping = async (chatId, messageThreadId, state) => {
488
- const now = Date.now();
489
- if (state.lastTypingAt && now - state.lastTypingAt < TYPING_INTERVAL_MS) {
490
- return;
491
- }
492
- state.lastTypingAt = now;
493
- await sendChatActionSafe(bot.api, chatId, "typing", messageThreadId).catch(() => { });
494
- };
495
- const sendExternalWorkingNotice = async (chatId, messageThreadId, state, snapshot) => {
496
- const turnKey = snapshot.activity.turnId ?? snapshot.activity.startedAt?.toISOString() ?? "unknown";
497
- if (state.workingNoticeTurnKey === turnKey) {
498
- return;
499
- }
500
- const prompt = trimLine(snapshot.latestUserMessage ?? "", 250);
501
- const fallbackText = prompt ? `Working on ${prompt}` : `Working on external ${snapshot.agentLabel} task...`;
502
- const html = prompt
503
- ? `<b>Working on</b> ${escapeHTML(prompt)}`
504
- : `<b>Working on</b> external ${escapeHTML(snapshot.agentLabel)} task...`;
505
- await sendTextMessage(bot.api, chatId, html, {
506
- fallbackText,
507
- messageThreadId,
386
+ },
508
387
  });
509
- state.workingNoticeTurnKey = turnKey;
510
- };
511
- const mirrorExternalSnapshot = async (contextKey, chatId, session, snapshot) => {
512
- const parsed = parseContextKey(contextKey);
513
- const previous = externalMirrors.get(contextKey);
514
- let state = previous;
515
- if (!state || state.threadId !== snapshot.threadId || state.rolloutPath !== snapshot.sourcePath) {
516
- state = {
517
- threadId: snapshot.threadId,
518
- rolloutPath: snapshot.sourcePath,
519
- lastLine: snapshot.lineCount,
520
- turnId: snapshot.activity.turnId,
521
- startedAt: snapshot.activity.startedAt,
522
- };
523
- externalMirrors.set(contextKey, state);
524
- }
525
- const mirrorMode = getEffectiveMirrorMode(contextKey);
526
- if (snapshot.activity.active) {
527
- state.turnId = snapshot.activity.turnId;
528
- state.startedAt = snapshot.activity.startedAt;
529
- const turnKey = snapshot.activity.turnId ?? snapshot.activity.startedAt?.toISOString() ?? "unknown";
530
- if (state.activityStartedTurnKey !== turnKey) {
531
- const info = session.getInfo();
532
- appendActivity({
533
- source: "cli",
534
- status: "running",
535
- type: "cli_turn_started",
536
- contextKey,
537
- threadId: snapshot.threadId,
538
- workspace: info.workspace,
539
- agentId: info.agentId,
540
- actor: CLI_ACTIVITY_ACTOR,
541
- prompt: snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`,
542
- detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
543
- });
544
- state.activityStartedTurnKey = turnKey;
545
- state.activityFinishedTurnKey = undefined;
546
- state.activityToolStartLines = [];
547
- state.activityToolEndLines = [];
548
- }
549
- if (mirrorMode !== "off") {
550
- await sendExternalMirrorTyping(chatId, parsed.messageThreadId, state);
551
- }
552
- if (mirrorMode === "final") {
553
- await sendExternalWorkingNotice(chatId, parsed.messageThreadId, state, snapshot);
554
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
555
- return;
556
- }
557
- if (mirrorMode === "off") {
558
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
559
- return;
560
- }
561
- const status = renderExternalMirrorStatus(snapshot, promptStore.list(contextKey).length);
562
- const now = Date.now();
563
- const canUpdateStatus = !state.latestStatusAt || now - state.latestStatusAt >= config.telegramMirrorMinUpdateMs;
564
- if (!state.statusMessageId) {
565
- const message = await sendTextMessage(bot.api, chatId, status.html, {
566
- fallbackText: status.plain,
567
- messageThreadId: parsed.messageThreadId,
568
- });
569
- state.statusMessageId = message.message_id;
570
- state.latestStatusAt = now;
571
- }
572
- else if (state.latestStatus !== status.plain && canUpdateStatus) {
573
- await safeEditMessage(bot, chatId, state.statusMessageId, status.html, {
574
- fallbackText: status.plain,
575
- });
576
- state.latestStatusAt = now;
577
- }
578
- state.latestStatus = status.plain;
579
- if (mirrorMode === "full") {
580
- const newEvents = snapshot.events
581
- .filter((event) => event.lineNumber > (state.latestMirroredEventLine ?? state.lastLine))
582
- .filter((event) => event.kind === "tool" || event.kind === "task")
583
- .slice(-4);
584
- for (const event of newEvents) {
585
- const rendered = renderExternalMirrorEvent(event);
586
- if (!rendered) {
587
- continue;
588
- }
589
- await sendTextMessage(bot.api, chatId, rendered.html, {
590
- fallbackText: rendered.plain,
591
- messageThreadId: parsed.messageThreadId,
592
- });
593
- state.latestMirroredEventLine = event.lineNumber;
594
- }
595
- }
596
- const info = session.getInfo();
597
- const loggedStartLines = new Set(state.activityToolStartLines ?? []);
598
- const loggedEndLines = new Set(state.activityToolEndLines ?? []);
599
- for (const event of snapshot.events.filter((event) => event.lineNumber > state.lastLine && event.kind === "tool")) {
600
- if (event.status === "started" && !loggedStartLines.has(event.lineNumber)) {
601
- appendActivity({
602
- source: "cli",
603
- status: "running",
604
- type: "cli_tool_started",
605
- contextKey,
606
- threadId: snapshot.threadId,
607
- workspace: info.workspace,
608
- agentId: info.agentId,
609
- actor: CLI_ACTIVITY_ACTOR,
610
- prompt: snapshot.latestUserMessage ?? undefined,
611
- detail: event.toolName ?? "tool",
612
- });
613
- loggedStartLines.add(event.lineNumber);
614
- }
615
- if ((event.status === "finished" || event.status === "failed") && !loggedEndLines.has(event.lineNumber)) {
616
- appendActivity({
617
- source: "cli",
618
- status: event.status === "failed" ? "failed" : "completed",
619
- type: event.status === "failed" ? "cli_tool_failed" : "cli_tool_completed",
620
- contextKey,
621
- threadId: snapshot.threadId,
622
- workspace: info.workspace,
623
- agentId: info.agentId,
624
- actor: CLI_ACTIVITY_ACTOR,
625
- prompt: snapshot.latestUserMessage ?? undefined,
626
- detail: event.toolName ?? "tool",
627
- });
628
- loggedEndLines.add(event.lineNumber);
629
- }
630
- }
631
- state.activityToolStartLines = [...loggedStartLines].slice(-200);
632
- state.activityToolEndLines = [...loggedEndLines].slice(-200);
633
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
634
- return;
635
- }
636
- if (!previous) {
637
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
638
- return;
639
- }
640
- const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
641
- if (terminalEvent) {
642
- const turnKey = terminalEvent.turnId ?? snapshot.activity.turnId ?? state.startedAt?.toString() ?? "unknown";
643
- if (state.activityFinishedTurnKey !== turnKey) {
644
- const info = session.getInfo();
645
- const startedAt = state.startedAt instanceof Date ? state.startedAt : state.startedAt ? new Date(state.startedAt) : snapshot.activity.startedAt;
646
- appendActivity({
647
- source: "cli",
648
- status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
649
- type: "cli_turn_finished",
650
- contextKey,
651
- threadId: snapshot.threadId,
652
- workspace: info.workspace,
653
- agentId: info.agentId,
654
- actor: CLI_ACTIVITY_ACTOR,
655
- prompt: snapshot.latestUserMessage ?? undefined,
656
- detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
657
- durationMs: startedAt && terminalEvent.timestamp ? Math.max(0, terminalEvent.timestamp.getTime() - startedAt.getTime()) : undefined,
658
- });
659
- state.activityFinishedTurnKey = turnKey;
660
- }
661
- if (mirrorMode !== "off") {
662
- const doneText = `${snapshot.agentLabel} CLI task ${terminalEvent.status}.`;
663
- if (state.statusMessageId) {
664
- await safeEditMessage(bot, chatId, state.statusMessageId, escapeHTML(doneText), {
665
- fallbackText: doneText,
666
- });
667
- }
668
- else if (shouldNotify(contextKey, "minimal")) {
669
- await sendTextMessage(bot.api, chatId, escapeHTML(doneText), {
670
- fallbackText: doneText,
671
- messageThreadId: parsed.messageThreadId,
672
- });
673
- }
674
- }
675
- const finalAgent = snapshot.events.filter((event) => event.kind === "agent" && event.text).at(-1);
676
- if (mirrorMode !== "off" && mirrorMode !== "status" && finalAgent?.text && finalAgent.lineNumber !== state.latestAgentLine) {
677
- await sendTextMessage(bot.api, chatId, `<b>${escapeHTML(snapshot.agentLabel)} CLI final answer:</b>`, {
678
- fallbackText: `${snapshot.agentLabel} CLI final answer:`,
679
- messageThreadId: parsed.messageThreadId,
680
- });
681
- for (const chunk of splitMarkdownForTelegram(finalAgent.text)) {
682
- await sendTextMessage(bot.api, chatId, chunk.text, {
683
- parseMode: chunk.parseMode,
684
- fallbackText: chunk.fallbackText,
685
- messageThreadId: parsed.messageThreadId,
686
- });
687
- }
688
- state.latestAgentLine = finalAgent.lineNumber;
689
- }
690
- await deliverCliGeneratedArtifacts(contextKey, chatId, session, state.startedAt, terminalEvent.turnId, parsed.messageThreadId);
691
- }
692
- state.workingNoticeTurnKey = undefined;
693
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
694
388
  };
695
389
  const canSendSystemMessagesToContext = (contextKey) => {
696
390
  if (!userStore.hasAdminUser()) {
@@ -706,59 +400,116 @@ export function createBot(config, registry) {
706
400
  if (!canSendSystemMessagesToContext(contextKey)) {
707
401
  return;
708
402
  }
709
- if (!startedAt || !turnId) {
710
- return;
711
- }
712
- const state = externalMirrors.get(contextKey);
713
- if (state?.artifactsDeliveredForTurnId === turnId) {
714
- return;
715
- }
716
- const workspace = session.getInfo().workspace;
717
- const report = await collectRecentWorkspaceArtifacts(workspace, {
718
- since: startedAt,
719
- until: new Date(),
720
- maxFileSize: config.maxFileSize,
721
- limit: 5,
722
- ignoreDirs: config.artifactIgnoreDirs,
723
- ignoreGlobs: config.artifactIgnoreGlobs,
724
- });
725
- if (isEmptyArtifactReport(report)) {
726
- if (state)
727
- state.artifactsDeliveredForTurnId = turnId;
728
- return;
729
- }
730
- const persistedReport = await persistWorkspaceArtifactReport(workspace, turnId, report).catch((error) => {
731
- console.error("Failed to persist CLI artifact report:", error);
732
- return null;
733
- });
734
- if (!config.telegramAutoSendArtifacts) {
735
- if (state)
736
- state.artifactsDeliveredForTurnId = turnId;
737
- return;
738
- }
739
- const summary = formatArtifactSummary(report.artifacts, report.skippedCount, report.omittedCount);
740
- await sendTextMessage(bot.api, chatId, escapeHTML(summary), {
741
- fallbackText: summary,
742
- messageThreadId,
743
- });
744
- for (const artifact of (persistedReport?.artifacts ?? report.artifacts)) {
745
- await sendArtifactFileByApi(bot.api, chatId, artifact, messageThreadId);
746
- }
747
- const info = session.getInfo();
748
- appendActivity({
749
- source: "cli",
750
- status: "info",
751
- type: "artifacts_sent",
403
+ await deliverChannelCliArtifacts({
404
+ config,
752
405
  contextKey,
753
- threadId: info.threadId,
754
- workspace: info.workspace,
755
- agentId: info.agentId,
756
- actor: CLI_ACTIVITY_ACTOR,
757
- detail: summary,
406
+ session,
407
+ startedAt,
408
+ turnId,
409
+ state: externalMirrors.get(contextKey),
410
+ autoSend: config.telegramAutoSendArtifacts,
411
+ sendSummaryWhenAutoSendDisabled: false,
412
+ logPrefix: "Telegram",
413
+ sendSummary: (summary) => sendTextMessage(bot.api, chatId, escapeHTML(summary), {
414
+ fallbackText: summary,
415
+ messageThreadId,
416
+ }).then(() => { }),
417
+ sendArtifact: (artifact) => sendArtifactFileByApi(bot.api, chatId, artifact, messageThreadId).then(() => { }),
418
+ appendActivity,
758
419
  });
759
- if (state)
760
- state.artifactsDeliveredForTurnId = turnId;
761
420
  };
421
+ const channelContextFromTelegramKey = (contextKey) => {
422
+ const parsed = parseContextKey(contextKey);
423
+ return {
424
+ channelId: "telegram",
425
+ chatId: String(parsed.chatId),
426
+ ...(parsed.messageThreadId ? { topicId: String(parsed.messageThreadId) } : {}),
427
+ };
428
+ };
429
+ const externalMirrorController = createChannelExternalMirrorController({
430
+ config,
431
+ states: externalMirrors,
432
+ typingIntervalMs: TYPING_INTERVAL_MS,
433
+ minUpdateMs: () => config.telegramMirrorMinUpdateMs,
434
+ mirrorMode: (contextKey) => getEffectiveMirrorMode(contextKey),
435
+ queueLength: (contextKey) => promptStore.list(contextKey).length,
436
+ activityActor: () => CLI_ACTIVITY_ACTOR,
437
+ appendActivity,
438
+ sendTyping: async (contextKey) => {
439
+ const parsed = parseContextKey(contextKey);
440
+ await sendChatActionSafe(bot.api, parsed.chatId, "typing", parsed.messageThreadId).catch(() => { });
441
+ },
442
+ sendWorkingNotice: async (contextKey, _context, state, snapshot, prompt) => {
443
+ const turnKey = snapshot.activity.turnId ?? snapshot.activity.startedAt?.toISOString() ?? "unknown";
444
+ if (state.workingNoticeTurnKey === turnKey) {
445
+ return;
446
+ }
447
+ const parsed = parseContextKey(contextKey);
448
+ const fallbackText = prompt ? `Working on ${prompt}` : `Working on external ${snapshot.agentLabel} task...`;
449
+ const html = prompt
450
+ ? `<b>Working on</b> ${escapeHTML(prompt)}`
451
+ : `<b>Working on</b> external ${escapeHTML(snapshot.agentLabel)} task...`;
452
+ await sendTextMessage(bot.api, parsed.chatId, html, {
453
+ fallbackText,
454
+ messageThreadId: parsed.messageThreadId,
455
+ });
456
+ state.workingNoticeTurnKey = turnKey;
457
+ },
458
+ sendStatus: async (contextKey, _context, _state, rendered) => {
459
+ const parsed = parseContextKey(contextKey);
460
+ const message = await sendTextMessage(bot.api, parsed.chatId, rendered.html, {
461
+ fallbackText: rendered.plain,
462
+ messageThreadId: parsed.messageThreadId,
463
+ });
464
+ return message.message_id;
465
+ },
466
+ editStatus: async (contextKey, _context, _state, messageId, rendered) => {
467
+ const parsed = parseContextKey(contextKey);
468
+ await safeEditMessage(bot, parsed.chatId, messageId, rendered.html, {
469
+ fallbackText: rendered.plain,
470
+ });
471
+ },
472
+ sendEvent: async (contextKey, _context, _state, rendered) => {
473
+ const parsed = parseContextKey(contextKey);
474
+ await sendTextMessage(bot.api, parsed.chatId, rendered.html, {
475
+ fallbackText: rendered.plain,
476
+ messageThreadId: parsed.messageThreadId,
477
+ });
478
+ },
479
+ sendDone: async (contextKey, _context, state, text) => {
480
+ const parsed = parseContextKey(contextKey);
481
+ if (state.statusMessageId) {
482
+ await safeEditMessage(bot, parsed.chatId, state.statusMessageId, escapeHTML(text), {
483
+ fallbackText: text,
484
+ });
485
+ return;
486
+ }
487
+ await sendTextMessage(bot.api, parsed.chatId, escapeHTML(text), {
488
+ fallbackText: text,
489
+ messageThreadId: parsed.messageThreadId,
490
+ });
491
+ },
492
+ sendFinalAnswer: async (contextKey, _context, _state, snapshot, text) => {
493
+ const parsed = parseContextKey(contextKey);
494
+ await sendTextMessage(bot.api, parsed.chatId, `<b>${escapeHTML(snapshot.agentLabel)} CLI final answer:</b>`, {
495
+ fallbackText: `${snapshot.agentLabel} CLI final answer:`,
496
+ messageThreadId: parsed.messageThreadId,
497
+ });
498
+ for (const chunk of splitMarkdownForTelegram(text)) {
499
+ await sendTextMessage(bot.api, parsed.chatId, chunk.text, {
500
+ parseMode: chunk.parseMode,
501
+ fallbackText: chunk.fallbackText,
502
+ messageThreadId: parsed.messageThreadId,
503
+ });
504
+ }
505
+ },
506
+ deliverArtifacts: (contextKey, _context, session, state, turnId) => {
507
+ const parsed = parseContextKey(contextKey);
508
+ return deliverCliGeneratedArtifacts(contextKey, parsed.chatId, session, state.startedAt, turnId, parsed.messageThreadId);
509
+ },
510
+ shouldSendDone: (contextKey) => shouldNotify(contextKey, "minimal"),
511
+ });
512
+ const mirrorExternalSnapshot = (contextKey, _chatId, session, snapshot) => externalMirrorController.mirror(contextKey, channelContextFromTelegramKey(contextKey), session, snapshot);
762
513
  const scheduleExternalQueueDrain = (ctx, contextKey, chatId, session) => {
763
514
  if (externalQueueTimers.has(contextKey)) {
764
515
  return;