mixdog 0.9.17 → 0.9.19
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/package.json +3 -2
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/recall-bench-cases.json +11 -0
- package/scripts/recall-bench.mjs +91 -2
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +5 -2
- package/scripts/tool-smoke.mjs +101 -27
- package/src/agents/debugger/AGENT.md +3 -3
- package/src/agents/heavy-worker/AGENT.md +7 -10
- package/src/agents/maintainer/AGENT.md +1 -2
- package/src/agents/reviewer/AGENT.md +1 -2
- package/src/agents/worker/AGENT.md +5 -8
- package/src/defaults/agents.json +3 -0
- package/src/mixdog-session-runtime.mjs +23 -6
- package/src/rules/agent/00-core.md +4 -3
- package/src/rules/agent/30-explorer.md +53 -22
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +24 -29
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
- package/src/runtime/channels/backends/discord-attachments.mjs +2 -2
- package/src/runtime/channels/backends/discord-gateway.mjs +26 -3
- package/src/runtime/channels/backends/discord.mjs +139 -7
- package/src/runtime/channels/index.mjs +290 -76
- package/src/runtime/channels/lib/crash-log.mjs +21 -3
- package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
- package/src/runtime/channels/lib/memory-client.mjs +32 -14
- package/src/runtime/channels/lib/output-forwarder.mjs +156 -14
- package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
- package/src/runtime/channels/lib/runtime-paths.mjs +48 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
- package/src/runtime/channels/lib/voice-transcription.mjs +4 -2
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
- package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
- package/src/runtime/memory/lib/query-handlers.mjs +36 -4
- package/src/runtime/memory/lib/recall-format.mjs +106 -6
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +10 -4
- package/src/runtime/shared/background-tasks.mjs +4 -2
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +30 -1
- package/src/session-runtime/config-lifecycle.mjs +1 -1
- package/src/session-runtime/cwd-plugins.mjs +46 -3
- package/src/session-runtime/mcp-glue.mjs +24 -3
- package/src/session-runtime/output-styles.mjs +44 -10
- package/src/session-runtime/workflow.mjs +16 -1
- package/src/standalone/channel-worker.mjs +88 -7
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/App.jsx +57 -77
- package/src/tui/app/channel-pickers.mjs +45 -0
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/transcript-window.mjs +66 -1
- package/src/tui/app/use-mouse-input.mjs +9 -2
- package/src/tui/app/use-prompt-handlers.mjs +7 -94
- package/src/tui/app/use-transcript-scroll.mjs +5 -1
- package/src/tui/app/use-transcript-window.mjs +74 -8
- package/src/tui/components/PromptInput.jsx +33 -64
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +4744 -4806
- package/src/tui/engine.mjs +109 -20
- package/src/tui/lib/voice-setup.mjs +166 -0
- package/src/tui/paste-attachments.mjs +12 -5
- package/src/tui/prompt-history-store.mjs +125 -12
- package/scripts/bench/cache-probe-tasks.json +0 -8
- package/scripts/bench/lead-review-tasks-r3.json +0 -20
- package/scripts/bench/lead-review-tasks.json +0 -20
- package/scripts/bench/r4-mixed-tasks.json +0 -20
- package/scripts/bench/r5-orchestrated-task.json +0 -7
- package/scripts/bench/review-tasks.json +0 -20
- package/scripts/bench/round-codex.json +0 -114
- package/scripts/bench/round-mixdog-lead-r3.json +0 -269
- package/scripts/bench/round-mixdog-lead.json +0 -269
- package/scripts/bench/round-mixdog.json +0 -126
- package/scripts/bench/round-r10-bigsample.json +0 -679
- package/scripts/bench/round-r11-codexalign.json +0 -257
- package/scripts/bench/round-r13-clientmeta.json +0 -464
- package/scripts/bench/round-r14-betafeatures.json +0 -466
- package/scripts/bench/round-r15-fulldefault.json +0 -462
- package/scripts/bench/round-r16-sessionid.json +0 -466
- package/scripts/bench/round-r17-wirebytes.json +0 -456
- package/scripts/bench/round-r18-prewarm.json +0 -468
- package/scripts/bench/round-r19-clean.json +0 -472
- package/scripts/bench/round-r20-prewarm-clean.json +0 -475
- package/scripts/bench/round-r21-delta-retry.json +0 -473
- package/scripts/bench/round-r22-full-probe.json +0 -693
- package/scripts/bench/round-r23-itemprobe.json +0 -701
- package/scripts/bench/round-r24-shapefix.json +0 -677
- package/scripts/bench/round-r25-serial.json +0 -464
- package/scripts/bench/round-r26-parallel3.json +0 -671
- package/scripts/bench/round-r27-parallel10.json +0 -894
- package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
- package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
- package/scripts/bench/round-r30-instid.json +0 -253
- package/scripts/bench/round-r31-upgradeprobe.json +0 -256
- package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
- package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
- package/scripts/bench/round-r34-orchestrated.json +0 -120
- package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
- package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
- package/scripts/bench/round-r4-codex.json +0 -114
- package/scripts/bench/round-r4-mixed.json +0 -225
- package/scripts/bench/round-r5-gpt-lead.json +0 -259
- package/scripts/bench/round-r6-codex.json +0 -114
- package/scripts/bench/round-r6-solo.json +0 -257
- package/scripts/bench/round-r7-full.json +0 -254
- package/scripts/bench/round-r8-fulldefault.json +0 -255
- package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
- package/src/tui/lib/voice-recorder.mjs +0 -469
|
@@ -205,20 +205,43 @@ function registerShardListeners(self) {
|
|
|
205
205
|
|
|
206
206
|
async function awaitLogin(self) {
|
|
207
207
|
let readyTimeout;
|
|
208
|
+
// Adaptive ready timeout: grow on consecutive login failures (30s→60s→90s
|
|
209
|
+
// cap) so an identify delayed by gateway rate-limiting can still complete
|
|
210
|
+
// its READY handshake instead of tripping a fixed 30s timeout every retry.
|
|
211
|
+
const readyMs = Math.min(90_000, 30_000 + (self._loginFailures || 0) * 30_000);
|
|
208
212
|
const readyPromise = new Promise((resolve, reject) => {
|
|
209
|
-
readyTimeout = setTimeout(
|
|
213
|
+
readyTimeout = setTimeout(
|
|
214
|
+
() => reject(new Error(`discord ready timeout (${Math.round(readyMs / 1000)}s)`)),
|
|
215
|
+
readyMs
|
|
216
|
+
);
|
|
210
217
|
self.client.once(self._discordEvents().ClientReady, () => {
|
|
211
218
|
clearTimeout(readyTimeout);
|
|
212
219
|
resolve();
|
|
213
220
|
});
|
|
214
221
|
});
|
|
222
|
+
// Guarantee the ready-timeout rejection can never surface as a process-global
|
|
223
|
+
// unhandledRejection regardless of how the race below settles: attach an
|
|
224
|
+
// absorbing handler; the real handling still happens via the awaits.
|
|
225
|
+
readyPromise.catch(() => {});
|
|
226
|
+
// Observe the ready timer from the start: login() can hang past the 30s
|
|
227
|
+
// timer (gateway reconnect), and a rejection on an un-awaited readyPromise
|
|
228
|
+
// becomes a process-global unhandledRejection → fatal crash path
|
|
229
|
+
// (channels/index.mjs unhandledRejection → _fatalCrash → exit(1)). Racing
|
|
230
|
+
// login with readyPromise keeps the rejection inside this connect() chain,
|
|
231
|
+
// which every caller already catches non-fatally.
|
|
232
|
+
const loginPromise = self.client.login(self.token);
|
|
233
|
+
// A late login settlement after the timeout raced ahead must not itself
|
|
234
|
+
// become a second unhandled rejection.
|
|
235
|
+
loginPromise.catch(() => {});
|
|
215
236
|
try {
|
|
216
|
-
await
|
|
237
|
+
await Promise.race([loginPromise, readyPromise]);
|
|
238
|
+
await readyPromise;
|
|
239
|
+
self._loginFailures = 0;
|
|
217
240
|
} catch (err) {
|
|
241
|
+
self._loginFailures = (self._loginFailures || 0) + 1;
|
|
218
242
|
clearTimeout(readyTimeout);
|
|
219
243
|
throw err;
|
|
220
244
|
}
|
|
221
|
-
await readyPromise;
|
|
222
245
|
}
|
|
223
246
|
|
|
224
247
|
export {
|
|
@@ -23,6 +23,20 @@ import { normalizeAccess, safeAttName } from "./discord-access.mjs";
|
|
|
23
23
|
import { MAX_ATTACHMENT_BYTES, downloadSingleAttachment } from "./discord-attachments.mjs";
|
|
24
24
|
import * as gateway from "./discord-gateway.mjs";
|
|
25
25
|
const MAX_CHUNK_LIMIT = MAX_DISCORD_MESSAGE;
|
|
26
|
+
// Per-attempt cap for a single discord.js fetch/send. Without it a wedged
|
|
27
|
+
// socket (network blip, "other side closed") leaves the await pending forever,
|
|
28
|
+
// which upstream leaves the forwarder's `sending` flag stuck and silences all
|
|
29
|
+
// outbound. On timeout we treat the client as dead and reset+reconnect it.
|
|
30
|
+
const SEND_ATTEMPT_TIMEOUT_MS = 30_000;
|
|
31
|
+
// Reconnect backoff: consecutive _resetClient failures back off exponentially
|
|
32
|
+
// with jitter up to a cap so a wedged/rate-limited gateway is not hammered with
|
|
33
|
+
// fixed sub-minute destroy()/login() cycles (that fixed cadence triggered
|
|
34
|
+
// Discord identify rate-limiting → the ready-timeout death loop).
|
|
35
|
+
const RESET_BACKOFF_BASE_MS = 2_000;
|
|
36
|
+
const RESET_BACKOFF_CAP_MS = 60_000;
|
|
37
|
+
// After a timed-out send is reset, cap how long we wait for the original
|
|
38
|
+
// (abandoned) promise to actually settle before giving up with unknown outcome.
|
|
39
|
+
const SETTLE_CAP_MS = 15_000;
|
|
26
40
|
const RECENT_SENT_CAP = 200;
|
|
27
41
|
class DiscordBackend {
|
|
28
42
|
name = "discord";
|
|
@@ -41,7 +55,12 @@ class DiscordBackend {
|
|
|
41
55
|
initialAccess;
|
|
42
56
|
recentSentIds = /* @__PURE__ */ new Set();
|
|
43
57
|
sendCount = 0;
|
|
58
|
+
_resetFailures = 0;
|
|
59
|
+
_loginFailures = 0;
|
|
44
60
|
typingIntervals = /* @__PURE__ */ new Map();
|
|
61
|
+
// In-flight sendMessage() promises, awaited by drainPendingSends() before a
|
|
62
|
+
// handoff disconnect so a reply is not cut off mid-delivery.
|
|
63
|
+
_pendingSends = /* @__PURE__ */ new Set();
|
|
45
64
|
constructor(config, stateDir) {
|
|
46
65
|
this.token = config.token;
|
|
47
66
|
this.mainChannelId = config.mainChannelId ?? "";
|
|
@@ -74,11 +93,70 @@ class DiscordBackend {
|
|
|
74
93
|
if (this.client) this.client.destroy();
|
|
75
94
|
this._connectPromise = null;
|
|
76
95
|
}
|
|
96
|
+
// ── Self-heal helpers ──────────────────────────────────────────────
|
|
97
|
+
/** Race a discord.js op-promise against a per-attempt timeout so a wedged
|
|
98
|
+
* socket can't hang the outbound path. On timeout the rejection carries
|
|
99
|
+
* `_timeout` so the caller knows the underlying op is still in flight (and
|
|
100
|
+
* must be settled/adopted, not blindly retried → duplicate). */
|
|
101
|
+
_withTimeout(promise, label) {
|
|
102
|
+
let timer;
|
|
103
|
+
const timeout = new Promise((_, reject) => {
|
|
104
|
+
timer = setTimeout(() => {
|
|
105
|
+
const e = new Error(`${label} timed out after ${SEND_ATTEMPT_TIMEOUT_MS}ms`);
|
|
106
|
+
e._timeout = true;
|
|
107
|
+
reject(e);
|
|
108
|
+
}, SEND_ATTEMPT_TIMEOUT_MS);
|
|
109
|
+
});
|
|
110
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
111
|
+
}
|
|
112
|
+
/** True when an error means the client is dead (aborted, "other side
|
|
113
|
+
* closed", or our per-attempt timeout) and must be reset+reconnected. */
|
|
114
|
+
_shouldResetClient(err) {
|
|
115
|
+
if (err?.name === "AbortError" || err?.code === "ABORT_ERR") return true;
|
|
116
|
+
const msg = String(err?.message || err || "").toLowerCase();
|
|
117
|
+
return msg.includes("aborted") || msg.includes("other side closed") ||
|
|
118
|
+
msg.includes("socket hang up") || msg.includes("econnreset") || msg.includes("timed out");
|
|
119
|
+
}
|
|
120
|
+
/** Destroy the wedged client and rebuild it via connect() so the next
|
|
121
|
+
* fetch/send (and the forwarder's queue-head-preserving retry) hits a fresh
|
|
122
|
+
* gateway/REST client. Serialized behind a single in-flight reset so
|
|
123
|
+
* concurrent callers await the SAME reconnect instead of racing multiple
|
|
124
|
+
* destroy()/login() cycles. */
|
|
125
|
+
async _resetClient() {
|
|
126
|
+
if (this._resetPromise) return this._resetPromise;
|
|
127
|
+
this._resetPromise = (async () => {
|
|
128
|
+
// Back off before rebuilding: on repeated reset failures wait an
|
|
129
|
+
// exponentially growing (jittered) delay capped at RESET_BACKOFF_CAP_MS
|
|
130
|
+
// so we never re-identify in a tight loop.
|
|
131
|
+
const n = this._resetFailures || 0;
|
|
132
|
+
if (n > 0) {
|
|
133
|
+
const cap = Math.min(RESET_BACKOFF_CAP_MS, RESET_BACKOFF_BASE_MS * 2 ** (n - 1));
|
|
134
|
+
const delay = Math.floor(cap / 2 + Math.random() * (cap / 2));
|
|
135
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
136
|
+
}
|
|
137
|
+
try { this.client?.destroy?.(); } catch {}
|
|
138
|
+
this.client = null;
|
|
139
|
+
this._connectPromise = null;
|
|
140
|
+
await this.connect();
|
|
141
|
+
this._resetFailures = 0;
|
|
142
|
+
})().catch((err) => {
|
|
143
|
+
this._resetFailures = (this._resetFailures || 0) + 1;
|
|
144
|
+
throw err;
|
|
145
|
+
}).finally(() => { this._resetPromise = null; });
|
|
146
|
+
return this._resetPromise;
|
|
147
|
+
}
|
|
148
|
+
/** Health-check the gateway; reset+reconnect if the ws is not READY. Called
|
|
149
|
+
* by the owner re-claim self-heal path. (discord.js Status.Ready === 0) */
|
|
150
|
+
async healthCheck() {
|
|
151
|
+
if (this.client && this.client.ws?.status === 0) return;
|
|
152
|
+
await this._resetClient();
|
|
153
|
+
}
|
|
77
154
|
resetSendCount() {
|
|
78
155
|
this.sendCount = 0;
|
|
79
156
|
}
|
|
80
157
|
startTyping(channelId) {
|
|
81
158
|
this.stopTyping(channelId);
|
|
159
|
+
if (!this.client) return;
|
|
82
160
|
const ch = this.client.channels.cache.get(channelId);
|
|
83
161
|
if (ch && "sendTyping" in ch) {
|
|
84
162
|
void ch.sendTyping().catch(() => {
|
|
@@ -100,7 +178,25 @@ class DiscordBackend {
|
|
|
100
178
|
}
|
|
101
179
|
}
|
|
102
180
|
// ── Outbound operations ────────────────────────────────────────────
|
|
181
|
+
// Public entry: tracks the send so drainPendingSends() can await in-flight
|
|
182
|
+
// deliveries before an ownership-handoff disconnect. Delegates to the impl.
|
|
103
183
|
async sendMessage(chatId, text, opts) {
|
|
184
|
+
const p = this._sendMessageImpl(chatId, text, opts);
|
|
185
|
+
const tracked = Promise.resolve(p).catch(() => {});
|
|
186
|
+
this._pendingSends.add(tracked);
|
|
187
|
+
tracked.finally(() => this._pendingSends.delete(tracked));
|
|
188
|
+
return p;
|
|
189
|
+
}
|
|
190
|
+
/** Await in-flight sends (bounded) so a handoff drains outbound before
|
|
191
|
+
* disconnect. Bounded by timeoutMs so a wedged send can't stall teardown. */
|
|
192
|
+
async drainPendingSends(timeoutMs = 4000) {
|
|
193
|
+
if (this._pendingSends.size === 0) return;
|
|
194
|
+
await Promise.race([
|
|
195
|
+
Promise.allSettled([...this._pendingSends]),
|
|
196
|
+
new Promise((r) => setTimeout(r, timeoutMs)),
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
199
|
+
async _sendMessageImpl(chatId, text, opts) {
|
|
104
200
|
const ch = await this.fetchAllowedChannel(chatId);
|
|
105
201
|
if (!("send" in ch)) throw new Error("channel is not sendable");
|
|
106
202
|
const files = opts?.files ?? [];
|
|
@@ -172,13 +268,38 @@ class DiscordBackend {
|
|
|
172
268
|
// resumes at this chunk index instead of re-sending 0..i-1.
|
|
173
269
|
let attempt = 0;
|
|
174
270
|
for (;;) {
|
|
271
|
+
const sendPromise = ch.send(payload);
|
|
175
272
|
try {
|
|
176
|
-
const sent = await
|
|
273
|
+
const sent = await this._withTimeout(sendPromise, "discord send");
|
|
177
274
|
this.noteSent(sent.id);
|
|
178
275
|
sentIds.push(sent.id);
|
|
179
276
|
break;
|
|
180
277
|
} catch (err) {
|
|
181
278
|
attempt++;
|
|
279
|
+
if (err?._timeout) {
|
|
280
|
+
// Watchdog abandoned the send but the socket may still deliver.
|
|
281
|
+
// Destroy the client to tear down the in-flight REST socket so the
|
|
282
|
+
// ORIGINAL promise settles, then AWAIT + adopt its real outcome:
|
|
283
|
+
// delivered → done (no retry → no duplicate); else requeue against
|
|
284
|
+
// the fresh client via a resume token.
|
|
285
|
+
try { await this._resetClient(); } catch {}
|
|
286
|
+
// Cap the settlement wait: destroy() should abort the in-flight
|
|
287
|
+
// socket, but never hang here if it doesn't. On unknown outcome
|
|
288
|
+
// throw retryable (duplicate risk accepted over a permanent hang).
|
|
289
|
+
sendPromise.catch(() => {}); // swallow a late rejection
|
|
290
|
+
const settled = await Promise.race([
|
|
291
|
+
sendPromise.then((v) => ({ ok: true, v }), () => ({ ok: false })),
|
|
292
|
+
new Promise((r) => setTimeout(() => r({ ok: false }), SETTLE_CAP_MS))
|
|
293
|
+
]);
|
|
294
|
+
if (settled.ok) {
|
|
295
|
+
this.noteSent(settled.v.id);
|
|
296
|
+
sentIds.push(settled.v.id);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
300
|
+
e.resumeToken = { hash: contentHash, nextChunkIdx: i, sentIds: [...sentIds], prefixed: applyPrefix, limit };
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
182
303
|
const status = err?.status ?? err?.code ?? err?.httpStatus;
|
|
183
304
|
// Classify: PERMANENT = a 4xx client error (unknown channel/message,
|
|
184
305
|
// missing access/permissions, 404 …) that will never succeed on
|
|
@@ -192,7 +313,15 @@ class DiscordBackend {
|
|
|
192
313
|
e.permanent = true;
|
|
193
314
|
throw e;
|
|
194
315
|
}
|
|
195
|
-
|
|
316
|
+
// Dead client (abort / "other side closed" / per-attempt timeout):
|
|
317
|
+
// destroy + reconnect so the forwarder's transient-retry path hits a
|
|
318
|
+
// fresh client, and throw a resume token now instead of burning the
|
|
319
|
+
// remaining attempts on a stale channel handle bound to the old client.
|
|
320
|
+
const dead = this._shouldResetClient(err);
|
|
321
|
+
if (dead) {
|
|
322
|
+
try { await this._resetClient(); } catch {}
|
|
323
|
+
}
|
|
324
|
+
if (dead || attempt >= 3) {
|
|
196
325
|
// Attach an opaque resume token pointing at the chunk that failed
|
|
197
326
|
// (i). sentIds holds every chunk already delivered (including any
|
|
198
327
|
// seeded from a prior token).
|
|
@@ -301,13 +430,13 @@ class DiscordBackend {
|
|
|
301
430
|
const msg = await ch.messages.fetch(messageId);
|
|
302
431
|
await msg.delete();
|
|
303
432
|
}
|
|
304
|
-
async downloadAttachment(chatId, messageId) {
|
|
433
|
+
async downloadAttachment(chatId, messageId, opts = {}) {
|
|
305
434
|
const ch = await this.fetchAllowedChannel(chatId);
|
|
306
435
|
const msg = await ch.messages.fetch(messageId);
|
|
307
436
|
if (msg.attachments.size === 0) return [];
|
|
308
437
|
const results = [];
|
|
309
438
|
for (const att of msg.attachments.values()) {
|
|
310
|
-
const path = await this.downloadSingleAttachment(att);
|
|
439
|
+
const path = await this.downloadSingleAttachment(att, opts);
|
|
311
440
|
results.push({
|
|
312
441
|
id: att.id,
|
|
313
442
|
path,
|
|
@@ -526,7 +655,10 @@ class DiscordBackend {
|
|
|
526
655
|
}
|
|
527
656
|
// ── Channel helpers ────────────────────────────────────────────────
|
|
528
657
|
async fetchTextChannel(id) {
|
|
529
|
-
|
|
658
|
+
if (!this.client) await this.connect();
|
|
659
|
+
const p = this.client.channels.fetch(id);
|
|
660
|
+
p.catch(() => {}); // if the watchdog wins, don't leak an unhandled rejection
|
|
661
|
+
const ch = await this._withTimeout(p, "discord channels.fetch");
|
|
530
662
|
if (!ch || !ch.isTextBased()) {
|
|
531
663
|
throw new Error(`channel ${id} not found or not text-based`);
|
|
532
664
|
}
|
|
@@ -571,8 +703,8 @@ class DiscordBackend {
|
|
|
571
703
|
throw new Error(`refusing to send channel state: ${f}`);
|
|
572
704
|
}
|
|
573
705
|
}
|
|
574
|
-
async downloadSingleAttachment(att) {
|
|
575
|
-
return downloadSingleAttachment(att, this.inboxDir);
|
|
706
|
+
async downloadSingleAttachment(att, opts = {}) {
|
|
707
|
+
return downloadSingleAttachment(att, this.inboxDir, opts);
|
|
576
708
|
}
|
|
577
709
|
}
|
|
578
710
|
export {
|