@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
@@ -1,46 +1,47 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { Client, Events, GatewayIntentBits, Partials, REST, Routes, } from "discord.js";
3
- import { ADMIN_GROUP_ID } from "./access-control.js";
4
- import { agentLabel, agentReasoningLabel, agentReasoningOptions } from "./agent.js";
5
- import { getAgentActivityLog, getExternalSnapshotForSession } from "./agent-activity.js";
6
- import { listAgentAdapterDescriptors } from "./agent-adapter.js";
7
- import { AgentUpdateManager } from "./agent-updates.js";
8
- import { enabledAgents } from "./agent-factory.js";
9
- import { collectRecentWorkspaceArtifacts, ensureOutDir, formatArtifactSummary, persistWorkspaceArtifactReport } from "./artifacts.js";
10
- import { buildFileInstructions, outboxPath, stageFile } from "./attachments.js";
11
- import { AuditLogStore } from "./audit-log.js";
12
- import { BotPreferencesStore } from "./bot-preferences.js";
13
- import { capabilitiesOf, filterActivityEvents, formatLocalDateTime, parseActivityOptions, renderExternalMirrorEvent, renderExternalMirrorStatus, renderPromptFailure, trimLine } from "./bot-rendering.js";
14
- import { renderAgentUpdateJobAction, renderAgentUpdateJobsAction, renderAgentUpdateLogAction, renderAgentUpdatePickerAction, renderQueueListAction } from "./channel-actions.js";
15
- import { createSharedChannelCommandDispatcher } from "./channel-command-core.js";
16
- import { ChannelCommandService } from "./channel-command-service.js";
17
- import { discordHelpCommandList } from "./channel-command-catalog.js";
18
- import { createChannelPromptEngine } from "./channel-prompt-engine.js";
19
- import { runChannelPeerPrompt } from "./channel-peer-prompt.js";
20
- import { deliverChannelAction } from "./channel-runtime.js";
21
- import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
22
- import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
23
- import { discordContextKey, isDiscordContextKey, parseDiscordContextKey } from "./context-key.js";
3
+ import { ADMIN_GROUP_ID } from "../../access/access-control.js";
4
+ import { agentLabel, agentReasoningLabel, agentReasoningOptions } from "../../agents/shared/agent.js";
5
+ import { getAgentActivityLog, getExternalSnapshotForSession } from "../../agents/shared/agent-activity.js";
6
+ import { hostAgentLoginCommand, hostAgentLogoutCommand } from "../../agents/shared/agent-auth-commands.js";
7
+ import { listAgentAdapterDescriptors } from "../../agents/shared/agent-adapter.js";
8
+ import { AgentUpdateManager } from "../../agents/shared/agent-updates.js";
9
+ import { enabledAgents } from "../../agents/shared/agent-factory.js";
10
+ import { ensureOutDir } from "../../artifacts/artifacts.js";
11
+ import { buildFileInstructions, outboxPath, stageFile } from "../../artifacts/attachments.js";
12
+ import { AuditLogStore } from "../../access/audit-log.js";
13
+ import { BotPreferencesStore } from "../../state/bot-preferences.js";
14
+ import { capabilitiesOf, filterActivityEvents, formatLocalDateTime, parseActivityOptions, renderPromptFailure, trimLine } from "../shared/bot-rendering.js";
15
+ import { renderAgentUpdateJobAction, renderAgentUpdateJobsAction, renderAgentUpdateLogAction, renderAgentUpdatePickerAction, renderQueueListAction } from "../shared/channel-actions.js";
16
+ import { createChannelActivityRecorder, createChannelAuditRecorder, createChannelBusyStore, createChannelPermissionChecker, createChannelQueueStatusController, } from "../shared/channel-bridge-controller.js";
17
+ import { createSharedChannelCommandDispatcher } from "../shared/channel-command-core.js";
18
+ import { ChannelCommandService } from "../shared/channel-command-service.js";
19
+ import { discordHelpCommandList } from "../shared/channel-command-catalog.js";
20
+ import { createChannelPromptEngine } from "../shared/channel-prompt-engine.js";
21
+ import { runChannelPeerPrompt } from "../shared/channel-peer-prompt.js";
22
+ import { deliverChannelAction } from "../shared/channel-runtime.js";
23
+ import { deliverChannelCliArtifacts } from "../shared/channel-cli-artifacts.js";
24
+ import { createChannelExternalMirrorController } from "../shared/channel-external-mirror-controller.js";
25
+ import { monitorChannelExternalContexts } from "../shared/channel-external-monitor.js";
26
+ import { discordContextKey, isDiscordContextKey, parseDiscordContextKey } from "../shared/context-key.js";
24
27
  import { DiscordBotChannelRuntime, actionFromDiscordCustomId, discordActionRows, splitDiscordMessage, trimDiscordMessage } from "./discord-channel-runtime.js";
