mixdog 0.9.4 → 0.9.5
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 +112 -38
- package/package.json +4 -1
- package/scripts/bench/r5-orchestrated-task.json +7 -0
- package/scripts/bench/round-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
- package/scripts/bench-run.mjs +40 -7
- package/scripts/internal-comms-bench.mjs +2 -4
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- package/scripts/recall-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/tool-smoke.mjs +2 -2
- package/src/agents/heavy-worker/AGENT.md +7 -7
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +9 -10
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/mixdog-session-runtime.mjs +15 -14
- package/src/output-styles/default.md +14 -18
- package/src/output-styles/minimal.md +4 -5
- package/src/output-styles/simple.md +7 -8
- package/src/rules/agent/00-common.md +6 -5
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +6 -3
- package/src/rules/lead/lead-tool.md +6 -4
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +53 -9
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +23 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +33 -4
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +93 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +27 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/channels/backends/discord-gateway.mjs +2 -2
- package/src/runtime/channels/backends/discord.mjs +15 -26
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +23 -43
- package/src/runtime/channels/lib/backend-dispatch.mjs +5 -3
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +4 -4
- package/src/runtime/channels/lib/event-queue.mjs +15 -9
- package/src/runtime/channels/lib/inbound-routing.mjs +2 -39
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- package/src/runtime/channels/lib/scheduler.mjs +27 -113
- package/src/runtime/channels/lib/webhook/deliveries.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +8 -47
- package/src/runtime/memory/index.mjs +9 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- package/src/runtime/memory/lib/ko-morph.mjs +195 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +47 -10
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/query-handlers.mjs +27 -5
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/shared/config.mjs +14 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/session-runtime/config-helpers.mjs +45 -10
- package/src/session-runtime/quick-model-rows.mjs +46 -11
- package/src/session-runtime/quick-search-models.mjs +28 -27
- package/src/session-runtime/settings-api.mjs +34 -1
- package/src/session-runtime/tool-defs.mjs +1 -1
- package/src/session-runtime/workflow.mjs +1 -1
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/agent-tool.mjs +30 -0
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/explore-tool.mjs +39 -10
- package/src/tui/App.jsx +126 -28
- package/src/tui/app/app-format.mjs +11 -4
- package/src/tui/app/channel-pickers.mjs +2 -4
- package/src/tui/app/maintenance-pickers.mjs +60 -28
- package/src/tui/app/model-options.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +0 -1
- package/src/tui/app/transcript-window.mjs +8 -2
- package/src/tui/app/use-transcript-scroll.mjs +3 -1
- package/src/tui/app/use-transcript-window.mjs +19 -1
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +32 -11
- package/src/tui/components/PromptInput.jsx +10 -1
- package/src/tui/components/StatusLine.jsx +6 -3
- package/src/tui/dist/index.mjs +440 -293
- package/src/tui/engine.mjs +48 -12
- package/src/tui/index.jsx +11 -52
- package/src/ui/statusline.mjs +10 -0
- package/src/workflows/default/WORKFLOW.md +15 -17
- package/src/workflows/sequential/WORKFLOW.md +15 -17
- package/vendor/ink/build/ink.js +9 -1
- package/scripts/recall-usecase-probe.json +0 -6
- package/src/runtime/channels/lib/holidays.mjs +0 -138
|
@@ -245,6 +245,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
245
245
|
let _softCapActive = false; // tools disabled + wrap-up injected
|
|
246
246
|
let _softCapGraceTurns = 0; // text-only grace turns consumed (max 2)
|
|
247
247
|
let _terminatedBySoftCap = false;
|
|
248
|
+
// Hard-cap final-answer turn: one tool-less wrap-up turn granted when the
|
|
249
|
+
// hard iteration cap fires, so the session ends with text, not empty.
|
|
250
|
+
let _capFinalTurnUsed = false;
|
|
248
251
|
// Completion-first steering ladder controller. Owns the (cumulative) level-1
|
|
249
252
|
// fire count, the all-read-only / serial-single / same-file-grep streaks,
|
|
250
253
|
// and the level-2 latch. Threaded via live getters so it reads the loop's
|
|
@@ -280,9 +283,28 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
280
283
|
while (true) {
|
|
281
284
|
throwIfAborted();
|
|
282
285
|
if (iterations >= maxLoopIterations) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
+
// Final-answer turn: instead of breaking mid-transcript (which
|
|
287
|
+
// yields an empty final for locator-style agents that never got to
|
|
288
|
+
// answer), give the model ONE tool-less text turn to wrap up, then
|
|
289
|
+
// stop. Same mechanism as the worker soft cap (empty sendTools).
|
|
290
|
+
if (_capFinalTurnUsed) {
|
|
291
|
+
process.stderr.write(`[loop] hard iteration cap ${maxLoopIterations} reached (sess=${sessionId || 'unknown'}); stopping loop.\n`);
|
|
292
|
+
terminatedByCap = true;
|
|
293
|
+
// The granted final turn produced no text (model kept emitting
|
|
294
|
+
// tool calls into refusal stubs, or thinking-only). Synthesize a
|
|
295
|
+
// non-empty final so callers never see an empty response.
|
|
296
|
+
if (response && !String(response.content || '').trim()) {
|
|
297
|
+
response.content = sessionAgent === 'explorer'
|
|
298
|
+
? 'EXPLORATION_FAILED'
|
|
299
|
+
: '[iteration cap reached before final text]';
|
|
300
|
+
if (Array.isArray(response.toolCalls)) response.toolCalls = [];
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
_capFinalTurnUsed = true;
|
|
305
|
+
_softCapActive = true; // reuse soft-cap plumbing: no tool defs, refusal stubs
|
|
306
|
+
messages.push({ role: 'user', content: '<system-reminder>\nIteration cap reached. Tools are disabled. Answer NOW with your best result from what you already found.\n</system-reminder>', meta: 'hook' });
|
|
307
|
+
process.stderr.write(`[loop] hard iteration cap ${maxLoopIterations} reached (sess=${sessionId || 'unknown'}); forcing final text turn.\n`);
|
|
286
308
|
}
|
|
287
309
|
if (_iterWarnStage < _iterWarnAt.length && iterations >= _iterWarnAt[_iterWarnStage]) {
|
|
288
310
|
_iterWarnStage += 1;
|
|
@@ -370,7 +392,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
370
392
|
pressureTokens,
|
|
371
393
|
});
|
|
372
394
|
} else {
|
|
373
|
-
try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
395
|
+
try { await opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
374
396
|
const compactStartedAt = Date.now();
|
|
375
397
|
// Clear the one-shot reactive-overflow flag now that this
|
|
376
398
|
// compact pass is consuming it (compactTrigger already
|
|
@@ -626,7 +648,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
626
648
|
}, compactErr);
|
|
627
649
|
}
|
|
628
650
|
}
|
|
629
|
-
try { opts.onStageChange?.('requesting'); } catch { /* best-effort */ }
|
|
651
|
+
try { await opts.onStageChange?.('requesting'); } catch { /* best-effort */ }
|
|
630
652
|
const compactChanged = messagesArrayChanged(messages, compacted);
|
|
631
653
|
if (compactChanged) {
|
|
632
654
|
messages.length = 0;
|
|
@@ -196,7 +196,7 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
196
196
|
const budgetSourceTokens = force ? Math.max(pressureTokens, triggerTokens) : pressureTokens;
|
|
197
197
|
const compactBudget = compactTargetBudget(boundary, reserveTokens, budgetSourceTokens);
|
|
198
198
|
const budget = compactBudget || boundary;
|
|
199
|
-
try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
199
|
+
try { await opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
200
200
|
const provider = opts.provider || getProvider(session.provider) || null;
|
|
201
201
|
let compacted;
|
|
202
202
|
let compactError = null;
|
|
@@ -206,9 +206,28 @@ export const PROVIDER_RETRY_JITTER_RATIO = (() => {
|
|
|
206
206
|
return 0.2;
|
|
207
207
|
})();
|
|
208
208
|
|
|
209
|
+
// Public workflow agents are not in agents.json (that file only defines hidden
|
|
210
|
+
// roles), so they historically always fell back to the 600s default. A wedged
|
|
211
|
+
// anthropic SSE stream (HTTP 200 then zero deltas — observed 2026-07-03 on a
|
|
212
|
+
// sonnet-5 worker, twice in a row) then holds the owner for the full 10 min.
|
|
213
|
+
// Scoped workers/reviewers should be reaped faster; lead keeps the generous
|
|
214
|
+
// default because interactive xhigh turns legitimately think for minutes.
|
|
215
|
+
const WORKFLOW_AGENT_STALL_ABORT_S = {
|
|
216
|
+
'worker': 300,
|
|
217
|
+
'reviewer': 300,
|
|
218
|
+
'debugger': 420,
|
|
219
|
+
'heavy-worker': 420,
|
|
220
|
+
'maintainer': 300,
|
|
221
|
+
'explore': 240,
|
|
222
|
+
'web-researcher': 300,
|
|
223
|
+
};
|
|
224
|
+
|
|
209
225
|
export function resolveAgentStallThresholds(agent, env = process.env) {
|
|
210
226
|
const cfg = agent ? getHiddenAgent(agent) : null;
|
|
211
|
-
const
|
|
227
|
+
const roleAbort = WORKFLOW_AGENT_STALL_ABORT_S[String(agent || '')] || null;
|
|
228
|
+
const cfgAbort = cfg?.stallCap?.idleSeconds > 0
|
|
229
|
+
? cfg.stallCap.idleSeconds
|
|
230
|
+
: (roleAbort || STALL_ABORT_S);
|
|
212
231
|
const envOverride = envThresholdSeconds(env);
|
|
213
232
|
const abort = envOverride != null ? envOverride : cfgAbort;
|
|
214
233
|
// Mid-stream "slow" warning disabled — an agent stall now notifies ONLY at
|
|
@@ -17,7 +17,7 @@ export const BUILTIN_TOOLS = [
|
|
|
17
17
|
name: 'read',
|
|
18
18
|
title: 'Mixdog Read',
|
|
19
19
|
annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
20
|
-
description: 'Read known file path(s). Prefer grep content_with_context or code_graph anchors first. Window with numeric offset+limit
|
|
20
|
+
description: 'Read known file path(s). Prefer grep content_with_context or code_graph anchors first. Window with numeric offset+limit. Batch paths/regions as real arrays; adjacent spans in one file = one window, not repeated calls. Dirs use list.',
|
|
21
21
|
inputSchema: {
|
|
22
22
|
type: 'object',
|
|
23
23
|
properties: {
|
|
@@ -42,7 +42,7 @@ export const BUILTIN_TOOLS = [
|
|
|
42
42
|
minItems: 1,
|
|
43
43
|
},
|
|
44
44
|
],
|
|
45
|
-
description: 'File path.
|
|
45
|
+
description: 'File path. Batch spans via path[] or {path,offset,limit}[] regions. Pass arrays directly; JSON strings are legacy recovery only.',
|
|
46
46
|
},
|
|
47
47
|
offset: { type: 'number', minimum: 0, description: 'Numeric lines to skip before reading; 0 starts at line 1. Continue with offset:N.' },
|
|
48
48
|
limit: { type: 'number', minimum: 1, description: 'Numeric max lines to return after offset.' },
|
|
@@ -53,16 +53,16 @@ export const BUILTIN_TOOLS = [
|
|
|
53
53
|
name: 'shell',
|
|
54
54
|
title: 'Mixdog Shell',
|
|
55
55
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
56
|
-
description: `Run shell to CHANGE state or RUN programs (git/build/test/run). Never to inspect the filesystem — reading, listing, searching,
|
|
56
|
+
description: `Run shell to CHANGE state or RUN programs (git/build/test/run). Never to inspect the filesystem — reading, listing, searching, existence checks use the dedicated tools, never shell. Set shell: powershell or bash. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
|
|
57
57
|
inputSchema: {
|
|
58
58
|
type: 'object',
|
|
59
59
|
properties: {
|
|
60
60
|
command: { type: 'string', description: 'Command.' },
|
|
61
|
-
cwd: { type: 'string', description: 'Working directory. Persists across shell calls
|
|
61
|
+
cwd: { type: 'string', description: 'Working directory. Persists across shell calls in a session — omit to reuse the previous command\'s directory (e.g. after cd); pass an absolute path to change it.' },
|
|
62
62
|
timeout: { type: 'number', description: 'Timeout ms.' },
|
|
63
63
|
merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
|
|
64
64
|
mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
|
|
65
|
-
shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows
|
|
65
|
+
shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows defaults to PowerShell; bash = Git Bash/POSIX.' },
|
|
66
66
|
},
|
|
67
67
|
required: ['command'],
|
|
68
68
|
},
|
|
@@ -87,7 +87,7 @@ export const BUILTIN_TOOLS = [
|
|
|
87
87
|
name: 'grep',
|
|
88
88
|
title: 'Mixdog Grep',
|
|
89
89
|
annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
90
|
-
description: 'Search file contents by text/regex in a known scope.
|
|
90
|
+
description: 'Search file contents by text/regex in a known scope. files_with_matches/count for broad anchors, content_with_context for narrow answers. One concept → one grep; multiple scopes = ONE call with path[].',
|
|
91
91
|
inputSchema: {
|
|
92
92
|
type: 'object',
|
|
93
93
|
properties: {
|
|
@@ -163,7 +163,7 @@ export const BUILTIN_TOOLS = [
|
|
|
163
163
|
{ type: 'string' },
|
|
164
164
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
165
165
|
],
|
|
166
|
-
description: 'Partial path/name words (not file contents), or query[] to batch
|
|
166
|
+
description: 'Partial path/name words (not file contents), or query[] to batch lookups in one call.',
|
|
167
167
|
},
|
|
168
168
|
path: { type: 'string', description: 'Base directory.' },
|
|
169
169
|
head_limit: { type: 'number', description: 'Max paths.' },
|
|
@@ -9,7 +9,7 @@ export const CODE_GRAPH_TOOL_DEFS = [
|
|
|
9
9
|
properties: {
|
|
10
10
|
mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers'], description: 'Repo-local operation; code flow before text search.' },
|
|
11
11
|
files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Source file(s): string or array; one call fans out per file (imports/dependents/related/impact/symbols/overview). `file` singular alias also accepted.' },
|
|
12
|
-
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Identifier(s)/keyword(s): string or array; batch in one call. One anchor is enough
|
|
12
|
+
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Identifier(s)/keyword(s): string or array; batch in one call. One anchor is enough. `symbol` singular alias also accepted.' },
|
|
13
13
|
symbol: { type: 'string', description: 'Singular alias for symbols.' },
|
|
14
14
|
file: { type: 'string', description: 'Singular alias for files.' },
|
|
15
15
|
body: { type: 'boolean', description: 'Include body.' },
|
|
@@ -31,7 +31,7 @@ async function connectInner(self) {
|
|
|
31
31
|
self.client = null;
|
|
32
32
|
throw err;
|
|
33
33
|
}
|
|
34
|
-
self.
|
|
34
|
+
self.persistAccessFromMainChannel();
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async function buildClient(self) {
|
|
@@ -120,7 +120,7 @@ function registerEventListeners(self) {
|
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isRoleSelectMenu() || interaction.isUserSelectMenu() || interaction.isChannelSelectMenu()) {
|
|
123
|
-
const needsModal = interaction.isButton() && (interaction.customId === "sched_add_next" || interaction.customId === "sched_edit_next" || interaction.customId === "
|
|
123
|
+
const needsModal = interaction.isButton() && (interaction.customId === "sched_add_next" || interaction.customId === "sched_edit_next" || interaction.customId === "profile_edit");
|
|
124
124
|
if (needsModal) {
|
|
125
125
|
if (self.onModalRequest) {
|
|
126
126
|
await Promise.resolve(self.onModalRequest(interaction)).catch((err) => {
|
|
@@ -326,47 +326,37 @@ class DiscordBackend {
|
|
|
326
326
|
try {
|
|
327
327
|
const parsed = readSection("channels");
|
|
328
328
|
const access = normalizeAccess(parsed.access ?? this.initialAccess);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
329
|
+
// Single main channel: auto-allow the configured channel id so channel
|
|
330
|
+
// adds apply without a restart (mirrors loadAccess()).
|
|
331
|
+
const id = this.mainChannelId;
|
|
332
|
+
if (id && !(id in access.channels)) {
|
|
333
|
+
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
338
334
|
}
|
|
339
335
|
return access;
|
|
340
336
|
} catch {
|
|
341
337
|
return this.initialAccess;
|
|
342
338
|
}
|
|
343
339
|
}
|
|
344
|
-
|
|
340
|
+
persistAccessFromMainChannel() {
|
|
345
341
|
if (this.isStatic || !this.configFile) return;
|
|
346
342
|
try {
|
|
343
|
+
const id = this.mainChannelId;
|
|
344
|
+
if (!id) return;
|
|
347
345
|
const parsed = readSection("channels");
|
|
348
|
-
if (!parsed.channelsConfig) return;
|
|
349
346
|
const access = normalizeAccess(parsed.access);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const id = entry.channelId;
|
|
354
|
-
if (id && !(id in access.channels)) {
|
|
355
|
-
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
356
|
-
changed = true;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
347
|
+
if (!(id in access.channels)) {
|
|
348
|
+
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
349
|
+
this.saveAccess(access);
|
|
359
350
|
}
|
|
360
|
-
if (changed) this.saveAccess(access);
|
|
361
351
|
} catch (err) {
|
|
362
|
-
process.stderr.write(`mixdog discord:
|
|
352
|
+
process.stderr.write(`mixdog discord: persistAccessFromMainChannel failed: ${err}\n`);
|
|
363
353
|
}
|
|
364
354
|
}
|
|
365
355
|
loadAccess() {
|
|
366
356
|
const a = this.bootAccess ?? this.readConfigAccess();
|
|
367
357
|
// Single-source channel setup: auto-allow the configured main channel so
|
|
368
358
|
// both inbound (gate) and outbound (fetchAllowedChannel) accept it from a
|
|
369
|
-
// single
|
|
359
|
+
// single channel.channelId — no separate access.channels entry.
|
|
370
360
|
// An explicit entry for the same channel is preserved (not overwritten).
|
|
371
361
|
if (this.mainChannelId && a && !(this.mainChannelId in (a.channels ?? {}))) {
|
|
372
362
|
return { ...a, channels: { ...(a.channels ?? {}), [this.mainChannelId]: { allowFrom: [], requireMention: false } } };
|
|
@@ -516,9 +506,8 @@ class DiscordBackend {
|
|
|
516
506
|
}
|
|
517
507
|
if (this.onMessage) {
|
|
518
508
|
// Thread messages were gated against the parent channel's policy (see
|
|
519
|
-
// gate() above)
|
|
520
|
-
//
|
|
521
|
-
// can fall back to the parent when the thread id has no entry.
|
|
509
|
+
// gate() above). Surface parentChatId so downstream routing can fall
|
|
510
|
+
// back to the parent when the thread id needs it.
|
|
522
511
|
const isThread = (() => { try { return !!msg.channel?.isThread?.(); } catch { return false; } })();
|
|
523
512
|
const parentChatId = isThread ? (msg.channel.parentId ?? null) : null;
|
|
524
513
|
this.onMessage({
|
|
@@ -112,22 +112,18 @@ class TelegramBackend {
|
|
|
112
112
|
formatOutgoing(text) {
|
|
113
113
|
return text;
|
|
114
114
|
}
|
|
115
|
-
// Fold
|
|
116
|
-
// DiscordBackend.readConfigAccess(). Read live so
|
|
117
|
-
//
|
|
115
|
+
// Fold the single configured main channel id into access.channels, mirroring
|
|
116
|
+
// DiscordBackend.readConfigAccess(). Read live so a channel change applies
|
|
117
|
+
// without a restart. Falls back to initialAccess if the read throws.
|
|
118
118
|
readConfigAccess() {
|
|
119
119
|
try {
|
|
120
120
|
const parsed = readSection("channels");
|
|
121
121
|
const access = normalizeAccess(parsed.access ?? this.initialAccess);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
122
|
+
// Single main channel: auto-allow the configured channel id so a channel
|
|
123
|
+
// change applies without a restart (mirrors loadAccess()).
|
|
124
|
+
const id = this.mainChannelId;
|
|
125
|
+
if (id && !(id in access.channels)) {
|
|
126
|
+
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
131
127
|
}
|
|
132
128
|
return access;
|
|
133
129
|
} catch {
|
|
@@ -208,12 +208,6 @@ const {
|
|
|
208
208
|
callMemoryAction,
|
|
209
209
|
handleMemoryCallResponse,
|
|
210
210
|
} = createParentBridge({ getInstanceId: () => INSTANCE_ID });
|
|
211
|
-
function resolveChannelLabel(channelsConfig, label) {
|
|
212
|
-
if (!label || !channelsConfig) return label;
|
|
213
|
-
const entry = channelsConfig[label];
|
|
214
|
-
if (entry?.channelId) return entry.channelId;
|
|
215
|
-
return label;
|
|
216
|
-
}
|
|
217
211
|
let channelBridgeActive = false;
|
|
218
212
|
function writeBridgeState(active) {
|
|
219
213
|
try {
|
|
@@ -518,11 +512,8 @@ async function rebindTranscriptContext(channelId, options = {}) {
|
|
|
518
512
|
const scheduler = new Scheduler(
|
|
519
513
|
config.nonInteractive ?? [],
|
|
520
514
|
config.interactive ?? [],
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
config.channelsConfig,
|
|
524
|
-
// 0.1.62: top-level normalized config carries quiet/schedules.
|
|
525
|
-
config
|
|
515
|
+
// Single resolved main-channel id used for the schedule `channel` flag.
|
|
516
|
+
config.channelId
|
|
526
517
|
);
|
|
527
518
|
// Register the pending-dispatch probe so the scheduler treats an in-flight
|
|
528
519
|
// bridge dispatch as "active" regardless of user-inbound silence.
|
|
@@ -608,10 +599,7 @@ async function bindPersistedTranscriptIfAny() {
|
|
|
608
599
|
}
|
|
609
600
|
}
|
|
610
601
|
if (!currentStatus.channelId && channelBridgeActive) {
|
|
611
|
-
const
|
|
612
|
-
const mainLabel = config.mainChannel ?? "main";
|
|
613
|
-
const mainEntry = chCfg?.[mainLabel];
|
|
614
|
-
const mainId = mainEntry?.channelId;
|
|
602
|
+
const mainId = config.channelId;
|
|
615
603
|
if (mainId) {
|
|
616
604
|
statusState.update((state) => {
|
|
617
605
|
state.channelId = mainId;
|
|
@@ -637,17 +625,14 @@ function shouldStartEventPipelineRuntime() {
|
|
|
637
625
|
}
|
|
638
626
|
function ensureEventPipelineRuntime() {
|
|
639
627
|
if (!eventPipeline) {
|
|
640
|
-
eventPipeline = new EventPipeline(config.events, config.
|
|
628
|
+
eventPipeline = new EventPipeline(config.events, config.channelId);
|
|
641
629
|
wireEventQueueHandlers(eventPipeline.getQueue());
|
|
642
630
|
}
|
|
643
631
|
return eventPipeline;
|
|
644
632
|
}
|
|
645
633
|
function ensureWebhookServerRuntime() {
|
|
646
634
|
if (!webhookServer) {
|
|
647
|
-
|
|
648
|
-
// top-level `quiet` subtree (and `webhook.respectQuiet`) introduced in
|
|
649
|
-
// 0.1.62. See applyDefaults() in lib/config.mjs.
|
|
650
|
-
webhookServer = new WebhookServer(config.webhook, { quiet: config.quiet ?? null });
|
|
635
|
+
webhookServer = new WebhookServer(config.webhook);
|
|
651
636
|
}
|
|
652
637
|
wireWebhookHandlers();
|
|
653
638
|
return webhookServer;
|
|
@@ -666,7 +651,7 @@ function syncOwnedWebhookAndEventRuntime({ reload = false } = {}) {
|
|
|
666
651
|
if (shouldStartEventPipelineRuntime()) {
|
|
667
652
|
const pipeline = ensureEventPipelineRuntime();
|
|
668
653
|
if (reload) {
|
|
669
|
-
pipeline.reloadConfig(config.events, config.
|
|
654
|
+
pipeline.reloadConfig(config.events, config.channelId);
|
|
670
655
|
wireEventQueueHandlers(pipeline.getQueue());
|
|
671
656
|
}
|
|
672
657
|
pipeline.start();
|
|
@@ -683,7 +668,7 @@ function syncOwnedWebhookAndEventRuntime({ reload = false } = {}) {
|
|
|
683
668
|
// so we don't race the bound port — calling start() synchronously
|
|
684
669
|
// here would re-listen before close() finishes and surface
|
|
685
670
|
// EADDRINUSE on the same port.
|
|
686
|
-
server.reloadConfig(config.webhook, {
|
|
671
|
+
server.reloadConfig(config.webhook, {
|
|
687
672
|
autoStart: false
|
|
688
673
|
}).then(() => {
|
|
689
674
|
// A stopWebhookAndEventRuntime() / deactivate landing during the async
|
|
@@ -940,10 +925,8 @@ async function reloadRuntimeConfig() {
|
|
|
940
925
|
scheduler.reloadConfig(
|
|
941
926
|
config.nonInteractive ?? [],
|
|
942
927
|
config.interactive ?? [],
|
|
943
|
-
//
|
|
944
|
-
config.
|
|
945
|
-
// 0.1.62: top-level normalized config (quiet/schedules).
|
|
946
|
-
config,
|
|
928
|
+
// Single resolved main-channel id used for the schedule `channel` flag.
|
|
929
|
+
config.channelId,
|
|
947
930
|
{ restart: bridgeRuntimeConnected }
|
|
948
931
|
);
|
|
949
932
|
const nextBackend = createBackend(config);
|
|
@@ -1128,15 +1111,17 @@ function wireWebhookHandlers() {
|
|
|
1128
1111
|
);
|
|
1129
1112
|
});
|
|
1130
1113
|
}
|
|
1131
|
-
function resolveWebhookChannelId(
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
const
|
|
1138
|
-
if (
|
|
1139
|
-
|
|
1114
|
+
function resolveWebhookChannelId(channelFlag) {
|
|
1115
|
+
// Single main channel: the endpoint's `channel` frontmatter is a boolean-ish
|
|
1116
|
+
// flag (any non-empty value except "false" → post to the main channel). A raw
|
|
1117
|
+
// channel id is still honored verbatim for owner-authored overrides.
|
|
1118
|
+
const main = config?.channelId || "";
|
|
1119
|
+
if (channelFlag == null) return main;
|
|
1120
|
+
const v = String(channelFlag).trim();
|
|
1121
|
+
if (v === "" || v.toLowerCase() === "false") return "";
|
|
1122
|
+
// A pure-digit / snowflake-looking value is treated as an explicit id;
|
|
1123
|
+
// anything else (legacy labels like "main") maps to the main channel.
|
|
1124
|
+
return /^-?\d+$/.test(v) ? v : main;
|
|
1140
1125
|
}
|
|
1141
1126
|
function wireEventQueueHandlers(eventQueue) {
|
|
1142
1127
|
if (!eventQueue) return;
|
|
@@ -1390,13 +1375,10 @@ import { TOOL_DEFS } from './tool-defs.mjs';
|
|
|
1390
1375
|
const BACKEND_TOOLS = /* @__PURE__ */ new Set(["reply", "fetch"]);
|
|
1391
1376
|
// ── Inbound routing / dedup / ownership helpers ─────────────────────────────
|
|
1392
1377
|
// Extracted → lib/inbound-routing.mjs. Bound to live config/identity getters.
|
|
1393
|
-
// Created here (ahead of backend-dispatch) so labelForChannelId is initialised
|
|
1394
|
-
// before it is passed into createBackendDispatch below.
|
|
1395
1378
|
const {
|
|
1396
1379
|
writeChannelOwner,
|
|
1397
1380
|
shouldDropDuplicateInbound,
|
|
1398
1381
|
resolveInboundRoute,
|
|
1399
|
-
labelForChannelId,
|
|
1400
1382
|
} = createInboundRouting({
|
|
1401
1383
|
getConfig: () => config,
|
|
1402
1384
|
getInstanceId: () => INSTANCE_ID,
|
|
@@ -1414,8 +1396,6 @@ const {
|
|
|
1414
1396
|
getConfig: () => config,
|
|
1415
1397
|
getBackend: () => backend,
|
|
1416
1398
|
scheduler,
|
|
1417
|
-
resolveChannelLabel,
|
|
1418
|
-
labelForChannelId,
|
|
1419
1399
|
});
|
|
1420
1400
|
// ── Worker/HTTP tool-call dispatch ──────────────────────────────────────────
|
|
1421
1401
|
// handleToolCall switch + bridge auto-connect retry wrapper. Extracted →
|
|
@@ -1472,7 +1452,8 @@ backend.onMessage = (msg) => {
|
|
|
1472
1452
|
}
|
|
1473
1453
|
if (!channelBridgeActive) return;
|
|
1474
1454
|
if (shouldDropDuplicateInbound(msg)) return;
|
|
1475
|
-
|
|
1455
|
+
// No label concept anymore: the channel id IS the identifier.
|
|
1456
|
+
recordFetchedMessages(msg.chatId, msg.chatId, [{ id: msg.messageId }], { markRead: true });
|
|
1476
1457
|
if (!writeChannelOwner(msg.chatId)) return;
|
|
1477
1458
|
const route = resolveInboundRoute(msg.chatId, msg.parentChatId);
|
|
1478
1459
|
scheduler.noteActivity();
|
|
@@ -1877,9 +1858,8 @@ if (_isWorkerMode && process.send) {
|
|
|
1877
1858
|
}
|
|
1878
1859
|
refreshToolExecConsumerMarker();
|
|
1879
1860
|
}
|
|
1880
|
-
const mainLabel = config?.mainChannel || 'main';
|
|
1881
1861
|
const target = (statusState?.read?.().channelId)
|
|
1882
|
-
||
|
|
1862
|
+
|| config?.channelId
|
|
1883
1863
|
|| null;
|
|
1884
1864
|
if (!target || !backend?.sendMessage) {
|
|
1885
1865
|
process.stderr.write(`mixdog channels: permission_request dropped, no target channel (request_id=${request_id})\n`);
|
|
@@ -3,7 +3,7 @@ import { recordFetchedMessages } from "./status-snapshot.mjs";
|
|
|
3
3
|
// Backend-tool dispatch helpers. Extracted verbatim from channels/index.mjs
|
|
4
4
|
// (behavior-preserving). Bound to live getters so runtime config/backend
|
|
5
5
|
// reloads keep the original file-level reference semantics.
|
|
6
|
-
function createBackendDispatch({ getConfig, getBackend, scheduler
|
|
6
|
+
function createBackendDispatch({ getConfig, getBackend, scheduler }) {
|
|
7
7
|
async function dispatchReply(args) {
|
|
8
8
|
const sendOpts = {
|
|
9
9
|
replyTo: args.reply_to,
|
|
@@ -22,12 +22,14 @@ function createBackendDispatch({ getConfig, getBackend, scheduler, resolveChanne
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
async function dispatchFetch(args) {
|
|
25
|
+
// `args.channel` is a raw channel id (no label resolution anymore); when
|
|
26
|
+
// omitted, fall back to the single configured main channel id.
|
|
25
27
|
const config = getConfig();
|
|
26
|
-
const channelId =
|
|
28
|
+
const channelId = args.channel || config.channelId || "";
|
|
27
29
|
const limit = args.limit ?? 20;
|
|
28
30
|
let msgs;
|
|
29
31
|
msgs = await getBackend().fetchMessages(channelId, limit);
|
|
30
|
-
recordFetchedMessages(channelId,
|
|
32
|
+
recordFetchedMessages(channelId, channelId, msgs);
|
|
31
33
|
const text = msgs.length === 0 ? "(no messages)" : msgs.map((m) => {
|
|
32
34
|
const atts = m.attachmentCount > 0 ? ` +${m.attachmentCount}att` : "";
|
|
33
35
|
return `[${m.ts}] ${m.user}: ${m.text} (id: ${m.id}${atts})`;
|