mixdog 0.9.4 → 0.9.6
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 +131 -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 +441 -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
|
@@ -5,7 +5,6 @@ import { TelegramBackend } from "../backends/telegram.mjs";
|
|
|
5
5
|
import { readSection, updateSection, CONFIG_PATH as MIXDOG_CONFIG_PATH, getDiscordToken, getTelegramToken, diagnoseDiscordTokenValue } from "../../shared/config.mjs";
|
|
6
6
|
import { listSchedules } from "../../shared/schedules-store.mjs";
|
|
7
7
|
import { resolvePluginData } from "../../shared/plugin-paths.mjs";
|
|
8
|
-
import { isHolidaySync } from "./holidays.mjs";
|
|
9
8
|
const DATA_DIR = resolvePluginData();
|
|
10
9
|
const CONFIG_FILE = MIXDOG_CONFIG_PATH;
|
|
11
10
|
const DEFAULT_ACCESS = {
|
|
@@ -18,34 +17,16 @@ const DEFAULT_CONFIG = {
|
|
|
18
17
|
discord: { token: "" },
|
|
19
18
|
telegram: { token: "" },
|
|
20
19
|
access: DEFAULT_ACCESS,
|
|
21
|
-
|
|
22
|
-
channelsConfig: {
|
|
23
|
-
main: { channelId: "", mode: "interactive" }
|
|
24
|
-
}
|
|
20
|
+
channel: { channelId: "" }
|
|
25
21
|
};
|
|
26
|
-
// Shared defaults layer
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// shape when the file has missing sections.
|
|
22
|
+
// Shared defaults layer. Merge semantics: user values win; defaults
|
|
23
|
+
// only fill missing fields. Helper is exported so the setup UI and
|
|
24
|
+
// runtime both produce the same shape when the file has missing sections.
|
|
30
25
|
const CONFIG_DEFAULTS = {
|
|
31
|
-
|
|
32
|
-
schedules: { respectQuiet: true },
|
|
33
|
-
webhook: { enabled: true, respectQuiet: false }
|
|
26
|
+
webhook: { enabled: true }
|
|
34
27
|
};
|
|
35
|
-
const DEFAULT_HOLIDAY_COUNTRY = "KR";
|
|
36
|
-
function normalizeHolidaySetting(value) {
|
|
37
|
-
if (value === true) return DEFAULT_HOLIDAY_COUNTRY;
|
|
38
|
-
if (typeof value === "string") {
|
|
39
|
-
const code = value.trim().toUpperCase();
|
|
40
|
-
return /^[A-Z]{2}$/.test(code) ? code : false;
|
|
41
|
-
}
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
28
|
function applyDefaults(config) {
|
|
45
29
|
const out = { ...(config || {}) };
|
|
46
|
-
out.quiet = { ...CONFIG_DEFAULTS.quiet, ...(out.quiet || {}) };
|
|
47
|
-
out.quiet.holidays = normalizeHolidaySetting(out.quiet.holidays);
|
|
48
|
-
out.schedules = { ...CONFIG_DEFAULTS.schedules, ...(out.schedules || {}) };
|
|
49
30
|
out.webhook = { ...CONFIG_DEFAULTS.webhook, ...(out.webhook || {}) };
|
|
50
31
|
return out;
|
|
51
32
|
}
|
|
@@ -57,126 +38,31 @@ function channelIdForBackend(entry = {}, backend = "discord") {
|
|
|
57
38
|
return String(entry?.discordChannelId || (entry?.telegramChatId ? "" : entry?.channelId) || "");
|
|
58
39
|
}
|
|
59
40
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
* `cfg` accepts exactly two shapes (top-level config with `.quiet`
|
|
75
|
-
* subtree, or a flat `{ schedule, holidays }` descriptor); anything
|
|
76
|
-
* else returns `false`.
|
|
77
|
-
*
|
|
78
|
-
* `tz` is an optional IANA timezone. When set, both the schedule
|
|
79
|
-
* window (HH:MM) and the holiday/weekend day are evaluated in that
|
|
80
|
-
* zone; when absent, the host's local timezone is used (unchanged
|
|
81
|
-
* behavior for callers that omit it, e.g. webhook.mjs).
|
|
82
|
-
*
|
|
83
|
-
* Behavior:
|
|
84
|
-
* - Schedule window uses "HH:MM-HH:MM". Midnight-crossing windows
|
|
85
|
-
* (start > end, e.g. "23:00-09:00") are honored. When start === end
|
|
86
|
-
* the window is treated as empty/never (preserved doc-ambiguous
|
|
87
|
-
* behavior — callers should avoid that shape).
|
|
88
|
-
* - `holidays` enabled AND today is a recognized holiday => true,
|
|
89
|
-
* regardless of schedule window.
|
|
90
|
-
* - `holidays === false` or missing => holidays ignored.
|
|
91
|
-
*
|
|
92
|
-
* Holiday detection strategy (0.1.63):
|
|
93
|
-
* Sync, non-blocking lookup via holidays.mjs `isHolidaySync`, backed
|
|
94
|
-
* by a pre-warmed in-memory cache (Nager-sourced). The `holidays`
|
|
95
|
-
* value doubles as the ISO country code (`true` => default country).
|
|
96
|
-
* - cache WARM => real public-holiday data (weekday holidays count).
|
|
97
|
-
* - cache COLD => documented fallback: weekend-only (Sat/Sun), while
|
|
98
|
-
* an async warm is kicked off for the next evaluation. No blocking
|
|
99
|
-
* I/O and no heuristics beyond this weekend fallback.
|
|
100
|
-
*
|
|
101
|
-
* Exported so scheduler.mjs and webhook.mjs can share one implementation.
|
|
102
|
-
*/
|
|
103
|
-
// Day-of-week and HH:MM for `now`, in `tz` when given, else host-local.
|
|
104
|
-
// An invalid IANA `tz` makes `Intl.DateTimeFormat` throw a RangeError;
|
|
105
|
-
// we catch it once and fall back to host-local rather than propagating.
|
|
106
|
-
function quietDayTime(now, tz) {
|
|
107
|
-
if (tz) {
|
|
108
|
-
try {
|
|
109
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
110
|
-
hour12: false, timeZone: tz,
|
|
111
|
-
hour: "2-digit", minute: "2-digit", weekday: "short",
|
|
112
|
-
}).formatToParts(now).reduce((acc, p) => { acc[p.type] = p.value; return acc; }, {});
|
|
113
|
-
const hour = parts.hour === "24" ? "00" : parts.hour;
|
|
114
|
-
const dowMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
115
|
-
return {
|
|
116
|
-
dow: dowMap[parts.weekday] ?? now.getDay(),
|
|
117
|
-
hhmm: `${hour}:${parts.minute}`,
|
|
118
|
-
};
|
|
119
|
-
} catch {
|
|
120
|
-
// Invalid timezone — fall through to host-local.
|
|
41
|
+
// Resolve the single active-backend channel id from the config's `channel`
|
|
42
|
+
// section. Backward compat (read-side only, no on-disk migration): when
|
|
43
|
+
// `channel` is absent, fall back to the legacy `channelsConfig[mainChannel]`
|
|
44
|
+
// entry (or the first entry that carries an id).
|
|
45
|
+
function resolveChannelId(raw = {}, backend = "discord") {
|
|
46
|
+
const channel = raw.channel && typeof raw.channel === "object" ? raw.channel : null;
|
|
47
|
+
if (channel) return channelIdForBackend(channel, backend);
|
|
48
|
+
const legacy = raw.channelsConfig && typeof raw.channelsConfig === "object" ? raw.channelsConfig : null;
|
|
49
|
+
if (legacy) {
|
|
50
|
+
const mainName = raw.mainChannel ?? "main";
|
|
51
|
+
const preferred = legacy[mainName];
|
|
52
|
+
if (preferred && typeof preferred === "object") {
|
|
53
|
+
const id = channelIdForBackend(preferred, backend);
|
|
54
|
+
if (id) return id;
|
|
121
55
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
// Return the target calendar day as a plain 'YYYY-MM-DD' string, in `tz`
|
|
129
|
-
// when given (else host-local). A string (not a Date) keeps the holiday
|
|
130
|
-
// contract symmetric: isHolidaySync compares the same string, so there
|
|
131
|
-
// is no UTC<->local round-trip to shift the day on UTC+12..+14 hosts.
|
|
132
|
-
// An invalid IANA `tz` throws a RangeError; catch once, fall to local.
|
|
133
|
-
function quietHolidayDateStr(now, tz) {
|
|
134
|
-
if (tz) {
|
|
135
|
-
try {
|
|
136
|
-
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
137
|
-
timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit",
|
|
138
|
-
}).formatToParts(now).reduce((acc, p) => { acc[p.type] = p.value; return acc; }, {});
|
|
139
|
-
return `${parts.year}-${parts.month}-${parts.day}`;
|
|
140
|
-
} catch {
|
|
141
|
-
// Invalid timezone — fall through to host-local.
|
|
56
|
+
for (const entry of Object.values(legacy)) {
|
|
57
|
+
if (entry && typeof entry === "object") {
|
|
58
|
+
const id = channelIdForBackend(entry, backend);
|
|
59
|
+
if (id) return id;
|
|
60
|
+
}
|
|
142
61
|
}
|
|
143
62
|
}
|
|
144
|
-
return
|
|
63
|
+
return "";
|
|
145
64
|
}
|
|
146
|
-
function isInQuietWindow(cfg, now = new Date(), tz = null) {
|
|
147
|
-
if (!cfg || typeof cfg !== "object") return false;
|
|
148
|
-
// Auto-detect shape: prefer cfg.quiet when present, else treat cfg as
|
|
149
|
-
// the flat { schedule, holidays } descriptor.
|
|
150
|
-
const quiet = cfg.quiet && typeof cfg.quiet === "object" ? cfg.quiet : cfg;
|
|
151
|
-
const schedule = quiet.schedule;
|
|
152
|
-
const holidaysOn = quiet.holidays === true || (typeof quiet.holidays === "string" && !!quiet.holidays.trim());
|
|
153
|
-
const country = quiet.holidays === true
|
|
154
|
-
? DEFAULT_HOLIDAY_COUNTRY
|
|
155
|
-
: (typeof quiet.holidays === "string" ? quiet.holidays.trim().toUpperCase() : null);
|
|
156
|
-
|
|
157
|
-
// Holiday branch: toggle on AND today qualifies => quiet regardless of window.
|
|
158
|
-
// Sync, pre-warmed cache lookup; cold cache falls back to weekend-only.
|
|
159
|
-
if (holidaysOn) {
|
|
160
|
-
const holiday = isHolidaySync(quietHolidayDateStr(now, tz), country);
|
|
161
|
-
if (holiday === true) return true;
|
|
162
|
-
if (holiday === null) {
|
|
163
|
-
// Cold cache: documented Sat/Sun fallback until the async warm lands.
|
|
164
|
-
const { dow } = quietDayTime(now, tz);
|
|
165
|
-
if (dow === 0 || dow === 6) return true;
|
|
166
|
-
}
|
|
167
|
-
// Warm cache + not a holiday: fall through to the schedule window.
|
|
168
|
-
}
|
|
169
65
|
|
|
170
|
-
// Schedule window check (TZ-aware when tz is set; host-local otherwise).
|
|
171
|
-
if (!schedule || typeof schedule !== "string") return false;
|
|
172
|
-
const parts = schedule.split("-");
|
|
173
|
-
if (parts.length !== 2) return false;
|
|
174
|
-
const [start, end] = parts;
|
|
175
|
-
// start === end => empty window (never matches); documented caveat.
|
|
176
|
-
const { hhmm } = quietDayTime(now, tz);
|
|
177
|
-
if (start > end) return hhmm >= start || hhmm < end; // midnight-crossing
|
|
178
|
-
return hhmm >= start && hhmm < end;
|
|
179
|
-
}
|
|
180
66
|
function loadConfig() {
|
|
181
67
|
try {
|
|
182
68
|
let raw = readSection("channels");
|
|
@@ -207,12 +93,16 @@ function loadConfig() {
|
|
|
207
93
|
// Anything else falls back to the discord default.
|
|
208
94
|
const backend = raw.backend === "telegram" ? "telegram" : "discord";
|
|
209
95
|
const telegramToken = getTelegramToken();
|
|
210
|
-
const
|
|
96
|
+
const channelId = resolveChannelId(raw, backend);
|
|
97
|
+
// Drop legacy multi-channel keys from the returned shape; the runtime
|
|
98
|
+
// reads only the resolved `channelId` now (read-side compat lives in
|
|
99
|
+
// resolveChannelId — no on-disk migration).
|
|
100
|
+
const { channelsConfig: _legacyChannels, mainChannel: _legacyMain, ...rawRest } = raw;
|
|
211
101
|
return applyDefaults({
|
|
212
102
|
...DEFAULT_CONFIG,
|
|
213
|
-
...
|
|
103
|
+
...rawRest,
|
|
214
104
|
backend,
|
|
215
|
-
|
|
105
|
+
channelId,
|
|
216
106
|
discord: { ...DEFAULT_CONFIG.discord, ...(({ token: _, ...rest }) => rest)(raw.discord || {}), ...(discordToken && !discordTokenProblem ? { token: discordToken } : {}) },
|
|
217
107
|
// Merge the keychain-resolved telegram token (harmless when backend is
|
|
218
108
|
// discord; the secret never lands in the on-disk config either way).
|
|
@@ -235,7 +125,7 @@ function loadConfig() {
|
|
|
235
125
|
updateSection("channels", () => DEFAULT_CONFIG);
|
|
236
126
|
process.stderr.write(
|
|
237
127
|
`mixdog: default channels config created in ${MIXDOG_CONFIG_PATH}
|
|
238
|
-
edit discord.token and
|
|
128
|
+
edit discord.token and channel.channelId to connect.
|
|
239
129
|
`
|
|
240
130
|
);
|
|
241
131
|
return applyDefaults(DEFAULT_CONFIG);
|
|
@@ -290,9 +180,9 @@ function createBackend(config) {
|
|
|
290
180
|
configPath: CONFIG_FILE,
|
|
291
181
|
access: config.access,
|
|
292
182
|
// Single-source channel setup: the main chat is auto-allowed inside
|
|
293
|
-
// TelegramBackend.loadAccess() so a configured
|
|
183
|
+
// TelegramBackend.loadAccess() so a configured channel.channelId
|
|
294
184
|
// is enough for both inbound gating and outbound.
|
|
295
|
-
mainChannelId: config.
|
|
185
|
+
mainChannelId: config.channelId
|
|
296
186
|
}, tgStateDir);
|
|
297
187
|
}
|
|
298
188
|
const discordToken = getDiscordToken();
|
|
@@ -311,9 +201,9 @@ function createBackend(config) {
|
|
|
311
201
|
configPath: CONFIG_FILE,
|
|
312
202
|
access: config.access,
|
|
313
203
|
// Single-source channel setup: the main channel is auto-allowed inside
|
|
314
|
-
// DiscordBackend.loadAccess() so a configured
|
|
204
|
+
// DiscordBackend.loadAccess() so a configured channel.channelId
|
|
315
205
|
// is enough for both inbound and outbound — no separate access.channels entry.
|
|
316
|
-
mainChannelId: config.
|
|
206
|
+
mainChannelId: config.channelId
|
|
317
207
|
}, stateDir);
|
|
318
208
|
}
|
|
319
209
|
const PROFILE_FILE = join(DATA_DIR, "profile.json");
|
|
@@ -326,11 +216,9 @@ function loadProfileConfig() {
|
|
|
326
216
|
}
|
|
327
217
|
export {
|
|
328
218
|
DATA_DIR,
|
|
329
|
-
DEFAULT_HOLIDAY_COUNTRY,
|
|
330
219
|
createBackend,
|
|
331
220
|
getDiscordToken,
|
|
332
221
|
getTelegramToken,
|
|
333
|
-
isInQuietWindow,
|
|
334
222
|
loadConfig,
|
|
335
223
|
loadProfileConfig
|
|
336
224
|
};
|
|
@@ -3,11 +3,11 @@ import { applyParser, evaluateFilter, applyTemplate } from "./executor.mjs";
|
|
|
3
3
|
class EventPipeline {
|
|
4
4
|
rules;
|
|
5
5
|
queue;
|
|
6
|
-
constructor(config,
|
|
6
|
+
constructor(config, channelId) {
|
|
7
7
|
const rawRules = config?.rules;
|
|
8
8
|
this.rules = (Array.isArray(rawRules) ? rawRules : [])
|
|
9
9
|
.filter((r) => r && typeof r === "object" && r.enabled !== false);
|
|
10
|
-
this.queue = new EventQueue(config?.queue,
|
|
10
|
+
this.queue = new EventQueue(config?.queue, channelId);
|
|
11
11
|
}
|
|
12
12
|
getQueue() {
|
|
13
13
|
return this.queue;
|
|
@@ -18,11 +18,11 @@ class EventPipeline {
|
|
|
18
18
|
stop() {
|
|
19
19
|
this.queue.stop();
|
|
20
20
|
}
|
|
21
|
-
reloadConfig(config,
|
|
21
|
+
reloadConfig(config, channelId) {
|
|
22
22
|
const rawRules = config?.rules;
|
|
23
23
|
this.rules = (Array.isArray(rawRules) ? rawRules : [])
|
|
24
24
|
.filter((r) => r && typeof r === "object" && r.enabled !== false);
|
|
25
|
-
this.queue.reloadConfig(config?.queue,
|
|
25
|
+
this.queue.reloadConfig(config?.queue, channelId);
|
|
26
26
|
}
|
|
27
27
|
// ── Source: webhook ───────────────────────────────────────────────
|
|
28
28
|
/** Handle an incoming webhook event */
|
|
@@ -38,7 +38,7 @@ function finiteInt(value, { min, max, def }) {
|
|
|
38
38
|
}
|
|
39
39
|
class EventQueue {
|
|
40
40
|
config;
|
|
41
|
-
|
|
41
|
+
channelId;
|
|
42
42
|
tickTimer = null;
|
|
43
43
|
batchTimer = null;
|
|
44
44
|
_processQueueRunning = false;
|
|
@@ -54,9 +54,9 @@ class EventQueue {
|
|
|
54
54
|
// Counter is 8-digit zero-padded so lex sort matches numeric order.
|
|
55
55
|
enqueueSeq = 0;
|
|
56
56
|
// track files already notified during active state
|
|
57
|
-
constructor(config,
|
|
57
|
+
constructor(config, channelId) {
|
|
58
58
|
this.config = config ?? {};
|
|
59
|
-
this.
|
|
59
|
+
this.channelId = channelId ?? "";
|
|
60
60
|
}
|
|
61
61
|
setInjectHandler(fn) {
|
|
62
62
|
this.injectFn = fn;
|
|
@@ -101,10 +101,10 @@ class EventQueue {
|
|
|
101
101
|
this.initialTickTimer = null;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
reloadConfig(config,
|
|
104
|
+
reloadConfig(config, channelId) {
|
|
105
105
|
this.stop();
|
|
106
106
|
this.config = config ?? {};
|
|
107
|
-
this.
|
|
107
|
+
this.channelId = channelId ?? "";
|
|
108
108
|
this.start();
|
|
109
109
|
}
|
|
110
110
|
// ── Enqueue ───────────────────────────────────────────────────────
|
|
@@ -392,10 +392,16 @@ ${p.item.prompt}`).join("\n\n")}`;
|
|
|
392
392
|
return false;
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
// The frontmatter `channel` key is a boolean-ish flag: any non-empty value
|
|
396
|
+
// except "false" means post to the single main channel; absent → inject.
|
|
397
|
+
// A pure-digit / snowflake value is honored verbatim as an explicit id
|
|
398
|
+
// override (matches resolveWebhookChannelId in index.mjs).
|
|
399
|
+
resolveChannel(flag) {
|
|
400
|
+
if (flag == null) return "";
|
|
401
|
+
const v = String(flag).trim();
|
|
402
|
+
if (v === "" || v.toLowerCase() === "false") return "";
|
|
403
|
+
if (/^-?\d+$/.test(v)) return v;
|
|
404
|
+
return this.channelId ?? "";
|
|
399
405
|
}
|
|
400
406
|
/** Remove items from queue — after processing, dismissal, or any resolution */
|
|
401
407
|
resolveItems(name, status = "done") {
|
|
@@ -59,52 +59,15 @@ function createInboundRouting({ getConfig, getInstanceId, getChannelOwnerPath })
|
|
|
59
59
|
|
|
60
60
|
function resolveInboundRoute(chatId, parentChatId) {
|
|
61
61
|
const config = getConfig();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!id || !config.channelsConfig) return null;
|
|
65
|
-
if (typeof main === "object" && main !== null && main.channelId === id) {
|
|
66
|
-
return { label: "main", entry: main };
|
|
67
|
-
}
|
|
68
|
-
for (const [label, entry] of Object.entries(config.channelsConfig)) {
|
|
69
|
-
if (typeof entry === "object" && entry !== null && entry.channelId === id) {
|
|
70
|
-
return { label, entry };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
};
|
|
75
|
-
// Prefer a direct channelsConfig match on the thread/channel id; fall back
|
|
76
|
-
// to the parent channel id so thread messages inherit the parent's label
|
|
77
|
-
// and mode (e.g. monitor) instead of being routed as untagged interactive.
|
|
78
|
-
const direct = findEntry(chatId);
|
|
79
|
-
if (direct) {
|
|
80
|
-
const mode = direct.entry.mode === "monitor" ? "monitor" : (direct.entry.mode || "interactive");
|
|
81
|
-
return { targetChatId: chatId, sourceChatId: chatId, sourceLabel: direct.label, sourceMode: mode };
|
|
82
|
-
}
|
|
83
|
-
if (parentChatId) {
|
|
84
|
-
const viaParent = findEntry(parentChatId);
|
|
85
|
-
if (viaParent) {
|
|
86
|
-
const mode = viaParent.entry.mode === "monitor" ? "monitor" : (viaParent.entry.mode || "interactive");
|
|
87
|
-
return { targetChatId: chatId, sourceChatId: parentChatId, sourceLabel: viaParent.label, sourceMode: mode };
|
|
88
|
-
}
|
|
89
|
-
}
|
|
62
|
+
// Single main channel: there is no per-channel label/mode map anymore.
|
|
63
|
+
// Every inbound message routes as interactive to its own chat id.
|
|
90
64
|
return { targetChatId: chatId, sourceChatId: chatId, sourceLabel: undefined, sourceMode: "interactive" };
|
|
91
65
|
}
|
|
92
66
|
|
|
93
|
-
// ── Reverse-lookup channelId → human label from channelsConfig ──────────────
|
|
94
|
-
function labelForChannelId(channelId) {
|
|
95
|
-
const config = getConfig();
|
|
96
|
-
if (!channelId || !config.channelsConfig) return channelId;
|
|
97
|
-
for (const [label, entry] of Object.entries(config.channelsConfig)) {
|
|
98
|
-
if (entry?.channelId === channelId) return label;
|
|
99
|
-
}
|
|
100
|
-
return channelId;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
67
|
return {
|
|
104
68
|
writeChannelOwner,
|
|
105
69
|
shouldDropDuplicateInbound,
|
|
106
70
|
resolveInboundRoute,
|
|
107
|
-
labelForChannelId,
|
|
108
71
|
};
|
|
109
72
|
}
|
|
110
73
|
|
|
@@ -22,28 +22,15 @@ class PendingInteractionStore {
|
|
|
22
22
|
this.patch(userId, channelId, { _msgId: messageId });
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
function getPendingSelectUpdate(customId, values) {
|
|
26
|
-
const value = values?.[0];
|
|
27
|
-
if (!value) return null;
|
|
28
|
-
const scheduleMatch = customId.match(/^sched_(add|edit)_(period|exec|mode)$/);
|
|
29
|
-
if (scheduleMatch) {
|
|
30
|
-
return { [scheduleMatch[2]]: value };
|
|
31
|
-
}
|
|
32
|
-
if (customId === "quiet_holidays_select") {
|
|
33
|
-
return { holidays: value };
|
|
34
|
-
}
|
|
35
|
-
if (customId === "activity_mode_select") {
|
|
36
|
-
return { activityMode: value };
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
25
|
function buildModalRequestSpec(customId, pending, profile) {
|
|
41
26
|
switch (customId) {
|
|
42
27
|
case "sched_add_next": {
|
|
43
28
|
const fields = [
|
|
44
29
|
{ id: "name", label: "Name", required: true },
|
|
45
30
|
{ id: "time", label: "Cron expression (e.g. 0 9 * * 1-5)", required: true },
|
|
46
|
-
|
|
31
|
+
// Single main channel: `channel` is a boolean-ish flag (true → post
|
|
32
|
+
// to the main channel, empty/false → inject into the Lead session).
|
|
33
|
+
{ id: "channel", label: "Post to channel? (true / false)", required: false, value: "true" }
|
|
47
34
|
];
|
|
48
35
|
if (pending.exec?.includes("script")) {
|
|
49
36
|
fields.push({ id: "script", label: "Script filename", required: true });
|
|
@@ -54,19 +41,10 @@ function buildModalRequestSpec(customId, pending, profile) {
|
|
|
54
41
|
fields
|
|
55
42
|
};
|
|
56
43
|
}
|
|
57
|
-
case "quiet_set_next":
|
|
58
|
-
return {
|
|
59
|
-
customId: "modal_quiet",
|
|
60
|
-
title: "Quiet Hours",
|
|
61
|
-
fields: [
|
|
62
|
-
{ id: "schedule", label: "Schedule quiet hours (e.g. 23:00-07:00)", required: false }
|
|
63
|
-
]
|
|
64
|
-
};
|
|
65
44
|
case "sched_edit_next": {
|
|
66
45
|
const fields = [
|
|
67
46
|
{ id: "time", label: "Cron expression (e.g. 0 9 * * 1-5)", required: false },
|
|
68
|
-
{ id: "channel", label: "
|
|
69
|
-
{ id: "dnd", label: "Quiet hours (e.g. 23:00-07:00, leave empty to disable)", required: false }
|
|
47
|
+
{ id: "channel", label: "Post to channel? (true / false)", required: false }
|
|
70
48
|
];
|
|
71
49
|
if (pending.exec?.includes("script")) {
|
|
72
50
|
fields.push({ id: "script", label: "Script filename", required: false });
|
|
@@ -77,15 +55,6 @@ function buildModalRequestSpec(customId, pending, profile) {
|
|
|
77
55
|
fields
|
|
78
56
|
};
|
|
79
57
|
}
|
|
80
|
-
case "activity_add_next":
|
|
81
|
-
return {
|
|
82
|
-
customId: "modal_activity_add",
|
|
83
|
-
title: "Add Activity Channel",
|
|
84
|
-
fields: [
|
|
85
|
-
{ id: "name", label: "Channel Name", required: true },
|
|
86
|
-
{ id: "id", label: "Channel ID", required: true }
|
|
87
|
-
]
|
|
88
|
-
};
|
|
89
58
|
case "profile_edit":
|
|
90
59
|
return {
|
|
91
60
|
customId: "modal_profile_edit",
|
|
@@ -101,84 +70,7 @@ function buildModalRequestSpec(customId, pending, profile) {
|
|
|
101
70
|
return null;
|
|
102
71
|
}
|
|
103
72
|
}
|
|
104
|
-
function buildModalExecutionPlan(customId, pending, fields) {
|
|
105
|
-
switch (customId) {
|
|
106
|
-
case "modal_sched_add": {
|
|
107
|
-
const params = {
|
|
108
|
-
time: fields.time,
|
|
109
|
-
channel: fields.channel || "general",
|
|
110
|
-
mode: pending.mode || "non-interactive",
|
|
111
|
-
period: pending.period || "daily",
|
|
112
|
-
exec: pending.exec || "prompt"
|
|
113
|
-
};
|
|
114
|
-
if (fields.script) params.script = fields.script;
|
|
115
|
-
return {
|
|
116
|
-
commands: [{ target: "bot", args: ["schedule", "add", fields.name], params }],
|
|
117
|
-
followup: { target: "bot", args: ["schedule", "list"], params: {} }
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
case "modal_quiet": {
|
|
121
|
-
const commands = [];
|
|
122
|
-
if (fields.schedule) commands.push({ target: "bot", args: ["quiet", "schedule", fields.schedule], params: {} });
|
|
123
|
-
if (pending.holidays && pending.holidays !== "none") {
|
|
124
|
-
commands.push({ target: "bot", args: ["quiet", "holidays", pending.holidays], params: {} });
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
commands,
|
|
128
|
-
followup: { target: "bot", args: ["quiet", "list"], params: {} }
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
case "modal_sched_edit": {
|
|
132
|
-
const name = pending.editName;
|
|
133
|
-
if (!name) return null;
|
|
134
|
-
const params = {};
|
|
135
|
-
if (fields.time) params.time = fields.time;
|
|
136
|
-
if (fields.channel) params.channel = fields.channel;
|
|
137
|
-
if (pending.period) params.period = pending.period;
|
|
138
|
-
if (pending.exec) params.exec = pending.exec;
|
|
139
|
-
if (pending.mode) params.mode = pending.mode;
|
|
140
|
-
if (fields.script) params.script = fields.script;
|
|
141
|
-
const commands = [
|
|
142
|
-
{ target: "bot", args: ["schedule", "edit", name], params }
|
|
143
|
-
];
|
|
144
|
-
if (fields.dnd) {
|
|
145
|
-
commands.push({ target: "bot", args: ["quiet", "schedule", fields.dnd], params: {} });
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
commands,
|
|
149
|
-
followup: { target: "bot", args: ["schedule", "detail", name], params: {} }
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
case "modal_activity_add":
|
|
153
|
-
return {
|
|
154
|
-
commands: [{
|
|
155
|
-
target: "bot",
|
|
156
|
-
args: ["activity", "add", fields.name],
|
|
157
|
-
params: {
|
|
158
|
-
id: fields.id,
|
|
159
|
-
mode: pending.activityMode || "interactive"
|
|
160
|
-
}
|
|
161
|
-
}],
|
|
162
|
-
followup: { target: "bot", args: ["activity", "list"], params: {} }
|
|
163
|
-
};
|
|
164
|
-
case "modal_profile_edit": {
|
|
165
|
-
const params = {};
|
|
166
|
-
if (fields.name) params.name = fields.name;
|
|
167
|
-
if (fields.role) params.role = fields.role;
|
|
168
|
-
if (fields.lang) params.lang = fields.lang;
|
|
169
|
-
if (fields.tone) params.tone = fields.tone;
|
|
170
|
-
return {
|
|
171
|
-
commands: Object.keys(params).length > 0 ? [{ target: "profile", args: ["set"], params }] : [],
|
|
172
|
-
followup: { target: "bot", args: ["profile", "list"], params: {} }
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
default:
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
73
|
export {
|
|
180
74
|
PendingInteractionStore,
|
|
181
|
-
|
|
182
|
-
buildModalRequestSpec,
|
|
183
|
-
getPendingSelectUpdate
|
|
75
|
+
buildModalRequestSpec
|
|
184
76
|
};
|