mixdog 0.9.96 → 0.9.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +0 -17
  2. package/package.json +16 -8
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/agent/30-explorer.md +43 -32
  9. package/src/rules/lead/01-general.md +1 -2
  10. package/src/rules/lead/lead-tool.md +0 -3
  11. package/src/rules/shared/01-tool.md +11 -10
  12. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  13. package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
  14. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  15. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
  17. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
  18. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  19. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
  20. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  21. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  22. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  24. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  25. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  26. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +42 -26
  27. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  28. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
  29. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  30. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
  31. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  32. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
  33. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  34. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  35. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  36. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  37. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  38. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  39. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  41. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
  42. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  43. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  44. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
  45. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  46. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  47. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  48. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  49. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  50. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  51. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  52. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -105
  55. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  58. package/src/runtime/attachments/pdf-extract.mjs +83 -0
  59. package/src/runtime/attachments/store.mjs +515 -0
  60. package/src/runtime/attachments/store.test.mjs +188 -0
  61. package/src/runtime/channels/lib/config.mjs +33 -33
  62. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  63. package/src/runtime/channels/lib/interaction-handlers.mjs +4 -194
  64. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  65. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  66. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  67. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  68. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  69. package/src/runtime/channels/lib/runtime-paths.mjs +5 -34
  70. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  72. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  73. package/src/runtime/channels/lib/worker-ipc.mjs +4 -70
  74. package/src/runtime/channels/lib/worker-main.mjs +39 -58
  75. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  76. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  77. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  78. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  79. package/src/runtime/channels/tool-defs.mjs +1 -1
  80. package/src/runtime/memory/index.mjs +72 -211
  81. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  82. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  83. package/src/runtime/memory/lib/http-router.mjs +1 -6
  84. package/src/runtime/memory/lib/http-wire.mjs +39 -4
  85. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  86. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  87. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  88. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  89. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  90. package/src/runtime/memory/lib/memory.mjs +1 -1
  91. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  92. package/src/runtime/search/index.mjs +2 -2
  93. package/src/runtime/shared/abort-race.mjs +70 -0
  94. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  95. package/src/runtime/shared/config.mjs +11 -24
  96. package/src/runtime/shared/json-metrics.mjs +94 -0
  97. package/src/runtime/shared/llm/index.mjs +1 -1
  98. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  99. package/src/runtime/shared/resource-admission.mjs +6 -5
  100. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  101. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  102. package/src/runtime/shared/tool-surface.mjs +91 -22
  103. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  104. package/src/runtime/shared/turn-snapshot.mjs +30 -2
  105. package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
  106. package/src/session-runtime/channel-config-api.mjs +5 -5
  107. package/src/session-runtime/config-lifecycle.mjs +27 -27
  108. package/src/session-runtime/lifecycle-api.mjs +22 -3
  109. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  110. package/src/session-runtime/prewarm.mjs +1 -1
  111. package/src/session-runtime/provider-models.mjs +12 -2
  112. package/src/session-runtime/quick-model-rows.mjs +38 -7
  113. package/src/session-runtime/quick-search-models.mjs +15 -17
  114. package/src/session-runtime/remote-control.mjs +6 -7
  115. package/src/session-runtime/runtime-core.mjs +61 -32
  116. package/src/session-runtime/runtime-tunables.mjs +9 -6
  117. package/src/session-runtime/self-update.mjs +1 -1
  118. package/src/session-runtime/session-lifecycle.mjs +36 -15
  119. package/src/session-runtime/session-title.mjs +28 -4
  120. package/src/session-runtime/session-turn-api.mjs +55 -15
  121. package/src/session-runtime/settings-api.mjs +7 -7
  122. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  123. package/src/standalone/channel-admin.mjs +42 -53
  124. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  125. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +73 -58
  126. package/src/standalone/channel-worker.mjs +124 -647
  127. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -132
  128. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  129. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  130. package/src/standalone/session-protocol.mjs +9 -4
  131. package/src/standalone/session-runtime-pool.mjs +393 -0
  132. package/src/standalone/session-runtime-record.mjs +20 -0
  133. package/src/standalone/session-runtime-worker.mjs +203 -0
  134. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +192 -157
  135. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
  136. package/src/standalone/session-wire.mjs +55 -0
  137. package/src/tui/App.jsx +4 -4
  138. package/src/tui/app/app-format.mjs +1 -1
  139. package/src/tui/app/channel-pickers.mjs +41 -41
  140. package/src/tui/app/input-parsers.mjs +2 -2
  141. package/src/tui/app/onboarding-steps.mjs +2 -2
  142. package/src/tui/app/project-picker.mjs +8 -8
  143. package/src/tui/app/prompt-submit.mjs +15 -17
  144. package/src/tui/app/settings-picker.mjs +25 -25
  145. package/src/tui/app/use-prompt-handlers.mjs +7 -3
  146. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  147. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  148. package/src/tui/components/ToolExecution.jsx +1 -1
  149. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  150. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  151. package/src/tui/dist/index.mjs +403 -170
  152. package/src/tui/hooks/useSession.mjs +16 -0
  153. package/src/tui/index.jsx +11 -11
  154. package/src/tui/paste-attachments.mjs +13 -7
  155. package/src/tui/prompt-history-store.mjs +52 -4
  156. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  157. package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
  158. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  159. package/src/tui/{engine → session}/context-state.mjs +3 -3
  160. package/src/tui/{engine → session}/labels.mjs +2 -2
  161. package/src/tui/{engine → session}/live-share.mjs +28 -14
  162. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  163. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  164. package/src/tui/session/oauth-flows.mjs +140 -0
  165. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  166. package/src/tui/{engine → session}/queue-helpers.mjs +8 -8
  167. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  168. package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
  169. package/src/tui/{engine → session}/session-api.mjs +17 -61
  170. package/src/tui/{engine → session}/session-flow.mjs +5 -3
  171. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  172. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  173. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  174. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  175. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  176. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  177. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  178. package/src/tui/{engine → session}/turn.mjs +29 -107
  179. package/src/tui/{engine-local-session.mjs → session-local.mjs} +72 -78
  180. package/src/tui/session.mjs +22 -0
  181. package/src/ui/statusline-segments.mjs +1 -1
  182. package/src/ui/statusline.mjs +1 -1
  183. package/vendor/ink/build/components/App.js +10 -1
  184. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  185. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  186. package/src/tui/engine/turn-watchdog.mjs +0 -92
  187. package/src/tui/engine.mjs +0 -22
  188. package/src/tui/hooks/useEngine.mjs +0 -16
  189. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  190. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  191. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  192. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  193. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, mkdirSync } from "fs";
