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.
Files changed (121) hide show
  1. package/README.md +112 -38
  2. package/package.json +4 -1
  3. package/scripts/bench/r5-orchestrated-task.json +7 -0
  4. package/scripts/bench/round-r13-clientmeta.json +464 -0
  5. package/scripts/bench/round-r14-betafeatures.json +466 -0
  6. package/scripts/bench/round-r15-fulldefault.json +462 -0
  7. package/scripts/bench/round-r16-sessionid.json +466 -0
  8. package/scripts/bench/round-r17-wirebytes.json +456 -0
  9. package/scripts/bench/round-r18-prewarm.json +468 -0
  10. package/scripts/bench/round-r19-clean.json +472 -0
  11. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  12. package/scripts/bench/round-r21-delta-retry.json +473 -0
  13. package/scripts/bench/round-r22-full-probe.json +693 -0
  14. package/scripts/bench/round-r23-itemprobe.json +701 -0
  15. package/scripts/bench/round-r24-shapefix.json +677 -0
  16. package/scripts/bench/round-r25-serial.json +464 -0
  17. package/scripts/bench/round-r26-parallel3.json +671 -0
  18. package/scripts/bench/round-r27-parallel10.json +894 -0
  19. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  20. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  21. package/scripts/bench/round-r30-instid.json +253 -0
  22. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  23. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  24. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  25. package/scripts/bench/round-r34-orchestrated.json +120 -0
  26. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  27. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  28. package/scripts/bench-run.mjs +40 -7
  29. package/scripts/internal-comms-bench.mjs +2 -4
  30. package/scripts/model-catalog-audit.mjs +209 -0
  31. package/scripts/model-list-sanitize-test.mjs +37 -0
  32. package/scripts/recall-bench.mjs +76 -13
  33. package/scripts/recall-quality-cases.json +12 -0
  34. package/scripts/tool-smoke.mjs +2 -2
  35. package/src/agents/heavy-worker/AGENT.md +7 -7
  36. package/src/agents/scheduler-task/AGENT.md +2 -3
  37. package/src/agents/webhook-handler/AGENT.md +2 -3
  38. package/src/agents/worker/AGENT.md +9 -10
  39. package/src/app.mjs +12 -1
  40. package/src/headless-role.mjs +7 -1
  41. package/src/mixdog-session-runtime.mjs +15 -14
  42. package/src/output-styles/default.md +14 -18
  43. package/src/output-styles/minimal.md +4 -5
  44. package/src/output-styles/simple.md +7 -8
  45. package/src/rules/agent/00-common.md +6 -5
  46. package/src/rules/agent/30-explorer.md +16 -5
  47. package/src/rules/lead/01-general.md +5 -5
  48. package/src/rules/lead/lead-brief.md +6 -3
  49. package/src/rules/lead/lead-tool.md +6 -4
  50. package/src/rules/shared/01-tool.md +17 -21
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  52. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +19 -0
  53. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -3
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +9 -1
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +53 -9
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +23 -10
  57. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +33 -4
  59. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  60. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +93 -0
  61. package/src/runtime/agent/orchestrator/session/loop.mjs +27 -5
  62. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
  65. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  66. package/src/runtime/channels/backends/discord-gateway.mjs +2 -2
  67. package/src/runtime/channels/backends/discord.mjs +15 -26
  68. package/src/runtime/channels/backends/telegram.mjs +8 -12
  69. package/src/runtime/channels/index.mjs +23 -43
  70. package/src/runtime/channels/lib/backend-dispatch.mjs +5 -3
  71. package/src/runtime/channels/lib/config.mjs +37 -149
  72. package/src/runtime/channels/lib/event-pipeline.mjs +4 -4
  73. package/src/runtime/channels/lib/event-queue.mjs +15 -9
  74. package/src/runtime/channels/lib/inbound-routing.mjs +2 -39
  75. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  76. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  77. package/src/runtime/channels/lib/webhook/deliveries.mjs +3 -2
  78. package/src/runtime/channels/lib/webhook.mjs +8 -47
  79. package/src/runtime/memory/index.mjs +9 -0
  80. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  81. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  82. package/src/runtime/memory/lib/memory-recall-store.mjs +47 -10
  83. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  84. package/src/runtime/memory/lib/query-handlers.mjs +27 -5
  85. package/src/runtime/memory/tool-defs.mjs +2 -2
  86. package/src/runtime/shared/config.mjs +14 -4
  87. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  88. package/src/runtime/shared/schedules-store.mjs +13 -3
  89. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  90. package/src/session-runtime/config-helpers.mjs +45 -10
  91. package/src/session-runtime/quick-model-rows.mjs +46 -11
  92. package/src/session-runtime/quick-search-models.mjs +28 -27
  93. package/src/session-runtime/settings-api.mjs +34 -1
  94. package/src/session-runtime/tool-defs.mjs +1 -1
  95. package/src/session-runtime/workflow.mjs +1 -1
  96. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  97. package/src/standalone/agent-tool.mjs +30 -0
  98. package/src/standalone/channel-admin.mjs +102 -90
  99. package/src/standalone/explore-tool.mjs +39 -10
  100. package/src/tui/App.jsx +126 -28
  101. package/src/tui/app/app-format.mjs +11 -4
  102. package/src/tui/app/channel-pickers.mjs +2 -4
  103. package/src/tui/app/maintenance-pickers.mjs +60 -28
  104. package/src/tui/app/model-options.mjs +5 -1
  105. package/src/tui/app/settings-picker.mjs +0 -1
  106. package/src/tui/app/transcript-window.mjs +8 -2
  107. package/src/tui/app/use-transcript-scroll.mjs +3 -1
  108. package/src/tui/app/use-transcript-window.mjs +19 -1
  109. package/src/tui/components/ConfirmBar.jsx +10 -7
  110. package/src/tui/components/Picker.jsx +32 -11
  111. package/src/tui/components/PromptInput.jsx +10 -1
  112. package/src/tui/components/StatusLine.jsx +6 -3
  113. package/src/tui/dist/index.mjs +440 -293
  114. package/src/tui/engine.mjs +48 -12
  115. package/src/tui/index.jsx +11 -52
  116. package/src/ui/statusline.mjs +10 -0
  117. package/src/workflows/default/WORKFLOW.md +15 -17
  118. package/src/workflows/sequential/WORKFLOW.md +15 -17
  119. package/vendor/ink/build/ink.js +9 -1
  120. package/scripts/recall-usecase-probe.json +0 -6
  121. 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
