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.
- package/README.md +0 -17
- package/package.json +16 -8
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +43 -32
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +42 -26
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -105
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/attachments/pdf-extract.mjs +83 -0
- package/src/runtime/attachments/store.mjs +515 -0
- package/src/runtime/attachments/store.test.mjs +188 -0
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- package/src/runtime/channels/lib/interaction-handlers.mjs +4 -194
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +5 -34
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +4 -70
- package/src/runtime/channels/lib/worker-main.mjs +39 -58
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/http-wire.mjs +39 -4
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- package/src/runtime/shared/json-metrics.mjs +94 -0
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +6 -5
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +30 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-models.mjs +12 -2
- package/src/session-runtime/quick-model-rows.mjs +38 -7
- package/src/session-runtime/quick-search-models.mjs +15 -17
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +61 -32
- package/src/session-runtime/runtime-tunables.mjs +9 -6
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +28 -4
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +73 -58
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +9 -4
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +192 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +15 -17
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-handlers.mjs +7 -3
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +403 -170
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/paste-attachments.mjs +13 -7
- package/src/tui/prompt-history-store.mjs +52 -4
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +28 -14
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +8 -8
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
- package/src/tui/{engine → session}/session-api.mjs +17 -61
- package/src/tui/{engine → session}/session-flow.mjs +5 -3
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +72 -78
- package/src/tui/session.mjs +22 -0
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-protocol.mjs +0 -32
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
|
44
|
-
if (
|
|
45
|
-
return String(entry?.telegramChatId ||
|
|
43
|
+
function channelIdForProvider(entry = {}, provider = "discord") {
|
|
44
|
+
if (provider === "telegram") {
|
|
45
|
+
return String(entry?.telegramChatId || "");
|
|
46
46
|
}
|
|
47
|
-
return String(entry?.discordChannelId ||
|
|
47
|
+
return String(entry?.discordChannelId || "");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// Resolve the single active-
|
|
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 = {},
|
|
52
|
+
function resolveChannelId(raw = {}, provider = "discord") {
|
|
53
53
|
const channel = raw.channel && typeof raw.channel === "object" ? raw.channel : null;
|
|
54
|
-
if (channel) return
|
|
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-
|
|
91
|
+
// Single-provider select: config.provider picks ONE of discord|telegram.
|
|
92
92
|
// Anything else falls back to the discord default.
|
|
93
|
-
const
|
|
93
|
+
const provider = raw.provider === "telegram" ? "telegram" : "discord";
|
|
94
94
|
const telegramToken = getTelegramToken();
|
|
95
|
-
const channelId = resolveChannelId(raw,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
165
|
+
return HEADLESS_PROVIDER;
|
|
166
166
|
}
|
|
167
|
-
// Single-
|
|
168
|
-
// config.
|
|
169
|
-
if (config.
|
|
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
|
|
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
|
|
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
|
-
//
|
|
183
|
-
//
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
//
|
|
204
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
17
|
+
getProvider,
|
|
18
18
|
getConfig,
|
|
19
19
|
getBridgeRuntimeConnected,
|
|
20
20
|
getChannelBridgeActive,
|
|
@@ -55,7 +55,7 @@ const inboundQueue = (() => {
|
|
|
55
55
|
};
|
|
56
56
|
})();
|
|
57
57
|
|
|
58
|
-
|
|
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 {
|
|
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
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|