2
2
  import { join } from "path";
3
- import { DiscordBackend } from "../backends/discord.mjs";
4
- import { TelegramBackend } from "../backends/telegram.mjs";
3
+ import { DiscordProvider } from "../providers/discord.mjs";
4
+ import { TelegramProvider } from "../providers/telegram.mjs";
5
5
  import {
6
6
  readSection,
7
7
  updateSection,
@@ -22,7 +22,7 @@ const DEFAULT_ACCESS = {
22
22
  channels: {}
23
23
  };
24
24
  const DEFAULT_CONFIG = {
25
- backend: "discord",
25
+ provider: "discord",
26
26
  discord: { token: "" },
27
27
  telegram: { token: "" },
28
28
  access: DEFAULT_ACCESS,
@@ -40,18 +40,18 @@ function applyDefaults(config) {
40
40
  return out;
41
41
  }
42
42
 
43
- function channelIdForBackend(entry = {}, backend = "discord") {
44
- if (backend === "telegram") {
45
- return String(entry?.telegramChatId || (entry?.discordChannelId ? "" : entry?.channelId) || "");
43
+ function channelIdForProvider(entry = {}, provider = "discord") {
44
+ if (provider === "telegram") {
45
+ return String(entry?.telegramChatId || "");
46
46
  }
47
- return String(entry?.discordChannelId || (entry?.telegramChatId ? "" : entry?.channelId) || "");
47
+ return String(entry?.discordChannelId || "");
48
48
  }
49
49
 
50
- // Resolve the single active-backend channel id from the config's `channel`
50
+ // Resolve the single active-provider channel id from the config's `channel`
51
51
  // section. Disk config carries a single `channel` object only.
52
- function resolveChannelId(raw = {}, backend = "discord") {
52
+ function resolveChannelId(raw = {}, provider = "discord") {
53
53
  const channel = raw.channel && typeof raw.channel === "object" ? raw.channel : null;
54
- if (channel) return channelIdForBackend(channel, backend);
54
+ if (channel) return channelIdForProvider(channel, provider);
55
55
  return "";
56
56
  }
57
57
 
@@ -88,20 +88,20 @@ async function loadConfig({ freshSecrets = false } = {}) {
88
88
  if (discordTokenProblem) {
89
89
  process.stderr.write(`mixdog: discord token ignored: ${discordTokenProblem}\n`);
90
90
  }
91
- // Single-backend select: config.backend picks ONE of discord|telegram.
91
+ // Single-provider select: config.provider picks ONE of discord|telegram.
92
92
  // Anything else falls back to the discord default.
93
- const backend = raw.backend === "telegram" ? "telegram" : "discord";
93
+ const provider = raw.provider === "telegram" ? "telegram" : "discord";
94
94
  const telegramToken = getTelegramToken();
95
- const channelId = resolveChannelId(raw, backend);
95
+ const channelId = resolveChannelId(raw, provider);
96
96
  // The runtime reads only the resolved `channelId`; disk carries a single
97
97
  // `channel` object, so spread `raw` directly.
98
98
  return applyDefaults({
99
99
  ...DEFAULT_CONFIG,
100
100
  ...raw,
101
- backend,
101
+ provider,
102
102
  channelId,
103
103
  discord: { ...DEFAULT_CONFIG.discord, ...(({ token: _, ...rest }) => rest)(raw.discord || {}), ...(discordToken && !discordTokenProblem ? { token: discordToken } : {}) },
104
- // Merge the keychain-resolved telegram token (harmless when backend is
104
+ // Merge the keychain-resolved telegram token (harmless when provider is
105
105
  // discord; the secret never lands in the on-disk config either way).
106
106
  telegram: { ...DEFAULT_CONFIG.telegram, ...(({ token: _t, ...rest }) => rest)(raw.telegram || {}), ...(telegramToken ? { token: telegramToken } : {}) },
107
107
  access: {
@@ -117,7 +117,7 @@ async function loadConfig({ freshSecrets = false } = {}) {
117
117
  updateSection("channels", () => DEFAULT_CONFIG);
118
118
  process.stderr.write(
119
119
  `mixdog: default channels config created in ${MIXDOG_CONFIG_PATH}
120
- edit the active backend channel id to connect.
120
+ edit the active provider channel id to connect.
121
121
  `
122
122
  );
123
123
  return applyDefaults(DEFAULT_CONFIG);
@@ -125,7 +125,7 @@ async function loadConfig({ freshSecrets = false } = {}) {
125
125
  throw err;
126
126
  }
127
127
  }
128
- const HEADLESS_BACKEND = {
128
+ const HEADLESS_PROVIDER = {
129
129
  name: "headless",
130
130
  MAX_MESSAGE_LENGTH: 2000,
131
131
  formatOutgoing(t) {
@@ -156,31 +156,31 @@ const HEADLESS_BACKEND = {
156
156
  on() {
157
157
  }
158
158
  };
159
- function createBackend(config) {
159
+ function createProvider(config) {
160
160
  // Channels-module toggle is MESSAGING-ONLY: automation (scheduler/webhooks)
161
- // keeps the worker alive, so a disabled module runs the headless backend
161
+ // keeps the worker alive, so a disabled module runs the headless provider
162
162
  // instead of blocking the whole runtime.
163
163
  if (config.enabled === false) {
164
164
  process.stderr.write("mixdog: channels messaging disabled; channel runtime running in headless mode\n");
165
- return HEADLESS_BACKEND;
165
+ return HEADLESS_PROVIDER;
166
166
  }
167
- // Single-backend select: exactly one backend is constructed based on
168
- // config.backend (discord|telegram). The two are mutually exclusive.
169
- if (config.backend === "telegram") {
167
+ // Single-provider select: exactly one provider is constructed based on
168
+ // config.provider (discord|telegram). The two are mutually exclusive.
169
+ if (config.provider === "telegram") {
170
170
  const telegramToken = getTelegramToken();
171
171
  if (!telegramToken) {
172
172
  process.stderr.write("mixdog: telegram bot not configured; channel runtime running in headless mode\n");
173
- return HEADLESS_BACKEND;
173
+ return HEADLESS_PROVIDER;
174
174
  }
175
175
  const tgStateDir = config.telegram?.stateDir ?? join(DATA_DIR, "telegram");
176
176
  mkdirSync(tgStateDir, { recursive: true });
177
- return new TelegramBackend({
177
+ return new TelegramProvider({
178
178
  ...config.telegram,
179
179
  configPath: CONFIG_FILE,
180
180
  access: config.access,
181
181
  // Single-source channel setup: the main chat is auto-allowed inside
182
- // TelegramBackend.loadAccess() so a configured channel.channelId
183
- // is enough for both inbound gating and outbound.
182
+ // TelegramProvider.loadAccess() so the configured Telegram target is
183
+ // enough for both inbound gating and outbound.
184
184
  mainChannelId: config.channelId
185
185
  }, tgStateDir);
186
186
  }
@@ -189,19 +189,19 @@ function createBackend(config) {
189
189
  if (discordTokenProblem) {
190
190
  process.stderr.write(`mixdog: discord token ignored: ${discordTokenProblem}\n`);
191
191
  }
192
- if (config.backend !== "discord" || !discordToken || discordTokenProblem) {
192
+ if (config.provider !== "discord" || !discordToken || discordTokenProblem) {
193
193
  process.stderr.write("mixdog: discord bot not configured; channel runtime running in headless mode\n");
194
- return HEADLESS_BACKEND;
194
+ return HEADLESS_PROVIDER;
195
195
  }
196
196
  const stateDir = config.discord.stateDir ?? join(DATA_DIR, "discord");
197
197
  mkdirSync(stateDir, { recursive: true });
198
- return new DiscordBackend({
198
+ return new DiscordProvider({
199
199
  ...config.discord,
200
200
  configPath: CONFIG_FILE,
201
201
  access: config.access,
202
202
  // Single-source channel setup: the main channel is auto-allowed inside
203
- // DiscordBackend.loadAccess() so a configured channel.channelId
204
- // is enough for both inbound and outbound — no separate access.channels entry.
203
+ // DiscordProvider.loadAccess() so the configured Discord target is enough
204
+ // for both inbound and outbound — no separate access.channels entry.
205
205
  mainChannelId: config.channelId
206
206
  }, stateDir);
207
207
  }
@@ -215,7 +215,7 @@ function loadProfileConfig() {
215
215
  }
216
216
  export {
217
217
  DATA_DIR,
218
- createBackend,
218
+ createProvider,
219
219
  getDiscordToken,
220
220
  getTelegramToken,
221
221
  loadConfig,
@@ -7,14 +7,14 @@ import {
7
7
  import { refreshActiveInstance } from "./runtime-paths.mjs";
8
8
  import { isNetworkError, retryOnNetwork } from "./network-retry.mjs";
9
9
  // Inbound message pipeline extracted from channels/index.mjs (behavior-
10
- // preserving): serial inbound queue, backend.onMessage transcript (re)bind +
10
+ // preserving): serial inbound queue, provider.onMessage transcript (re)bind +
11
11
  // steal logic, and handleInbound voice-transcription + parent notify. Bound to
12
12
  // live runtime getters and shared primitives.
13
13
  function isImageAttachment(contentType) {
14
14
  return typeof contentType === "string" && contentType.toLowerCase().startsWith("image/");
15
15
  }
16
16
  export function createInboundHandler({
17
- getBackend,
17
+ getProvider,
18
18
  getConfig,
19
19
  getBridgeRuntimeConnected,
20
20
  getChannelBridgeActive,
@@ -55,7 +55,7 @@ const inboundQueue = (() => {
55
55
  };
56
56
  })();
57
57
 
58
- getBackend().onMessage = (msg) => {
58
+ getProvider().onMessage = (msg) => {
59
59
  const receivedAtMs = Number.isFinite(msg.receivedAtMs) ? msg.receivedAtMs : Date.now();
60
60
  const onMessageAtMs = Date.now();
61
61
  const gateBlocked = (!getBridgeRuntimeConnected() || !getBridgeOwnershipSnapshot().owned)
@@ -72,7 +72,7 @@ getBackend().onMessage = (msg) => {
72
72
  msg.__bridgeGateRetries = attempt;
73
73
  msg.receivedAtMs = receivedAtMs;
74
74
  setTimeout(() => {
75
- try { getBackend().onMessage?.(msg); } catch { /* best-effort */ }
75
+ try { getProvider().onMessage?.(msg); } catch { /* best-effort */ }
76
76
  }, 2000 * attempt)?.unref?.();
77
77
  } else {
78
78
  try { process.stderr.write(`mixdog: inbound message dropped after ${attempt - 1} ${gateBlocked} retries (channel=${msg.chatId}, id=${msg.messageId})\n`); } catch {}
@@ -242,7 +242,7 @@ getBackend().onMessage = (msg) => {
242
242
  });
243
243
  void (async () => {
244
244
  try {
245
- await getBackend().react(msg.chatId, msg.messageId, "\u{1F914}");
245
+ await getProvider().react(msg.chatId, msg.messageId, "\u{1F914}");
246
246
  } catch {
247
247
  }
248
248
  await bindingDone;
@@ -271,7 +271,7 @@ async function handleInbound(msg, route, options = {}) {
271
271
  const files = await retryOnNetwork(
272
272
  // Short per-attempt timeout (vs the 180s default) bounds worst-case
273
273
  // blockage of the serial inboundQueue on a bad voice message.
274
- () => getBackend().downloadAttachment(msg.chatId, msg.messageId, { timeoutMs: 20_000 }),
274
+ () => getProvider().downloadAttachment(msg.chatId, msg.messageId, { timeoutMs: 20_000 }),
275
275
  { label: "voice.download" }
276
276
  );
277
277
  // concurrency handled inside transcribeVoice queue; loop is sequential so last att wins
@@ -312,7 +312,7 @@ async function handleInbound(msg, route, options = {}) {
312
312
  if (imageAtts.length > 0) {
313
313
  try {
314
314
  const files = (await retryOnNetwork(
315
- () => getBackend().downloadAttachment(msg.chatId, msg.messageId, {
315
+ () => getProvider().downloadAttachment(msg.chatId, msg.messageId, {
316
316
  timeoutMs: 20_000,
317
317
  filter: (a) => isImageAttachment(a.contentType),
318
318
  }),
@@ -1,12 +1,6 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- // Discord permission-prompt + interaction/modal routing extracted from
4
- // channels/index.mjs (behavior-preserving). Installs backend.onInteraction /
5
- // onModalRequest handlers and the terminal tool-exec signal watcher; returns the
6
- // pending-permission map + marker refresher for the worker IPC handler.
1
+ // Discord interaction/modal routing extracted from channels/index.mjs.
7
2
  export function createInteractionHandlers({
8
- getBackend,
9
- getConfig,
3
+ getProvider,
10
4
  getBridgeRuntimeConnected,
11
5
  instanceId,
12
6
  getBridgeOwnershipSnapshot,
@@ -14,34 +8,13 @@ export function createInteractionHandlers({
14
8
  pendingSetup,
15
9
  buildModalRequestSpec,
16
10
  loadProfileConfig,
17
- getDiscordToken,
18
11
  sendNotifyToParent,
19
12
  scheduler,
20
13
  controlClaudeSession,
21
14
  writeTextFile,
22
15
  TURN_END_FILE,
23
- getPermissionResultPath,
24
- TERMINAL_LEAD_PID,
25
- localTimestamp,
26
- isMixdogDebug,
27
- appendSessionStartCriticalLog,
28
- DATA_DIR,
29
- _bootLog,
30
- RUNTIME_ROOT,
31
16
  }) {
32
- function editDiscordMessage(channelId, messageId, label) {
33
- // Behavior-preserving: route through the getBackend() abstraction (which uses
34
- // discord.js under the hood) instead of issuing a raw REST PATCH. Errors
35
- // are swallowed to stderr to match the prior fire-and-forget shape — the
36
- // call site never awaited the HTTPS request either.
37
- if (!getDiscordToken()) return;
38
- const text = `\u{1F510} **Permission Request** \u2014 ${label}`;
39
- void getBackend().editMessage(channelId, messageId, text, { components: [] }).catch((err) => {
40
- process.stderr.write(`mixdog: editDiscordMessage failed: ${err}
41
- `);
42
- });
43
- }
44
- getBackend().onModalRequest = async (rawInteraction) => {
17
+ getProvider().onModalRequest = async (rawInteraction) => {
45
18
  if (!getBridgeRuntimeConnected() || !getBridgeOwnershipSnapshot().owned) {
46
19
  refreshBridgeOwnershipSafe();
47
20
  return;
@@ -67,170 +40,8 @@ getBackend().onModalRequest = async (rawInteraction) => {
67
40
  modal.addComponents(...rows);
68
41
  await rawInteraction.showModal(modal);
69
42
  };
70
- const pendingPermRequests = new Map();
71
- const TOOL_EXEC_CONSUMER_MARKER = path.join(RUNTIME_ROOT, '.tool-exec-consumer');
72
- function refreshToolExecConsumerMarker() {
73
- try {
74
- if (pendingPermRequests.size > 0) {
75
- fs.writeFileSync(TOOL_EXEC_CONSUMER_MARKER, String(Date.now()));
76
- } else {
77
- try { fs.unlinkSync(TOOL_EXEC_CONSUMER_MARKER); } catch {}
78
- }
79
- } catch {}
80
- }
81
- // Watch for terminal-approved tool executions. The PostToolUse hook writes a
82
- // signal file per tool call; when we see one, find the oldest pending perm
83
- // request with a matching tool name and mark its Discord message as
84
- // "Allowed (terminal)" so users don't see stale active buttons.
85
- try {
86
- try { if (!fs.existsSync(RUNTIME_ROOT)) fs.mkdirSync(RUNTIME_ROOT, { recursive: true }); } catch {}
87
- const SIGNAL_RE = /^tool-exec-\d+-[0-9a-f]+\.signal$/;
88
- fs.watch(RUNTIME_ROOT, { persistent: false }, (eventType, filename) => {
89
- if (!filename || !SIGNAL_RE.test(filename)) return;
90
- setTimeout(() => {
91
- try {
92
- const signalPath = path.join(RUNTIME_ROOT, filename);
93
- let raw;
94
- try { raw = fs.readFileSync(signalPath, 'utf8'); } catch { return; }
95
- let payload;
96
- try { payload = JSON.parse(raw); } catch { return; }
97
- const toolName = payload?.toolName;
98
- if (!toolName) return;
99
- const sigFilePath = payload?.filePath || '';
100
- let oldestKey = null;
101
- let oldestEntry = null;
102
- for (const [k, v] of pendingPermRequests) {
103
- if (v.toolName !== toolName) continue;
104
- // Bind on filePath too. If both sides are empty (non-file tools
105
- // like Bash), toolName alone is the match. Otherwise both must
106
- // equal — prevents two concurrent Edit/Write requests from
107
- // cross-approving each other.
108
- const vFilePath = v.filePath || '';
109
- if (vFilePath || sigFilePath) {
110
- if (vFilePath !== sigFilePath) continue;
111
- }
112
- if (!oldestEntry || v.createdAt < oldestEntry.createdAt) {
113
- oldestKey = k;
114
- oldestEntry = v;
115
- }
116
- }
117
- // No matching pending request — leave the signal on disk so a
118
- // agent role hook (or other consumer) gets a chance to claim it.
119
- if (!oldestKey || !oldestEntry) return;
120
- if (oldestEntry.channelId && oldestEntry.messageId) {
121
- try {
122
- editDiscordMessage(oldestEntry.channelId, oldestEntry.messageId, 'Allowed (terminal)');
123
- } catch (err) {
124
- try { process.stderr.write(`mixdog channels: tool-exec signal edit failed: ${err && err.message || err}\n`); } catch {}
125
- }
126
- }
127
- pendingPermRequests.delete(oldestKey);
128
- refreshToolExecConsumerMarker();
129
- // Only unlink once we've confirmed the match and handled it.
130
- try { fs.unlinkSync(signalPath); } catch {}
131
- } catch (err) {
132
- try { process.stderr.write(`mixdog channels: tool-exec signal handler error: ${err && err.message || err}\n`); } catch {}
133
- }
134
- }, 50);
135
- });
136
- // Stale-signal sweeper: any signal file older than 60s is removed so
137
- // unclaimed files don't accumulate on disk. Runs every 30s.
138
- setInterval(() => {
139
- try {
140
- const now = Date.now();
141
- const entries = fs.readdirSync(RUNTIME_ROOT);
142
- for (const name of entries) {
143
- if (!SIGNAL_RE.test(name)) continue;
144
- const p = path.join(RUNTIME_ROOT, name);
145
- try {
146
- const st = fs.statSync(p);
147
- if (now - st.mtimeMs > 60_000) {
148
- try { fs.unlinkSync(p); } catch {}
149
- }
150
- } catch {}
151
- }
152
- } catch {}
153
- }, 30_000)?.unref?.();
154
- } catch (err) {
155
- try { process.stderr.write(`mixdog channels: tool-exec signal watcher setup failed: ${err && err.message || err}\n`); } catch {}
156
- }
157
43
 
158
- getBackend().onInteraction = (interaction) => {
159
- // Channel-route permission reply. Custom_id format: perm-ch-{request_id}-{allow|session|deny}.
160
- // request_id is the 5-letter short ID CC generates via shortRequestId().
161
- // Emit notifications/claude/channel/permission back to the MCP host; the race
162
- // logic in interactiveHandler.ts resolves the pending request and dismisses
163
- // every other racer (local dialog, bridge, hook, classifier).
164
- if (interaction.customId?.startsWith("perm-ch-")) {
165
- const match = interaction.customId.match(/^perm-ch-([a-km-z]{5})-(allow|session|deny)$/);
166
- if (!match) return;
167
- const [, requestId, action] = match;
168
- const access = getConfig().access;
169
- if (access?.allowFrom?.length > 0 && !access.allowFrom.includes(interaction.userId)) {
170
- process.stderr.write(`mixdog: perm-ch button rejected — user ${interaction.userId} not in allowFrom\n`);
171
- return;
172
- }
173
- const pending = pendingPermRequests.get(requestId);
174
- pendingPermRequests.delete(requestId);
175
- refreshToolExecConsumerMarker();
176
- const params = { request_id: requestId };
177
- if (action === 'deny') {
178
- params.behavior = 'deny';
179
- } else if (action === 'session') {
180
- params.behavior = 'allow';
181
- const toolName = pending?.toolName;
182
- if (toolName) {
183
- params.updatedPermissions = [{ type: 'addRules', rules: [{ toolName }], behavior: 'allow', destination: 'session' }];
184
- }
185
- } else {
186
- params.behavior = 'allow';
187
- }
188
- sendNotifyToParent('notifications/claude/channel/permission', params);
189
- const labels = { allow: 'Approved', session: 'Session Approved', deny: 'Denied' };
190
- if (interaction.message?.id && interaction.channelId) {
191
- editDiscordMessage(interaction.channelId, interaction.message.id, labels[action] || action);
192
- }
193
- return;
194
- }
195
- if (interaction.customId?.startsWith("perm-")) {
196
- const match = interaction.customId.match(/^perm-([0-9a-f]{32})-(allow|session|deny)$/);
197
- if (!match) return;
198
- const [, uuid, action] = match;
199
- const access = getConfig().access;
200
- if (!access) {
201
- const _permDropLine = `[${localTimestamp()}] perm interaction dropped: no access getConfig()\n`;
202
- if (isMixdogDebug()) {
203
- fs.appendFileSync(_bootLog, _permDropLine);
204
- } else {
205
- appendSessionStartCriticalLog(DATA_DIR, `[channels] ${_permDropLine}`);
206
- }
207
- return;
208
- }
209
- if (access.allowFrom?.length > 0 && !access.allowFrom.includes(interaction.userId)) {
210
- process.stderr.write(`mixdog: perm button rejected \u2014 user ${interaction.userId} not in allowFrom
211
- `);
212
- return;
213
- }
214
- const resultPaths = [getPermissionResultPath(instanceId, uuid)];
215
- const leadInstanceId = String(TERMINAL_LEAD_PID);
216
- if (leadInstanceId && leadInstanceId !== instanceId) {
217
- resultPaths.push(getPermissionResultPath(leadInstanceId, uuid));
218
- }
219
- for (const resultPath of resultPaths) {
220
- try {
221
- fs.writeFileSync(resultPath, action, { flag: "wx" });
222
- } catch (e) {
223
- if (e.code !== "EEXIST") {
224
- process.stderr.write(`mixdog: writePermissionResult failed: ${e.message}\n`);
225
- }
226
- }
227
- }
228
- const labels = { allow: "Approved", session: "Session Approved", deny: "Denied" };
229
- if (interaction.message?.id && interaction.channelId) {
230
- editDiscordMessage(interaction.channelId, interaction.message.id, labels[action] || action);
231
- }
232
- return;
233
- }
44
+ getProvider().onInteraction = (interaction) => {
234
45
  if (!getBridgeRuntimeConnected() || !getBridgeOwnershipSnapshot().owned) {
235
46
  refreshBridgeOwnershipSafe();
236
47
  return;
@@ -256,5 +67,4 @@ getBackend().onInteraction = (interaction) => {
256
67
  }
257
68
  });
258
69
  };
259
- return { pendingPermRequests, refreshToolExecConsumerMarker };
260
70
  }