- mainChannel: "main",
22
- channelsConfig: {
23
- main: { channelId: "", mode: "interactive" }
24
- }
20
+ channel: { channelId: "" }
25
21
  };
26
- // Shared defaults layer (DND / per-channel respectQuiet). Merge
27
- // semantics: user values win; defaults only fill missing fields.
28
- // Helper is exported so the setup UI and runtime both produce the same
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
- quiet: { schedule: "23:00-09:00", holidays: false },
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
- function channelsConfigForBackend(rawChannelsConfig = {}, backend = "discord") {
61
- return Object.fromEntries(Object.entries(rawChannelsConfig || {}).map(([name, entry]) => {
62
- const value = entry && typeof entry === "object" ? entry : {};
63
- const channelId = channelIdForBackend(value, backend);
64
- return [name, { ...value, channelId }];
65
- }));
66
- }
67
-
68
- /**
69
- * Shared DND / quiet-window helper used by scheduler + webhook.
70
- *
71
- * Signature contract (as of 0.1.62):
72
- * isInQuietWindow(cfg, now = new Date(), tz = null) -> boolean
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
- return {
124
- dow: now.getDay(),
125
- hhmm: `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`,
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 `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
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 rawChannelsConfig = { ...DEFAULT_CONFIG.channelsConfig, ...(raw.channelsConfig || {}) };
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
- ...raw,
103
+ ...rawRest,
214
104
  backend,
215
- channelsConfig: channelsConfigForBackend(rawChannelsConfig, backend),
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 channelsConfig.main.channelId to connect.
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 channelsConfig.main.channelId
183
+ // TelegramBackend.loadAccess() so a configured channel.channelId
294
184
  // is enough for both inbound gating and outbound.
295
- mainChannelId: config.channelsConfig?.main?.channelId
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 channelsConfig.main.channelId
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.channelsConfig?.main?.channelId
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, channelsConfig) {
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, channelsConfig);
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, channelsConfig) {
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, channelsConfig);
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
- channelsConfig;
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, channelsConfig) {
57
+ constructor(config, channelId) {
58
58
  this.config = config ?? {};
59
- this.channelsConfig = channelsConfig ?? null;
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, channelsConfig) {
104
+ reloadConfig(config, channelId) {
105
105
  this.stop();
106
106
  this.config = config ?? {};
107
- this.channelsConfig = channelsConfig ?? null;
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
- resolveChannel(label) {
396
- if (!label || !this.channelsConfig) return "";
397
- const entry = this.channelsConfig?.[label];
398
- return entry?.channelId ?? label;
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
- const main = config.channelsConfig?.main;
63
- const findEntry = (id) => {
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
- { id: "channel", label: "Channel", required: false, value: "general" }
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: "Channel", required: false },
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
- buildModalExecutionPlan,
182
- buildModalRequestSpec,
183
- getPendingSelectUpdate
75
+ buildModalRequestSpec
184
76
  };