moltbot-termux 2026.1.27-1 → 2026.1.27-2-pre

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 (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/agents/channel-tools.js +31 -2
  3. package/dist/agents/models-config.providers.js +4 -4
  4. package/dist/agents/transcript-policy.js +2 -1
  5. package/dist/build-info.json +3 -3
  6. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  7. package/dist/cli/banner.js +6 -6
  8. package/dist/commands/onboard-helpers.js +6 -6
  9. package/dist/config/paths.js +3 -0
  10. package/dist/discord/send.outbound.js +4 -4
  11. package/dist/discord/send.shared.js +30 -1
  12. package/dist/discord/targets.js +66 -0
  13. package/dist/telegram/bot/helpers.js +13 -2
  14. package/dist/telegram/bot-message-context.js +4 -2
  15. package/dist/telegram/bot-native-commands.js +13 -8
  16. package/dist/telegram/bot.js +7 -5
  17. package/dist/telegram/monitor.js +19 -1
  18. package/dist/telegram/network-errors.js +4 -0
  19. package/docs/concepts/model-providers.md +4 -3
  20. package/docs/gateway/configuration.md +5 -5
  21. package/docs/providers/moonshot.md +13 -2
  22. package/package.json +3 -3
  23. package/skills/bitwarden/SKILL.md +101 -0
  24. package/skills/bitwarden/references/templates.md +116 -0
  25. package/skills/bitwarden/scripts/bw-session.sh +33 -0
  26. package/extensions/googlechat/node_modules/.bin/clawdbot +0 -21
  27. package/extensions/googlechat/node_modules/.bin/moltbot +0 -21
  28. package/extensions/line/node_modules/.bin/clawdbot +0 -21
  29. package/extensions/line/node_modules/.bin/moltbot +0 -21
  30. package/extensions/memory-core/node_modules/.bin/clawdbot +0 -21
  31. package/extensions/memory-core/node_modules/.bin/moltbot +0 -21
  32. package/extensions/msteams/node_modules/.bin/clawdbot +0 -21
  33. package/extensions/msteams/node_modules/.bin/moltbot +0 -21
  34. package/extensions/nostr/node_modules/.bin/clawdbot +0 -21
  35. package/extensions/nostr/node_modules/.bin/moltbot +0 -21
  36. package/extensions/twitch/node_modules/.bin/clawdbot +0 -21
  37. package/extensions/twitch/node_modules/.bin/moltbot +0 -21
  38. package/extensions/zalo/node_modules/.bin/clawdbot +0 -21
  39. package/extensions/zalo/node_modules/.bin/moltbot +0 -21
  40. package/extensions/zalouser/node_modules/.bin/clawdbot +0 -21
  41. package/extensions/zalouser/node_modules/.bin/moltbot +0 -21
package/CHANGELOG.md CHANGED
@@ -77,6 +77,13 @@ Status: beta.
77
77
  - macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee.
78
78
  - Web UI: auto-expand the chat compose textarea while typing (with sensible max height). (#2950) Thanks @shivamraut101.
79
79
  - Gateway: prevent crashes on transient network errors (fetch failures, timeouts, DNS). Added fatal error detection to only exit on truly critical errors. Fixes #2895, #2879, #2873. (#2980) Thanks @elliotsecops.
80
+ - Agents: guard channel tool listActions to avoid plugin crashes. (#2859) Thanks @mbelinky.
81
+ - Discord: avoid resolving bare channel names to user DMs when a username matches. Thanks @thewilloftheshadow.
82
+ - Discord: fix directory config type import for target resolution. Thanks @thewilloftheshadow.
83
+ - Providers: update MiniMax API endpoint and compatibility mode. (#3064) Thanks @hlbbbbbbb.
84
+ - Telegram: treat more network errors as recoverable in polling. (#3013) Thanks @ryancontent.
85
+ - Discord: resolve usernames to user IDs for outbound messages. (#2649) Thanks @nonggialiang.
86
+ - Providers: update Moonshot Kimi model references to kimi-k2.5. (#2762) Thanks @MarvinCui.
80
87
  - Gateway: suppress AbortError and transient network errors in unhandled rejections. (#2451) Thanks @Glucksberg.
81
88
  - TTS: keep /tts status replies on text-only commands and avoid duplicate block-stream audio. (#2451) Thanks @Glucksberg.
82
89
  - Security: pin npm overrides to keep tar@7.5.4 for install toolchains.
@@ -90,6 +97,7 @@ Status: beta.
90
97
  - Agents: release session locks on process termination and cover more signals. (#2483) Thanks @janeexai.
91
98
  - Agents: skip cooldowned providers during model failover. (#2143) Thanks @YiWang24.
92
99
  - Telegram: harden polling + retry behavior for transient network errors and Node 22 transport issues. (#2420) Thanks @techboss.
100
+ - Telegram: ignore non-forum group message_thread_id while preserving DM thread sessions. (#2731) Thanks @dylanneve1.
93
101
  - Telegram: wrap reasoning italics per line to avoid raw underscores. (#2181) Thanks @YuriNachos.
94
102
  - Telegram: centralize API error logging for delivery and bot calls. (#2492) Thanks @altryne.
95
103
  - Voice Call: enforce Twilio webhook signature verification for ngrok URLs; disable ngrok free tier bypass by default.
@@ -1,6 +1,7 @@
1
1
  import { getChannelDock } from "../channels/dock.js";
2
2
  import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
3
3
  import { normalizeAnyChannelId } from "../channels/registry.js";
4
+ import { defaultRuntime } from "../runtime.js";
4
5
  /**
5
6
  * Get the list of supported message actions for a specific channel.
6
7
  * Returns an empty array if channel is not found or has no actions configured.
@@ -12,7 +13,7 @@ export function listChannelSupportedActions(params) {
12
13
  if (!plugin?.actions?.listActions)
13
14
  return [];
14
15
  const cfg = params.cfg ?? {};
15
- return plugin.actions.listActions({ cfg });
16
+ return runPluginListActions(plugin, cfg);
16
17
  }
17
18
  /**
18
19
  * Get the list of all supported message actions across all configured channels.
@@ -23,7 +24,7 @@ export function listAllChannelSupportedActions(params) {
23
24
  if (!plugin.actions?.listActions)
24
25
  continue;
25
26
  const cfg = params.cfg ?? {};
26
- const channelActions = plugin.actions.listActions({ cfg });
27
+ const channelActions = runPluginListActions(plugin, cfg);
27
28
  for (const action of channelActions) {
28
29
  actions.add(action);
29
30
  }
@@ -56,3 +57,31 @@ export function resolveChannelMessageToolHints(params) {
56
57
  .map((entry) => entry.trim())
57
58
  .filter(Boolean);
58
59
  }
60
+ const loggedListActionErrors = new Set();
61
+ function runPluginListActions(plugin, cfg) {
62
+ if (!plugin.actions?.listActions)
63
+ return [];
64
+ try {
65
+ const listed = plugin.actions.listActions({ cfg });
66
+ return Array.isArray(listed) ? listed : [];
67
+ }
68
+ catch (err) {
69
+ logListActionsError(plugin.id, err);
70
+ return [];
71
+ }
72
+ }
73
+ function logListActionsError(pluginId, err) {
74
+ const message = err instanceof Error ? err.message : String(err);
75
+ const key = `${pluginId}:${message}`;
76
+ if (loggedListActionErrors.has(key))
77
+ return;
78
+ loggedListActionErrors.add(key);
79
+ const stack = err instanceof Error && err.stack ? err.stack : null;
80
+ const details = stack ?? message;
81
+ defaultRuntime.error?.(`[channel-tools] ${pluginId}.actions.listActions failed: ${details}`);
82
+ }
83
+ export const __testing = {
84
+ resetLoggedListActionErrors() {
85
+ loggedListActionErrors.clear();
86
+ },
87
+ };
@@ -4,7 +4,7 @@ import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
4
4
  import { discoverBedrockModels } from "./bedrock-discovery.js";
5
5
  import { buildSyntheticModelDefinition, SYNTHETIC_BASE_URL, SYNTHETIC_MODEL_CATALOG, } from "./synthetic-models.js";
6
6
  import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js";
7
- const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic";
7
+ const MINIMAX_API_BASE_URL = "https://api.minimax.chat/v1";
8
8
  const MINIMAX_DEFAULT_MODEL_ID = "MiniMax-M2.1";
9
9
  const MINIMAX_DEFAULT_VISION_MODEL_ID = "MiniMax-VL-01";
10
10
  const MINIMAX_DEFAULT_CONTEXT_WINDOW = 200000;
@@ -17,7 +17,7 @@ const MINIMAX_API_COST = {
17
17
  cacheWrite: 10,
18
18
  };
19
19
  const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
20
- const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2-0905-preview";
20
+ const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2.5";
21
21
  const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
22
22
  const MOONSHOT_DEFAULT_MAX_TOKENS = 8192;
23
23
  const MOONSHOT_DEFAULT_COST = {
@@ -198,7 +198,7 @@ export function normalizeProviders(params) {
198
198
  function buildMinimaxProvider() {
199
199
  return {
200
200
  baseUrl: MINIMAX_API_BASE_URL,
201
- api: "anthropic-messages",
201
+ api: "openai-completions",
202
202
  models: [
203
203
  {
204
204
  id: MINIMAX_DEFAULT_MODEL_ID,
@@ -228,7 +228,7 @@ function buildMoonshotProvider() {
228
228
  models: [
229
229
  {
230
230
  id: MOONSHOT_DEFAULT_MODEL_ID,
231
- name: "Kimi K2 0905 Preview",
231
+ name: "Kimi K2.5",
232
232
  reasoning: false,
233
233
  input: ["text"],
234
234
  cost: MOONSHOT_DEFAULT_COST,
@@ -30,7 +30,8 @@ function isAnthropicApi(modelApi, provider) {
30
30
  if (modelApi === "anthropic-messages")
31
31
  return true;
32
32
  const normalized = normalizeProviderId(provider ?? "");
33
- return normalized === "anthropic" || normalized === "minimax";
33
+ // MiniMax now uses openai-completions API, not anthropic-messages
34
+ return normalized === "anthropic";
34
35
  }
35
36
  function isMistralModel(params) {
36
37
  const provider = normalizeProviderId(params.provider ?? "");
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.1.27-1",
3
- "commit": "0c9dde1181e14a0b0da113dc119ba7d5a3977ece",
4
- "builtAt": "2026-01-28T05:08:31.899Z"
2
+ "version": "2026.1.27-2-pre",
3
+ "commit": "ea53cbc3cebd63f309cc698a7de6d7c1c83d8811",
4
+ "builtAt": "2026-01-28T06:28:40.764Z"
5
5
  }
@@ -1 +1 @@
1
- 78705dc5edb010dd13a5225641862dd1580e2058b2ff024d9be6e2ec011f80e5
1
+ 5ab92d64997cf901fe504836ff46635580f54cac27a47cefee42eb50bfced85f
@@ -46,12 +46,12 @@ export function formatCliBannerLine(version, options = {}) {
46
46
  return `${line1}\n${line2}`;
47
47
  }
48
48
  const LOBSTER_ASCII = [
49
- "░████░█░░░░░█████░█░░░█░███░░████░░████░░▀█▀",
50
- "█░░░░░█░░░░░█░░░█░█░█░█░█░░█░█░░░█░█░░░█░░█░",
51
- "█░░░░░█░░░░░█████░█░█░█░█░░█░████░░█░░░█░░█░",
52
- "█░░░░░█░░░░░█░░░█░█░█░█░█░░█░█░░█░░█░░░█░░█░",
53
- "░████░█████░█░░░█░░█░█░░███░░████░░░███░░░█░",
54
- " 🦞 FRESH DAILY 🦞",
49
+ "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
50
+ "██░▄▀▄░██░▄▄▄░██░████▄▄░▄▄██░▄▄▀██░▄▄▄░█▄▄░▄▄██",
51
+ "██░█░█░██░███░██░██████░████░▄▄▀██░███░███░████",
52
+ "██░███░██░▀▀▀░██░▀▀░███░████░▀▀░██░▀▀▀░███░████",
53
+ "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
54
+ " 🦞 FRESH DAILY 🦞 ",
55
55
  ];
56
56
  export function formatCliBannerArt(options = {}) {
57
57
  const rich = options.richTty ?? isRich();
@@ -53,12 +53,12 @@ export function randomToken() {
53
53
  }
54
54
  export function printWizardHeader(runtime) {
55
55
  const header = [
56
- "░████░█░░░░░█████░█░░░█░███░░████░░████░░▀█▀",
57
- "█░░░░░█░░░░░█░░░█░█░█░█░█░░█░█░░░█░█░░░█░░█░",
58
- "█░░░░░█░░░░░█████░█░█░█░█░░█░████░░█░░░█░░█░",
59
- "█░░░░░█░░░░░█░░░█░█░█░█░█░░█░█░░█░░█░░░█░░█░",
60
- "░████░█████░█░░░█░░█░█░░███░░████░░░███░░░█░",
61
- " 🦞 FRESH DAILY 🦞",
56
+ "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
57
+ "██░▄▀▄░██░▄▄▄░██░████▄▄░▄▄██░▄▄▀██░▄▄▄░█▄▄░▄▄██",
58
+ "██░█░█░██░███░██░██████░████░▄▄▀██░███░███░████",
59
+ "██░███░██░▀▀▀░██░▀▀░███░████░▀▀░██░▀▀▀░███░████",
60
+ "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
61
+ " 🦞 FRESH DAILY 🦞 ",
62
62
  ].join("\n");
63
63
  runtime.log(header);
64
64
  }
@@ -93,6 +93,7 @@ export function resolveConfigPath(env = process.env, stateDir = resolveStateDir(
93
93
  const override = env.MOLTBOT_CONFIG_PATH?.trim() || env.CLAWDBOT_CONFIG_PATH?.trim();
94
94
  if (override)
95
95
  return resolveUserPath(override);
96
+ const stateOverride = env.MOLTBOT_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
96
97
  const candidates = [
97
98
  path.join(stateDir, CONFIG_FILENAME),
98
99
  path.join(stateDir, LEGACY_CONFIG_FILENAME),
@@ -107,6 +108,8 @@ export function resolveConfigPath(env = process.env, stateDir = resolveStateDir(
107
108
  });
108
109
  if (existing)
109
110
  return existing;
111
+ if (stateOverride)
112
+ return path.join(stateDir, CONFIG_FILENAME);
110
113
  const defaultStateDir = resolveStateDir(env, homedir);
111
114
  if (path.resolve(stateDir) === path.resolve(defaultStateDir)) {
112
115
  return resolveConfigPathCandidate(env, homedir);
@@ -5,7 +5,7 @@ import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
5
5
  import { recordChannelActivity } from "../infra/channel-activity.js";
6
6
  import { convertMarkdownTables } from "../markdown/tables.js";
7
7
  import { resolveDiscordAccount } from "./accounts.js";
8
- import { buildDiscordSendError, createDiscordClient, normalizeDiscordPollInput, normalizeStickerIds, parseRecipient, resolveChannelId, sendDiscordMedia, sendDiscordText, } from "./send.shared.js";
8
+ import { buildDiscordSendError, createDiscordClient, normalizeDiscordPollInput, normalizeStickerIds, parseAndResolveRecipient, resolveChannelId, sendDiscordMedia, sendDiscordText, } from "./send.shared.js";
9
9
  export async function sendMessageDiscord(to, text, opts = {}) {
10
10
  const cfg = loadConfig();
11
11
  const accountInfo = resolveDiscordAccount({
@@ -20,7 +20,7 @@ export async function sendMessageDiscord(to, text, opts = {}) {
20
20
  const chunkMode = resolveChunkMode(cfg, "discord", accountInfo.accountId);
21
21
  const textWithTables = convertMarkdownTables(text ?? "", tableMode);
22
22
  const { token, rest, request } = createDiscordClient(opts, cfg);
23
- const recipient = parseRecipient(to);
23
+ const recipient = await parseAndResolveRecipient(to, opts.accountId);
24
24
  const { channelId } = await resolveChannelId(rest, recipient, request);
25
25
  let result;
26
26
  try {
@@ -52,7 +52,7 @@ export async function sendMessageDiscord(to, text, opts = {}) {
52
52
  export async function sendStickerDiscord(to, stickerIds, opts = {}) {
53
53
  const cfg = loadConfig();
54
54
  const { rest, request } = createDiscordClient(opts, cfg);
55
- const recipient = parseRecipient(to);
55
+ const recipient = await parseAndResolveRecipient(to, opts.accountId);
56
56
  const { channelId } = await resolveChannelId(rest, recipient, request);
57
57
  const content = opts.content?.trim();
58
58
  const stickers = normalizeStickerIds(stickerIds);
@@ -70,7 +70,7 @@ export async function sendStickerDiscord(to, stickerIds, opts = {}) {
70
70
  export async function sendPollDiscord(to, poll, opts = {}) {
71
71
  const cfg = loadConfig();
72
72
  const { rest, request } = createDiscordClient(opts, cfg);
73
- const recipient = parseRecipient(to);
73
+ const recipient = await parseAndResolveRecipient(to, opts.accountId);
74
74
  const { channelId } = await resolveChannelId(rest, recipient, request);
75
75
  const content = opts.content?.trim();
76
76
  const payload = normalizeDiscordPollInput(poll);
@@ -9,7 +9,7 @@ import { resolveDiscordAccount } from "./accounts.js";
9
9
  import { chunkDiscordTextWithMode } from "./chunk.js";
10
10
  import { fetchChannelPermissionsDiscord, isThreadChannelType } from "./send.permissions.js";
11
11
  import { DiscordSendError } from "./send.types.js";
12
- import { parseDiscordTarget } from "./targets.js";
12
+ import { parseDiscordTarget, resolveDiscordTarget } from "./targets.js";
13
13
  import { normalizeDiscordToken } from "./token.js";
14
14
  const DISCORD_TEXT_LIMIT = 2000;
15
15
  const DISCORD_MAX_STICKERS = 3;
@@ -68,6 +68,35 @@ function parseRecipient(raw) {
68
68
  }
69
69
  return { kind: target.kind, id: target.id };
70
70
  }
71
+ /**
72
+ * Parse and resolve Discord recipient, including username lookup.
73
+ * This enables sending DMs by username (e.g., "john.doe") by querying
74
+ * the Discord directory to resolve usernames to user IDs.
75
+ *
76
+ * @param raw - The recipient string (username, ID, or known format)
77
+ * @param accountId - Discord account ID to use for directory lookup
78
+ * @returns Parsed DiscordRecipient with resolved user ID if applicable
79
+ */
80
+ export async function parseAndResolveRecipient(raw, accountId) {
81
+ const cfg = loadConfig();
82
+ const accountInfo = resolveDiscordAccount({ cfg, accountId });
83
+ // First try to resolve using directory lookup (handles usernames)
84
+ const resolved = await resolveDiscordTarget(raw, {
85
+ cfg,
86
+ accountId: accountInfo.accountId,
87
+ });
88
+ if (resolved) {
89
+ return { kind: resolved.kind, id: resolved.id };
90
+ }
91
+ // Fallback to standard parsing (for channels, etc.)
92
+ const parsed = parseDiscordTarget(raw, {
93
+ ambiguousMessage: `Ambiguous Discord recipient "${raw.trim()}". Use "user:${raw.trim()}" for DMs or "channel:${raw.trim()}" for channel messages.`,
94
+ });
95
+ if (!parsed) {
96
+ throw new Error("Recipient is required for Discord sends");
97
+ }
98
+ return { kind: parsed.kind, id: parsed.id };
99
+ }
71
100
  function normalizeStickerIds(raw) {
72
101
  const ids = raw.map((entry) => entry.trim()).filter(Boolean);
73
102
  if (ids.length === 0) {
@@ -1,4 +1,5 @@
1
1
  import { buildMessagingTarget, ensureTargetId, requireTargetKind, } from "../channels/targets.js";
2
+ import { listDiscordDirectoryPeersLive } from "./directory-live.js";
2
3
  export function parseDiscordTarget(raw, options = {}) {
3
4
  const trimmed = raw.trim();
4
5
  if (!trimmed)
@@ -41,3 +42,68 @@ export function resolveDiscordChannelId(raw) {
41
42
  const target = parseDiscordTarget(raw, { defaultKind: "channel" });
42
43
  return requireTargetKind({ platform: "Discord", target, kind: "channel" });
43
44
  }
45
+ /**
46
+ * Resolve a Discord username to user ID using the directory lookup.
47
+ * This enables sending DMs by username instead of requiring explicit user IDs.
48
+ *
49
+ * @param raw - The username or raw target string (e.g., "john.doe")
50
+ * @param options - Directory configuration params (cfg, accountId, limit)
51
+ * @param parseOptions - Optional parsing options (defaultKind, ambiguousMessage)
52
+ * @returns Parsed MessagingTarget with user ID, or undefined if not found
53
+ */
54
+ export async function resolveDiscordTarget(raw, options, parseOptions = {}) {
55
+ const trimmed = raw.trim();
56
+ if (!trimmed)
57
+ return undefined;
58
+ const shouldLookup = isExplicitUserLookup(trimmed, parseOptions);
59
+ const directParse = safeParseDiscordTarget(trimmed, parseOptions);
60
+ if (directParse && directParse.kind !== "channel") {
61
+ return directParse;
62
+ }
63
+ if (!shouldLookup) {
64
+ return directParse ?? parseDiscordTarget(trimmed, parseOptions);
65
+ }
66
+ // Try to resolve as a username via directory lookup
67
+ try {
68
+ const directoryEntries = await listDiscordDirectoryPeersLive({
69
+ ...options,
70
+ query: trimmed,
71
+ limit: 1,
72
+ });
73
+ const match = directoryEntries[0];
74
+ if (match && match.kind === "user") {
75
+ // Extract user ID from the directory entry (format: "user:<id>")
76
+ const userId = match.id.replace(/^user:/, "");
77
+ return buildMessagingTarget("user", userId, trimmed);
78
+ }
79
+ }
80
+ catch (error) {
81
+ // Directory lookup failed - fall through to parse as-is
82
+ // This preserves existing behavior for channel names
83
+ }
84
+ // Fallback to original parsing (for channels, etc.)
85
+ return parseDiscordTarget(trimmed, parseOptions);
86
+ }
87
+ function safeParseDiscordTarget(input, options) {
88
+ try {
89
+ return parseDiscordTarget(input, options);
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
95
+ function isExplicitUserLookup(input, options) {
96
+ if (/^<@!?(\d+)>$/.test(input)) {
97
+ return true;
98
+ }
99
+ if (/^(user:|discord:)/.test(input)) {
100
+ return true;
101
+ }
102
+ if (input.startsWith("@")) {
103
+ return true;
104
+ }
105
+ if (/^\d+$/.test(input)) {
106
+ return options.defaultKind === "user";
107
+ }
108
+ return false;
109
+ }
@@ -1,10 +1,21 @@
1
1
  import { formatLocationText } from "../../channels/location.js";
2
2
  const TELEGRAM_GENERAL_TOPIC_ID = 1;
3
+ /**
4
+ * Resolve the thread ID for Telegram forum topics.
5
+ * For non-forum groups, returns undefined even if messageThreadId is present
6
+ * (reply threads in regular groups should not create separate sessions).
7
+ * For forum groups, returns the topic ID (or General topic ID=1 if unspecified).
8
+ */
3
9
  export function resolveTelegramForumThreadId(params) {
4
- if (params.isForum && params.messageThreadId == null) {
10
+ // Non-forum groups: ignore message_thread_id (reply threads are not real topics)
11
+ if (!params.isForum) {
12
+ return undefined;
13
+ }
14
+ // Forum groups: use the topic ID, defaulting to General topic
15
+ if (params.messageThreadId == null) {
5
16
  return TELEGRAM_GENERAL_TOPIC_ID;
6
17
  }
7
- return params.messageThreadId ?? undefined;
18
+ return params.messageThreadId;
8
19
  }
9
20
  /**
10
21
  * Build thread params for Telegram API calls (messages, media).
@@ -66,7 +66,8 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
66
66
  },
67
67
  });
68
68
  const baseSessionKey = route.sessionKey;
69
- const dmThreadId = !isGroup ? resolvedThreadId : undefined;
69
+ // DMs: use raw messageThreadId for thread sessions (not resolvedThreadId which is for forums)
70
+ const dmThreadId = !isGroup ? messageThreadId : undefined;
70
71
  const threadKeys = dmThreadId != null
71
72
  ? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
72
73
  : null;
@@ -438,7 +439,8 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
438
439
  Sticker: allMedia[0]?.stickerMetadata,
439
440
  ...(locationData ? toLocationContext(locationData) : undefined),
440
441
  CommandAuthorized: commandAuthorized,
441
- MessageThreadId: resolvedThreadId,
442
+ // For groups: use resolvedThreadId (forum topics only); for DMs: use raw messageThreadId
443
+ MessageThreadId: isGroup ? resolvedThreadId : messageThreadId,
442
444
  IsForum: isForum,
443
445
  // Originating channel for reply routing.
444
446
  OriginatingChannel: "telegram",
@@ -189,7 +189,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
189
189
  ...customCommands,
190
190
  ];
191
191
  if (allCommands.length > 0) {
192
- void withTelegramApiErrorLogging({
192
+ withTelegramApiErrorLogging({
193
193
  operation: "setMyCommands",
194
194
  runtime,
195
195
  fn: () => bot.api.setMyCommands(allCommands),
@@ -220,6 +220,8 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
220
220
  if (!auth)
221
221
  return;
222
222
  const { chatId, isGroup, isForum, resolvedThreadId, senderId, senderUsername, groupConfig, topicConfig, commandAuthorized, } = auth;
223
+ const messageThreadId = msg.message_thread_id;
224
+ const threadIdForSend = isGroup ? resolvedThreadId : messageThreadId;
223
225
  const commandDefinition = findCommandByNativeName(command.name, "telegram");
224
226
  const rawText = ctx.match?.trim() ?? "";
225
227
  const commandArgs = commandDefinition
@@ -261,7 +263,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
261
263
  runtime,
262
264
  fn: () => bot.api.sendMessage(chatId, title, {
263
265
  ...(replyMarkup ? { reply_markup: replyMarkup } : {}),
264
- ...(resolvedThreadId != null ? { message_thread_id: resolvedThreadId } : {}),
266
+ ...(threadIdForSend != null ? { message_thread_id: threadIdForSend } : {}),
265
267
  }),
266
268
  });
267
269
  return;
@@ -276,7 +278,8 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
276
278
  },
277
279
  });
278
280
  const baseSessionKey = route.sessionKey;
279
- const dmThreadId = !isGroup ? resolvedThreadId : undefined;
281
+ // DMs: use raw messageThreadId for thread sessions (not resolvedThreadId which is for forums)
282
+ const dmThreadId = !isGroup ? messageThreadId : undefined;
280
283
  const threadKeys = dmThreadId != null
281
284
  ? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
282
285
  : null;
@@ -319,7 +322,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
319
322
  CommandSource: "native",
320
323
  SessionKey: `telegram:slash:${senderId || chatId}`,
321
324
  CommandTargetSessionKey: sessionKey,
322
- MessageThreadId: resolvedThreadId,
325
+ MessageThreadId: threadIdForSend,
323
326
  IsForum: isForum,
324
327
  // Originating context for sub-agent announce routing
325
328
  OriginatingChannel: "telegram",
@@ -343,7 +346,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
343
346
  bot,
344
347
  replyToMode,
345
348
  textLimit,
346
- messageThreadId: resolvedThreadId,
349
+ messageThreadId: threadIdForSend,
347
350
  tableMode,
348
351
  chunkMode,
349
352
  linkPreview: telegramCfg.linkPreview,
@@ -393,7 +396,9 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
393
396
  });
394
397
  if (!auth)
395
398
  return;
396
- const { resolvedThreadId, senderId, commandAuthorized } = auth;
399
+ const { resolvedThreadId, senderId, commandAuthorized, isGroup } = auth;
400
+ const messageThreadId = msg.message_thread_id;
401
+ const threadIdForSend = isGroup ? resolvedThreadId : messageThreadId;
397
402
  const result = await executePluginCommand({
398
403
  command: match.command,
399
404
  args: match.args,
@@ -417,7 +422,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
417
422
  bot,
418
423
  replyToMode,
419
424
  textLimit,
420
- messageThreadId: resolvedThreadId,
425
+ messageThreadId: threadIdForSend,
421
426
  tableMode,
422
427
  chunkMode,
423
428
  linkPreview: telegramCfg.linkPreview,
@@ -427,7 +432,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
427
432
  }
428
433
  }
429
434
  else if (nativeDisabledExplicit) {
430
- void withTelegramApiErrorLogging({
435
+ withTelegramApiErrorLogging({
431
436
  operation: "setMyCommands",
432
437
  runtime,
433
438
  fn: () => bot.api.setMyCommands([]),
@@ -45,11 +45,12 @@ export function getTelegramSequentialKey(ctx) {
45
45
  return `telegram:${chatId}:control`;
46
46
  return "telegram:control";
47
47
  }
48
+ const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup";
49
+ const messageThreadId = msg?.message_thread_id;
48
50
  const isForum = msg?.chat?.is_forum;
49
- const threadId = resolveTelegramForumThreadId({
50
- isForum,
51
- messageThreadId: msg?.message_thread_id,
52
- });
51
+ const threadId = isGroup
52
+ ? resolveTelegramForumThreadId({ isForum, messageThreadId })
53
+ : messageThreadId;
53
54
  if (typeof chatId === "number") {
54
55
  return threadId != null ? `telegram:${chatId}:topic:${threadId}` : `telegram:${chatId}`;
55
56
  }
@@ -357,7 +358,8 @@ export function createTelegramBot(opts) {
357
358
  peer: { kind: isGroup ? "group" : "dm", id: peerId },
358
359
  });
359
360
  const baseSessionKey = route.sessionKey;
360
- const dmThreadId = !isGroup ? resolvedThreadId : undefined;
361
+ // DMs: use raw messageThreadId for thread sessions (not resolvedThreadId which is for forums)
362
+ const dmThreadId = !isGroup ? messageThreadId : undefined;
361
363
  const threadKeys = dmThreadId != null
362
364
  ? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
363
365
  : null;
@@ -50,6 +50,23 @@ const isGetUpdatesConflict = (err) => {
50
50
  .toLowerCase();
51
51
  return haystack.includes("getupdates");
52
52
  };
53
+ const NETWORK_ERROR_SNIPPETS = [
54
+ "fetch failed",
55
+ "network",
56
+ "timeout",
57
+ "socket",
58
+ "econnreset",
59
+ "econnrefused",
60
+ "undici",
61
+ ];
62
+ const isNetworkRelatedError = (err) => {
63
+ if (!err)
64
+ return false;
65
+ const message = formatErrorMessage(err).toLowerCase();
66
+ if (!message)
67
+ return false;
68
+ return NETWORK_ERROR_SNIPPETS.some((snippet) => message.includes(snippet));
69
+ };
53
70
  export async function monitorTelegramProvider(opts = {}) {
54
71
  const cfg = opts.config ?? loadConfig();
55
72
  const account = resolveTelegramAccount({
@@ -126,7 +143,8 @@ export async function monitorTelegramProvider(opts = {}) {
126
143
  }
127
144
  const isConflict = isGetUpdatesConflict(err);
128
145
  const isRecoverable = isRecoverableTelegramNetworkError(err, { context: "polling" });
129
- if (!isConflict && !isRecoverable) {
146
+ const isNetworkError = isNetworkRelatedError(err);
147
+ if (!isConflict && !isRecoverable && !isNetworkError) {
130
148
  throw err;
131
149
  }
132
150
  restartAttempts += 1;
@@ -14,6 +14,8 @@ const RECOVERABLE_ERROR_CODES = new Set([
14
14
  "UND_ERR_BODY_TIMEOUT",
15
15
  "UND_ERR_SOCKET",
16
16
  "UND_ERR_ABORTED",
17
+ "ECONNABORTED",
18
+ "ERR_NETWORK",
17
19
  ]);
18
20
  const RECOVERABLE_ERROR_NAMES = new Set([
19
21
  "AbortError",
@@ -24,6 +26,8 @@ const RECOVERABLE_ERROR_NAMES = new Set([
24
26
  ]);
25
27
  const RECOVERABLE_MESSAGE_SNIPPETS = [
26
28
  "fetch failed",
29
+ "typeerror: fetch failed",
30
+ "undici",
27
31
  "network error",
28
32
  "network request",
29
33
  "client network socket disconnected",
@@ -130,9 +130,10 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
130
130
 
131
131
  - Provider: `moonshot`
132
132
  - Auth: `MOONSHOT_API_KEY`
133
- - Example model: `moonshot/kimi-k2-0905-preview`
133
+ - Example model: `moonshot/kimi-k2.5`
134
134
  - Kimi K2 model IDs:
135
135
  {/* moonshot-kimi-k2-model-refs:start */}
136
+ - `moonshot/kimi-k2.5`
136
137
  - `moonshot/kimi-k2-0905-preview`
137
138
  - `moonshot/kimi-k2-turbo-preview`
138
139
  - `moonshot/kimi-k2-thinking`
@@ -141,7 +142,7 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
141
142
  ```json5
142
143
  {
143
144
  agents: {
144
- defaults: { model: { primary: "moonshot/kimi-k2-0905-preview" } }
145
+ defaults: { model: { primary: "moonshot/kimi-k2.5" } }
145
146
  },
146
147
  models: {
147
148
  mode: "merge",
@@ -150,7 +151,7 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
150
151
  baseUrl: "https://api.moonshot.ai/v1",
151
152
  apiKey: "${MOONSHOT_API_KEY}",
152
153
  api: "openai-completions",
153
- models: [{ id: "kimi-k2-0905-preview", name: "Kimi K2 0905 Preview" }]
154
+ models: [{ id: "kimi-k2.5", name: "Kimi K2.5" }]
154
155
  }
155
156
  }
156
157
  }
@@ -2396,8 +2396,8 @@ Use Moonshot's OpenAI-compatible endpoint:
2396
2396
  env: { MOONSHOT_API_KEY: "sk-..." },
2397
2397
  agents: {
2398
2398
  defaults: {
2399
- model: { primary: "moonshot/kimi-k2-0905-preview" },
2400
- models: { "moonshot/kimi-k2-0905-preview": { alias: "Kimi K2" } }
2399
+ model: { primary: "moonshot/kimi-k2.5" },
2400
+ models: { "moonshot/kimi-k2.5": { alias: "Kimi K2.5" } }
2401
2401
  }
2402
2402
  },
2403
2403
  models: {
@@ -2409,8 +2409,8 @@ Use Moonshot's OpenAI-compatible endpoint:
2409
2409
  api: "openai-completions",
2410
2410
  models: [
2411
2411
  {
2412
- id: "kimi-k2-0905-preview",
2413
- name: "Kimi K2 0905 Preview",
2412
+ id: "kimi-k2.5",
2413
+ name: "Kimi K2.5",
2414
2414
  reasoning: false,
2415
2415
  input: ["text"],
2416
2416
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -2426,7 +2426,7 @@ Use Moonshot's OpenAI-compatible endpoint:
2426
2426
 
2427
2427
  Notes:
2428
2428
  - Set `MOONSHOT_API_KEY` in the environment or use `moltbot onboard --auth-choice moonshot-api-key`.
2429
- - Model ref: `moonshot/kimi-k2-0905-preview`.
2429
+ - Model ref: `moonshot/kimi-k2.5`.
2430
2430
  - Use `https://api.moonshot.cn/v1` if you need the China endpoint.
2431
2431
 
2432
2432
  ### Kimi Code