25
28
  import { createDiscordArtifactCommandHandler, sendRecentDiscordArtifacts } from "./discord-artifacts.js";
26
29
  import { argumentFromDiscordInteraction, discordCommands, isUnauthenticatedDiscordCommandAllowed, parseDiscordMessageCommand, permissionForDiscordAction, requiredPermissionForDiscordCommand } from "./discord-command-surface.js";
27
30
  import { discordRateLimiter, getDiscordRateLimitMetrics } from "./discord-rate-limit.js";
28
- import { friendlyErrorText } from "./error-messages.js";
29
- import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
30
- import { spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
31
- import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
32
- import { RemoteRelayClient } from "./peer-client.js";
33
- import { checkPiAuthStatus } from "./pi-auth.js";
34
- import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
35
- import { RelayArtifactService } from "./relay-artifact-service.js";
36
- import { configureRedaction, redactText } from "./redaction.js";
37
- import { renderSessionInfoPlain } from "./session-format.js";
38
- import { canWriteWithLock, SessionLockStore } from "./session-locks.js";
39
- import { SessionRegistry } from "./session-registry.js";
40
- import { transcribeAudio } from "./voice.js";
41
- import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
42
- import { UserStore } from "./user-management.js";
43
- import { WebActivityStore } from "./web-state.js";
31
+ import { friendlyErrorText } from "../../core/error-messages.js";
32
+ import { spawnConnectorRestart, spawnSelfUpdate } from "../../support/operations.js";
33
+ import { RemoteRelayClient } from "../../peers/peer-client.js";
34
+ import { PromptStore, toPromptEnvelope } from "../../state/prompt-store.js";
35
+ import { RelayArtifactService } from "../../runtime/relay-artifact-service.js";
36
+ import { RelayAuthService } from "../../runtime/relay-auth-service.js";
37
+ import { configureRedaction, redactText } from "../../core/redaction.js";
38
+ import { renderSessionInfoPlain } from "../shared/session-format.js";
39
+ import { canWriteWithLock, SessionLockStore } from "../../access/session-locks.js";
40
+ import { SessionRegistry } from "../../state/session-registry.js";
41
+ import { transcribeAudio } from "../../artifacts/voice.js";
42
+ import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "../../core/workspace-policy.js";
43
+ import { UserStore } from "../../access/user-management.js";
44
+ import { WebActivityStore } from "../../web/web-state.js";
44
45
  export { isUnauthenticatedDiscordCommandAllowed, permissionForDiscordAction, requiredPermissionForDiscordCommand } from "./discord-command-surface.js";
45
46
  const EDIT_DEBOUNCE_MS = 1500;
46
47
  const TYPING_INTERVAL_MS = 4500;
@@ -75,24 +76,23 @@ export function createDiscordBridge(config, registry) {
75
76
  const lockStore = new SessionLockStore(config.workspace, config.stateBackend);
76
77
  const userStore = new UserStore();
77
78
  const artifactService = new RelayArtifactService(config);
79
+ const authService = new RelayAuthService(config);
78
80
  const agentUpdates = new AgentUpdateManager();
79
81
  const commandService = new ChannelCommandService(config);
80
- const busyStates = new Map();
82
+ const busyStates = createChannelBusyStore();
81
83
  const turnProgress = new Map();
82
84
  const draining = new Set();
83
85
  const picks = new Map();
84
86
  const responseOwners = new Map();
85
87
  const externalMirrors = new Map();
86
- const queueStatusMessages = new Map();
88
+ const queueStatusMessages = createChannelQueueStatusController({
89
+ send: async (_contextKey, context, text) => (await runtime.sendMessage(context, { text, fallbackText: text })).messageId,
90
+ edit: async (_contextKey, context, messageId, text) => {
91
+ await runtime.editMessage(context, messageId, { text, fallbackText: text });
92
+ },
93
+ });
87
94
  let externalMonitor;
88
- const getBusyState = (contextKey) => {
89
- let state = busyStates.get(contextKey);
90
- if (!state) {
91
- state = { processing: false, switching: false };
92
- busyStates.set(contextKey, state);
93
- }
94
- return state;
95
- };
95
+ const getBusyState = (contextKey) => busyStates.get(contextKey);
96
96
  const actorFor = (request) => ({
97
97
  channel: "discord",
98
98
  id: request.authUser?.user.id ?? `discord:${request.user.id}`,
@@ -100,27 +100,19 @@ export function createDiscordBridge(config, registry) {
100
100
  username: request.authUser?.user.email ?? request.user.username,
101
101
  channelUserId: request.user.id,
102
102
  });
103
- const appendActivity = (request, input) => {
104
- activityStore.append({
105
- source: "discord",
106
- contextKey: request.contextKey,
107
- actor: input.actor ?? actorFor(request),
108
- workspace: input.workspace ?? config.workspace,
109
- threadId: input.threadId ?? null,
110
- ...input,
111
- });
112
- };
113
- const audit = (request, input) => {
114
- auditLog.append({
115
- channelId: "discord",
116
- contextKey: input.contextKey ?? request.contextKey,
117
- actor: input.actor ?? actorFor(request),
118
- actorId: request.authUser?.user.id ?? request.user.id,
119
- actorRole: request.authUser?.groups.map((group) => group.name).join(", ") ?? "unauthenticated",
120
- ...input,
121
- });
122
- };
123
- const hasPermission = (request, permission) => userStore.hasPermission(request.authUser, permission);
103
+ const appendActivity = createChannelActivityRecorder({
104
+ source: "discord",
105
+ workspace: config.workspace,
106
+ activityStore,
107
+ actorFor,
108
+ });
109
+ const audit = createChannelAuditRecorder({
110
+ channelId: "discord",
111
+ auditLog,
112
+ actorFor,
113
+ actorIdFor: (request) => request.user.id,
114
+ });
115
+ const hasPermission = createChannelPermissionChecker(userStore);
124
116
  const reply = async (request, content, options = {}) => {
125
117
  const chunks = splitDiscordMessage(content);
126
118
  if (request.interaction) {
@@ -232,7 +224,7 @@ export function createDiscordBridge(config, registry) {
232
224
  };
233
225
  const commandArtifacts = createDiscordArtifactCommandHandler(artifactDeps);
234
226
  const getBusyReason = (contextKey) => {
235
- const state = busyStates.get(contextKey);
227
+ const state = busyStates.peek(contextKey);
236
228
  const session = registry.get(contextKey);
237
229
  if (state?.processing || state?.switching || session?.isProcessing()) {
238
230
  return { busy: true, kind: "connector", state: state ?? getBusyState(contextKey) };
@@ -244,210 +236,7 @@ export function createDiscordBridge(config, registry) {
244
236
  return { busy: false, kind: "idle" };
245
237
  };
246
238
  const updateQueueStatusMessage = async (contextKey, context, text) => {
247
- const state = queueStatusMessages.get(contextKey) ?? {};
248
- if (state.lastText === text && state.messageId) {
249
- return;
250
- }
251
- if (!state.messageId) {
252
- const sent = await runtime.sendMessage(context, { text, fallbackText: text });
253
- state.messageId = sent.messageId;
254
- state.lastText = text;
255
- queueStatusMessages.set(contextKey, state);
256
- return;
257
- }
258
- await runtime.editMessage(context, state.messageId, { text, fallbackText: text });
259
- state.lastText = text;
260
- queueStatusMessages.set(contextKey, state);
261
- };
262
- const sendExternalMirrorTyping = async (context, state) => {
263
- const now = Date.now();
264
- if (state.lastTypingAt && now - state.lastTypingAt < TYPING_INTERVAL_MS) {
265
- return;
266
- }
267
- state.lastTypingAt = now;
268
- await runtime.sendTyping(context).catch(() => { });
269
- };
270
- const sendExternalWorkingNotice = async (context, state, snapshot) => {
271
- const turnKey = snapshot.activity.turnId ?? snapshot.activity.startedAt?.toISOString() ?? "unknown";
272
- if (state.workingNoticeTurnKey === turnKey) {
273
- return;
274
- }
275
- const prompt = trimLine(snapshot.latestUserMessage ?? "", 250);
276
- const text = prompt
277
- ? `**Working on** ${prompt}`
278
- : `**Working on** external ${snapshot.agentLabel} task...`;
279
- await runtime.sendMessage(context, {
280
- text,
281
- fallbackText: prompt ? `Working on ${prompt}` : `Working on external ${snapshot.agentLabel} task...`,
282
- });
283
- state.workingNoticeTurnKey = turnKey;
284
- };
285
- const mirrorExternalSnapshot = async (contextKey, context, session, snapshot) => {
286
- const previous = externalMirrors.get(contextKey);
287
- let state = previous;
288
- if (!state || state.threadId !== snapshot.threadId || state.rolloutPath !== snapshot.sourcePath) {
289
- state = {
290
- threadId: snapshot.threadId,
291
- rolloutPath: snapshot.sourcePath,
292
- lastLine: snapshot.lineCount,
293
- turnId: snapshot.activity.turnId,
294
- startedAt: snapshot.activity.startedAt,
295
- };
296
- externalMirrors.set(contextKey, state);
297
- }
298
- const mirrorMode = preferencesStore.get(contextKey).mirrorMode ?? config.discordMirrorMode;
299
- if (snapshot.activity.active) {
300
- state.turnId = snapshot.activity.turnId;
301
- state.startedAt = snapshot.activity.startedAt;
302
- const turnKey = snapshot.activity.turnId ?? snapshot.activity.startedAt?.toISOString() ?? "unknown";
303
- if (state.activityStartedTurnKey !== turnKey) {
304
- const info = session.getInfo();
305
- activityStore.append({
306
- source: "cli",
307
- status: "running",
308
- type: "cli_turn_started",
309
- contextKey,
310
- threadId: snapshot.threadId,
311
- workspace: info.workspace,
312
- agentId: info.agentId,
313
- actor: { channel: "cli", label: `${snapshot.agentLabel} CLI` },
314
- prompt: snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`,
315
- detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
316
- });
317
- state.activityStartedTurnKey = turnKey;
318
- state.activityFinishedTurnKey = undefined;
319
- state.activityToolStartLines = [];
320
- state.activityToolEndLines = [];
321
- }
322
- if (mirrorMode !== "off") {
323
- await sendExternalMirrorTyping(context, state);
324
- }
325
- if (mirrorMode === "final") {
326
- await sendExternalWorkingNotice(context, state, snapshot);
327
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
328
- return;
329
- }
330
- if (mirrorMode === "off") {
331
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
332
- return;
333
- }
334
- const status = renderExternalMirrorStatus(snapshot, promptStore.list(contextKey).length);
335
- const statusMessage = { text: status.html, fallbackText: status.plain, parseMode: "html" };
336
- const now = Date.now();
337
- const canUpdateStatus = !state.latestStatusAt || now - state.latestStatusAt >= config.discordMirrorMinUpdateMs;
338
- if (!state.statusMessageId) {
339
- const sent = await runtime.sendMessage(context, statusMessage);
340
- state.statusMessageId = sent.messageId;
341
- state.latestStatusAt = now;
342
- }
343
- else if (state.latestStatus !== status.plain && canUpdateStatus) {
344
- await runtime.editMessage(context, state.statusMessageId, statusMessage);
345
- state.latestStatusAt = now;
346
- }
347
- state.latestStatus = status.plain;
348
- if (mirrorMode === "full") {
349
- const newEvents = snapshot.events
350
- .filter((event) => event.lineNumber > (state.latestMirroredEventLine ?? state.lastLine))
351
- .filter((event) => event.kind === "tool" || event.kind === "task")
352
- .slice(-4);
353
- for (const event of newEvents) {
354
- const rendered = renderExternalMirrorEvent(event);
355
- if (!rendered) {
356
- continue;
357
- }
358
- await deliverChannelAction(runtime, context, rendered);
359
- state.latestMirroredEventLine = event.lineNumber;
360
- }
361
- }
362
- const info = session.getInfo();
363
- const loggedStartLines = new Set(state.activityToolStartLines ?? []);
364
- const loggedEndLines = new Set(state.activityToolEndLines ?? []);
365
- for (const event of snapshot.events.filter((event) => event.lineNumber > state.lastLine && event.kind === "tool")) {
366
- if (event.status === "started" && !loggedStartLines.has(event.lineNumber)) {
367
- activityStore.append({
368
- source: "cli",
369
- status: "running",
370
- type: "cli_tool_started",
371
- contextKey,
372
- threadId: snapshot.threadId,
373
- workspace: info.workspace,
374
- agentId: info.agentId,
375
- actor: { channel: "cli", label: `${snapshot.agentLabel} CLI` },
376
- prompt: snapshot.latestUserMessage ?? undefined,
377
- detail: event.toolName ?? "tool",
378
- });
379
- loggedStartLines.add(event.lineNumber);
380
- }
381
- if ((event.status === "finished" || event.status === "failed") && !loggedEndLines.has(event.lineNumber)) {
382
- activityStore.append({
383
- source: "cli",
384
- status: event.status === "failed" ? "failed" : "completed",
385
- type: event.status === "failed" ? "cli_tool_failed" : "cli_tool_completed",
386
- contextKey,
387
- threadId: snapshot.threadId,
388
- workspace: info.workspace,
389
- agentId: info.agentId,
390
- actor: { channel: "cli", label: `${snapshot.agentLabel} CLI` },
391
- prompt: snapshot.latestUserMessage ?? undefined,
392
- detail: event.toolName ?? "tool",
393
- });
394
- loggedEndLines.add(event.lineNumber);
395
- }
396
- }
397
- state.activityToolStartLines = [...loggedStartLines].slice(-200);
398
- state.activityToolEndLines = [...loggedEndLines].slice(-200);
399
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
400
- return;
401
- }
402
- if (!previous) {
403
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
404
- return;
405
- }
406
- const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
407
- if (terminalEvent) {
408
- const turnKey = terminalEvent.turnId ?? snapshot.activity.turnId ?? state.startedAt?.toString() ?? "unknown";
409
- if (state.activityFinishedTurnKey !== turnKey) {
410
- const info = session.getInfo();
411
- const startedAt = state.startedAt instanceof Date ? state.startedAt : state.startedAt ? new Date(state.startedAt) : snapshot.activity.startedAt;
412
- activityStore.append({
413
- source: "cli",
414
- status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
415
- type: "cli_turn_finished",
416
- contextKey,
417
- threadId: snapshot.threadId,
418
- workspace: info.workspace,
419
- agentId: info.agentId,
420
- actor: { channel: "cli", label: `${snapshot.agentLabel} CLI` },
421
- prompt: snapshot.latestUserMessage ?? undefined,
422
- detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
423
- durationMs: startedAt && terminalEvent.timestamp ? Math.max(0, terminalEvent.timestamp.getTime() - startedAt.getTime()) : undefined,
424
- });
425
- state.activityFinishedTurnKey = turnKey;
426
- }
427
- if (mirrorMode !== "off") {
428
- const doneText = `${snapshot.agentLabel} CLI task ${terminalEvent.status}.`;
429
- if (state.statusMessageId) {
430
- await runtime.editMessage(context, state.statusMessageId, { text: doneText, fallbackText: doneText });
431
- }
432
- else {
433
- await runtime.sendMessage(context, { text: doneText, fallbackText: doneText });
434
- }
435
- }
436
- const finalAgent = snapshot.events.filter((event) => event.kind === "agent" && event.text).at(-1);
437
- if (mirrorMode !== "off" && mirrorMode !== "status" && finalAgent?.text && finalAgent.lineNumber !== state.latestAgentLine) {
438
- await runtime.sendMessage(context, {
439
- text: `**${snapshot.agentLabel} CLI final answer:**`,
440
- fallbackText: `${snapshot.agentLabel} CLI final answer:`,
441
- });
442
- for (const chunk of splitDiscordMessage(finalAgent.text)) {
443
- await runtime.sendMessage(context, { text: chunk, fallbackText: chunk });
444
- }
445
- state.latestAgentLine = finalAgent.lineNumber;
446
- }
447
- await deliverCliGeneratedArtifacts(contextKey, context, session, state.startedAt, terminalEvent.turnId);
448
- }
449
- state.workingNoticeTurnKey = undefined;
450
- state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
239
+ await queueStatusMessages.update(contextKey, context, text);
451
240
  };
452
241
  const ensureActiveThread = async (request, session) => {
453
242
  if (!session.hasActiveThread()) {
@@ -455,69 +244,15 @@ export function createDiscordBridge(config, registry) {
455
244
  updateSession(request, session);
456
245
  }
457
246
  };
458
- const checkAgentAuthStatus = async (info) => {
459
- if (info.agentId === "pi")
460
- return checkPiAuthStatus(info.model);
461
- if (info.agentId === "hermes")
462
- return checkHermesAuthStatus({ baseUrl: config.hermesApiBaseUrl, apiKey: config.hermesApiKey });
463
- if (info.agentId === "openclaw")
464
- return checkOpenClawAuthStatus({ gatewayUrl: config.openClawGatewayUrl, token: config.openClawGatewayToken, password: config.openClawGatewayPassword });
465
- if (info.agentId === "claude-code")
466
- return checkClaudeCodeAuthStatus(config.claudeCodeCliPath);
467
- return checkAuthStatus(config.codexApiKey);
468
- };
469
- const checkLoginAuthStatus = async (info) => {
470
- if (info.agentId === "hermes")
471
- return checkHermesAuthStatus({ baseUrl: config.hermesApiBaseUrl, apiKey: config.hermesApiKey });
472
- if (info.agentId === "claude-code")
473
- return checkClaudeCodeAuthStatus(config.claudeCodeCliPath);
474
- return checkAuthStatus(config.codexApiKey);
475
- };
476
- const startAgentLogin = (info) => {
477
- if (info.agentId === "hermes")
478
- return startHermesLogin(config.hermesCliPath);
479
- if (info.agentId === "claude-code")
480
- return startClaudeCodeLogin(config.claudeCodeCliPath);
481
- if (info.agentId === "codex")
482
- return startCodexLogin();
483
- return Promise.resolve({
484
- success: false,
485
- message: `${info.agentLabel} login is not managed by NordRelay. Run the agent login flow on the host.`,
486
- });
487
- };
488
- const startAgentLogout = (info) => {
489
- if (info.agentId === "hermes")
490
- return startHermesLogout(config.hermesCliPath);
491
- if (info.agentId === "claude-code")
492
- return startClaudeCodeLogout(config.claudeCodeCliPath);
493
- if (info.agentId === "codex")
494
- return startCodexLogout();
495
- return Promise.resolve({
496
- success: false,
497
- message: `${info.agentLabel} logout is not managed by NordRelay. Run the agent logout flow on the host.`,
498
- });
499
- };
247
+ const checkAgentAuthStatus = (info) => authService.check(info);
248
+ const checkLoginAuthStatus = (info) => authService.check(info);
249
+ const startAgentLogin = (info) => authService.startLogin(info);
250
+ const startAgentLogout = (info) => authService.startLogout(info);
500
251
  const hostLoginCommand = (info) => {
501
- if (info.agentId === "hermes")
502
- return `${config.hermesCliPath ?? "hermes"} login --no-browser`;
503
- if (info.agentId === "claude-code")
504
- return `${config.claudeCodeCliPath ?? "claude"} auth login`;
505
- if (info.agentId === "pi")
506
- return `${config.piCliPath ?? "pi"} auth login`;
507
- if (info.agentId === "openclaw")
508
- return `${config.openClawCliPath ?? "openclaw"} login`;
509
- return "codex login --device-auth";
252
+ return hostAgentLoginCommand(config, info);
510
253
  };
511
254
  const hostLogoutCommand = (info) => {
512
- if (info.agentId === "hermes")
513
- return `${config.hermesCliPath ?? "hermes"} logout`;
514
- if (info.agentId === "claude-code")
515
- return `${config.claudeCodeCliPath ?? "claude"} auth logout`;
516
- if (info.agentId === "pi")
517
- return `${config.piCliPath ?? "pi"} auth logout`;
518
- if (info.agentId === "openclaw")
519
- return `${config.openClawCliPath ?? "openclaw"} logout`;
520
- return "codex logout";
255
+ return hostAgentLogoutCommand(config, info);
521
256
  };
522
257
  const denyIfLocked = async (request) => {
523
258
  const lock = lockStore.get(request.contextKey);
@@ -745,57 +480,73 @@ export function createDiscordBridge(config, registry) {
745
480
  }
746
481
  };
747
482
  const deliverCliGeneratedArtifacts = async (contextKey, context, session, startedAt, turnId) => {
748
- if (!startedAt || !turnId) {
749
- return;
750
- }
751
- const state = externalMirrors.get(contextKey);
752
- if (state?.artifactsDeliveredForTurnId === turnId) {
753
- return;
754
- }
755
- const workspace = session.getInfo().workspace;
756
- const report = await collectRecentWorkspaceArtifacts(workspace, {
757
- since: startedAt,
758
- until: new Date(),
759
- maxFileSize: config.maxFileSize,
760
- limit: 5,
761
- ignoreDirs: config.artifactIgnoreDirs,
762
- ignoreGlobs: config.artifactIgnoreGlobs,
763
- });
764
- if (report.artifacts.length === 0 && report.skippedCount === 0 && !report.omittedCount) {
765
- if (state)
766
- state.artifactsDeliveredForTurnId = turnId;
767
- return;
768
- }
769
- const persisted = await persistWorkspaceArtifactReport(workspace, turnId, report).catch((error) => {
770
- console.error("Failed to persist Discord CLI artifact report:", error);
771
- return null;
772
- });
773
- const summary = formatArtifactSummary(report.artifacts, report.skippedCount, report.omittedCount);
774
- if (summary) {
775
- await runtime.sendMessage(context, { text: summary, fallbackText: summary });
776
- }
777
- if (config.discordAutoSendArtifacts) {
778
- for (const artifact of (persisted?.artifacts ?? report.artifacts).slice(0, 5)) {
779
- await runtime.sendFile(context, { localPath: artifact.localPath, name: artifact.name }).catch((error) => {
780
- console.error(`Failed to send Discord CLI artifact ${artifact.name}:`, error);
781
- });
782
- }
783
- }
784
- const info = session.getInfo();
785
- activityStore.append({
786
- source: "cli",
787
- status: "info",
788
- type: config.discordAutoSendArtifacts ? "artifacts_sent" : "artifacts_detected",
483
+ await deliverChannelCliArtifacts({
484
+ config,
789
485
  contextKey,
790
- threadId: info.threadId,
791
- workspace: info.workspace,
792
- agentId: info.agentId,
793
- actor: { channel: "cli", label: `${info.agentLabel} CLI` },
794
- detail: summary,
486
+ session,
487
+ startedAt,
488
+ turnId,
489
+ state: externalMirrors.get(contextKey),
490
+ autoSend: config.discordAutoSendArtifacts,
491
+ sendSummaryWhenAutoSendDisabled: true,
492
+ logPrefix: "Discord",
493
+ sendSummary: (summary) => runtime.sendMessage(context, { text: summary, fallbackText: summary }).then(() => { }),
494
+ sendArtifact: (artifact) => runtime.sendFile(context, { localPath: artifact.localPath, name: artifact.name }).then(() => { }).catch((error) => {
495
+ console.error(`Failed to send Discord CLI artifact ${artifact.name}:`, error);
496
+ }),
497
+ appendActivity: (input) => {
498
+ activityStore.append(input);
499
+ },
795
500
  });
796
- if (state)
797
- state.artifactsDeliveredForTurnId = turnId;
798
501
  };
502
+ const externalMirrorController = createChannelExternalMirrorController({
503
+ config,
504
+ states: externalMirrors,
505
+ typingIntervalMs: TYPING_INTERVAL_MS,
506
+ minUpdateMs: () => config.discordMirrorMinUpdateMs,
507
+ mirrorMode: (contextKey) => preferencesStore.get(contextKey).mirrorMode ?? config.discordMirrorMode,
508
+ queueLength: (contextKey) => promptStore.list(contextKey).length,
509
+ activityActor: (snapshot) => ({ channel: "cli", label: `${snapshot.agentLabel} CLI` }),
510
+ appendActivity: (input) => {
511
+ activityStore.append(input);
512
+ },
513
+ sendTyping: (_contextKey, context) => runtime.sendTyping(context).catch(() => { }),
514
+ sendWorkingNotice: async (_contextKey, context, state, snapshot, prompt) => {
515
+ const turnKey = snapshot.activity.turnId ?? snapshot.activity.startedAt?.toISOString() ?? "unknown";
516
+ if (state.workingNoticeTurnKey === turnKey) {
517
+ return;
518
+ }
519
+ const text = prompt ? `**Working on** ${prompt}` : `**Working on** external ${snapshot.agentLabel} task...`;
520
+ await runtime.sendMessage(context, {
521
+ text,
522
+ fallbackText: prompt ? `Working on ${prompt}` : `Working on external ${snapshot.agentLabel} task...`,
523
+ });
524
+ state.workingNoticeTurnKey = turnKey;
525
+ },
526
+ sendStatus: async (_contextKey, context, _state, rendered) => {
527
+ const sent = await runtime.sendMessage(context, { text: rendered.html, fallbackText: rendered.plain, parseMode: "html" });
528
+ return sent.messageId;
529
+ },
530
+ editStatus: (_contextKey, context, _state, messageId, rendered) => runtime.editMessage(context, messageId, { text: rendered.html, fallbackText: rendered.plain, parseMode: "html" }),
531
+ sendEvent: (_contextKey, context, _state, rendered) => deliverChannelAction(runtime, context, rendered).then(() => { }),
532
+ sendDone: (_contextKey, context, state, text) => {
533
+ if (state.statusMessageId) {
534
+ return runtime.editMessage(context, state.statusMessageId, { text, fallbackText: text });
535
+ }
536
+ return runtime.sendMessage(context, { text, fallbackText: text }).then(() => { });
537
+ },
538
+ sendFinalAnswer: async (_contextKey, context, _state, snapshot, text) => {
539
+ await runtime.sendMessage(context, {
540
+ text: `**${snapshot.agentLabel} CLI final answer:**`,
541
+ fallbackText: `${snapshot.agentLabel} CLI final answer:`,
542
+ });
543
+ for (const chunk of splitDiscordMessage(text)) {
544
+ await runtime.sendMessage(context, { text: chunk, fallbackText: chunk });
545
+ }
546
+ },
547
+ deliverArtifacts: (contextKey, context, session, state, turnId) => deliverCliGeneratedArtifacts(contextKey, context, session, state.startedAt, turnId),
548
+ });
549
+ const mirrorExternalSnapshot = externalMirrorController.mirror;
799
550
  const commandDispatcher = createSharedChannelCommandDispatcher({
800
551
  transport: "discord",
801
552
  bindings: [
@@ -1644,47 +1395,36 @@ export function createDiscordBridge(config, registry) {
1644
1395
  return id;
1645
1396
  };
1646
1397
  const monitorExternalContexts = async () => {
1647
- const keys = new Set([
1648
- ...registry.listContexts().map((context) => context.contextKey),
1649
- ...promptStore.listContextKeys(),
1650
- ].filter(isDiscordContextKey));
1651
- for (const contextKey of keys) {
1652
- const parsed = parseDiscordContextKey(contextKey);
1653
- if (!parsed)
1654
- continue;
1655
- if (!canSendSystemMessagesToDiscordContext(userStore, contextKey)) {
1656
- continue;
1657
- }
1658
- const guildId = parsed.guildId?.startsWith("dm-") ? undefined : parsed.guildId;
1659
- if (!isDiscordGuildAllowed(guildId) || !isDiscordChannelAllowedByEnv(parsed.channelId)) {
1660
- continue;
1661
- }
1662
- const session = await registry.getOrCreate(contextKey, { deferThreadStart: true }).catch(() => null);
1663
- if (!session)
1664
- continue;
1665
- const context = {
1666
- channelId: "discord",
1667
- chatId: parsed.threadId ?? parsed.channelId,
1668
- topicId: parsed.threadId,
1669
- };
1670
- const snapshot = getExternalSnapshotForSession(session, config, { maxEvents: 1 });
1671
- const previous = externalMirrors.get(contextKey);
1672
- const mirrorSnapshot = snapshot
1673
- ? getExternalSnapshotForSession(session, config, {
1674
- afterLine: previous?.lastLine ?? Number.MAX_SAFE_INTEGER,
1675
- }) ?? snapshot
1676
- : null;
1677
- if (mirrorSnapshot && !session.isProcessing()) {
1678
- await mirrorExternalSnapshot(contextKey, context, session, mirrorSnapshot);
1679
- }
1680
- if (mirrorSnapshot?.activity.active) {
1681
- if (promptStore.list(contextKey).length > 0) {
1682
- await updateQueueStatusMessage(contextKey, context, `Waiting for ${mirrorSnapshot.agentLabel} CLI task... ${promptStore.list(contextKey).length} queued${promptStore.isPaused(contextKey) ? " (paused)" : ""}.`).catch(() => { });
1683
- }
1684
- continue;
1685
- }
1686
- if (promptStore.list(contextKey).length > 0 && !promptStore.isPaused(contextKey) && !session.isProcessing()) {
1687
- await updateQueueStatusMessage(contextKey, context, `CLI task finished, running queued prompt 1/${promptStore.list(contextKey).length}.`).catch(() => { });
1398
+ await monitorChannelExternalContexts({
1399
+ config,
1400
+ registry,
1401
+ promptStore,
1402
+ isContextKey: isDiscordContextKey,
1403
+ canSendSystemMessages: (contextKey) => canSendSystemMessagesToDiscordContext(userStore, contextKey),
1404
+ isAllowed: (contextKey) => {
1405
+ const parsed = parseDiscordContextKey(contextKey);
1406
+ if (!parsed)
1407
+ return false;
1408
+ const guildId = parsed.guildId?.startsWith("dm-") ? undefined : parsed.guildId;
1409
+ return isDiscordGuildAllowed(guildId) && isDiscordChannelAllowedByEnv(parsed.channelId);
1410
+ },
1411
+ contextForKey: (contextKey) => {
1412
+ const parsed = parseDiscordContextKey(contextKey);
1413
+ if (!parsed)
1414
+ return null;
1415
+ return {
1416
+ channelId: "discord",
1417
+ chatId: parsed.threadId ?? parsed.channelId,
1418
+ ...(parsed.threadId ? { topicId: parsed.threadId } : {}),
1419
+ };
1420
+ },
1421
+ previousLastLine: (contextKey) => externalMirrors.get(contextKey)?.lastLine,
1422
+ mirrorSnapshot: mirrorExternalSnapshot,
1423
+ updateQueueStatus: updateQueueStatusMessage,
1424
+ drainQueue: async (contextKey, context) => {
1425
+ const parsed = parseDiscordContextKey(contextKey);
1426
+ if (!parsed)
1427
+ return;
1688
1428
  const systemRequest = {
1689
1429
  contextKey,
1690
1430
  context,
@@ -1695,8 +1435,8 @@ export function createDiscordBridge(config, registry) {
1695
1435
  source: "message",
1696
1436
  };
1697
1437
  await drainQueue(systemRequest);
1698
- }
1699
- }
1438
+ },
1439
+ });
1700
1440
  };
1701
1441
  const registerSlashCommands = async () => {
1702
1442
  if (!config.discordClientId || !config.discordAutoRegisterCommands || config.discordCommandMode === "message" || !config.discordBotToken) {