switchroom 0.14.44 → 0.14.46
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/dist/cli/switchroom.js +144 -23
- package/examples/switchroom.yaml +26 -0
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +69 -16
- package/telegram-plugin/gateway/gateway.ts +79 -10
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +17 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +38 -6
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +84 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -30596,7 +30596,7 @@ function runInlinedSecretChecks(_config, deps = {}) {
|
|
|
30596
30596
|
}
|
|
30597
30597
|
let parsed;
|
|
30598
30598
|
try {
|
|
30599
|
-
parsed =
|
|
30599
|
+
parsed = import_yaml14.parse(raw);
|
|
30600
30600
|
} catch {
|
|
30601
30601
|
return [
|
|
30602
30602
|
{
|
|
@@ -30624,9 +30624,9 @@ function runInlinedSecretChecks(_config, deps = {}) {
|
|
|
30624
30624
|
fix: `Move it to the per-agent-ACL'd vault: \`switchroom vault set <name>\` ` + `then replace the literal with \`vault:<name>\`. Rotate the exposed ` + `value if any agent may already have read it.`
|
|
30625
30625
|
}));
|
|
30626
30626
|
}
|
|
30627
|
-
var
|
|
30627
|
+
var import_yaml14, SECRET_KEY_EXACT;
|
|
30628
30628
|
var init_doctor_inlined_secrets = __esm(() => {
|
|
30629
|
-
|
|
30629
|
+
import_yaml14 = __toESM(require_dist(), 1);
|
|
30630
30630
|
SECRET_KEY_EXACT = new Set([
|
|
30631
30631
|
"bot_token",
|
|
30632
30632
|
"client_secret",
|
|
@@ -49462,8 +49462,8 @@ var {
|
|
|
49462
49462
|
} = import__.default;
|
|
49463
49463
|
|
|
49464
49464
|
// src/build-info.ts
|
|
49465
|
-
var VERSION = "0.14.
|
|
49466
|
-
var COMMIT_SHA = "
|
|
49465
|
+
var VERSION = "0.14.46";
|
|
49466
|
+
var COMMIT_SHA = "df3deacf";
|
|
49467
49467
|
|
|
49468
49468
|
// src/cli/agent.ts
|
|
49469
49469
|
init_source();
|
|
@@ -72684,6 +72684,47 @@ ${childIndent}approvalAuth: ${value}`;
|
|
|
72684
72684
|
return { kind: "rewritten", content: rewritten };
|
|
72685
72685
|
}
|
|
72686
72686
|
|
|
72687
|
+
// src/cli/supergroup-setup-yaml.ts
|
|
72688
|
+
var import_yaml13 = __toESM(require_dist(), 1);
|
|
72689
|
+
function isValidSupergroupChatId(value) {
|
|
72690
|
+
return /^-\d+$/.test(value);
|
|
72691
|
+
}
|
|
72692
|
+
function setAgentSupergroupChatId(yamlText, agentName, chatId) {
|
|
72693
|
+
if (!isValidSupergroupChatId(chatId)) {
|
|
72694
|
+
throw new Error(`setAgentSupergroupChatId: chat_id must be a negative integer string (got "${chatId}")`);
|
|
72695
|
+
}
|
|
72696
|
+
const doc = import_yaml13.parseDocument(yamlText);
|
|
72697
|
+
const root = doc.contents;
|
|
72698
|
+
if (!import_yaml13.isMap(root)) {
|
|
72699
|
+
throw new Error("setAgentSupergroupChatId: YAML root is not a map");
|
|
72700
|
+
}
|
|
72701
|
+
const agents = root.get("agents", true);
|
|
72702
|
+
if (!import_yaml13.isMap(agents)) {
|
|
72703
|
+
throw new Error("setAgentSupergroupChatId: config has no `agents:` map");
|
|
72704
|
+
}
|
|
72705
|
+
const agent = agents.get(agentName, true);
|
|
72706
|
+
if (!import_yaml13.isMap(agent)) {
|
|
72707
|
+
throw new Error(`setAgentSupergroupChatId: agent "${agentName}" not found in config`);
|
|
72708
|
+
}
|
|
72709
|
+
const channels = agent.get("channels", true);
|
|
72710
|
+
if (import_yaml13.isMap(channels)) {
|
|
72711
|
+
const telegram = channels.get("telegram", true);
|
|
72712
|
+
if (import_yaml13.isMap(telegram)) {
|
|
72713
|
+
if (telegram.get("chat_id") === chatId)
|
|
72714
|
+
return yamlText;
|
|
72715
|
+
telegram.set("chat_id", chatId);
|
|
72716
|
+
} else {
|
|
72717
|
+
channels.set("telegram", doc.createNode({ chat_id: chatId }));
|
|
72718
|
+
}
|
|
72719
|
+
} else {
|
|
72720
|
+
agent.set("channels", doc.createNode({ telegram: { chat_id: chatId } }));
|
|
72721
|
+
}
|
|
72722
|
+
const out = String(doc);
|
|
72723
|
+
return out.endsWith(`
|
|
72724
|
+
`) ? out : out + `
|
|
72725
|
+
`;
|
|
72726
|
+
}
|
|
72727
|
+
|
|
72687
72728
|
// src/cli/setup.ts
|
|
72688
72729
|
var STEP_PENDING = source_default.gray("\u25cb");
|
|
72689
72730
|
var STEP_ACTIVE = source_default.blue("->");
|
|
@@ -72711,6 +72752,7 @@ function registerSetupCommand(program3) {
|
|
|
72711
72752
|
if (userId && userId !== "0") {
|
|
72712
72753
|
saveUserConfig(userId);
|
|
72713
72754
|
}
|
|
72755
|
+
await stepSupergroupMode(agentBots, switchroomConfigPath, nonInteractive);
|
|
72714
72756
|
const forumChatId = "0";
|
|
72715
72757
|
await stepCreateTopics(config, botToken, nonInteractive);
|
|
72716
72758
|
await stepMemoryBackend(config, nonInteractive, switchroomConfigPath);
|
|
@@ -72966,6 +73008,59 @@ async function stepDmPairing(agentBots, nonInteractive, userIdFlag) {
|
|
|
72966
73008
|
return { userId: manualId || "0", chatId: 0 };
|
|
72967
73009
|
}
|
|
72968
73010
|
}
|
|
73011
|
+
var SUPERGROUP_DOC = "docs/supergroup-mode.md";
|
|
73012
|
+
async function stepSupergroupMode(agentBots, configPath, nonInteractive) {
|
|
73013
|
+
stepHeader(4, "Supergroup mode (optional)", STEP_ACTIVE);
|
|
73014
|
+
if (nonInteractive) {
|
|
73015
|
+
console.log(source_default.gray(` ${STEP_DONE} Skipped (DM-only default). To have one agent own a forum`));
|
|
73016
|
+
console.log(source_default.gray(` supergroup, see ${SUPERGROUP_DOC}.`));
|
|
73017
|
+
return;
|
|
73018
|
+
}
|
|
73019
|
+
console.log(source_default.gray(" Most setups stay DM-only \u2014 each agent DMs you privately. Supergroup"));
|
|
73020
|
+
console.log(source_default.gray(" mode instead has ONE agent own a Telegram forum supergroup and route"));
|
|
73021
|
+
console.log(source_default.gray(" its replies + automated events into per-topic threads. Opt-in."));
|
|
73022
|
+
const want = await askYesNo(" Configure an agent to own a forum supergroup now?", false);
|
|
73023
|
+
if (!want) {
|
|
73024
|
+
console.log(source_default.gray(` ${STEP_DONE} Staying DM-only. Enable later via ${SUPERGROUP_DOC}.`));
|
|
73025
|
+
return;
|
|
73026
|
+
}
|
|
73027
|
+
const agentNames = Object.keys(agentBots);
|
|
73028
|
+
if (agentNames.length === 0) {
|
|
73029
|
+
console.log(source_default.yellow(` No agents configured yet \u2014 skipping. See ${SUPERGROUP_DOC}.`));
|
|
73030
|
+
return;
|
|
73031
|
+
}
|
|
73032
|
+
const agent = agentNames.length === 1 ? agentNames[0] : await askChoice(" Which agent owns the supergroup?", agentNames);
|
|
73033
|
+
console.log(source_default.gray(" First create the forum supergroup in Telegram (a group with Topics"));
|
|
73034
|
+
console.log(source_default.gray(" enabled) and add the agent's bot as an admin. Its id is the digits in"));
|
|
73035
|
+
console.log(source_default.gray(" a message link t.me/c/<id>/\u2026 written as a negative -100\u2026 number."));
|
|
73036
|
+
const chatId = await ask(" Supergroup chat_id (e.g. -1001234567890, or Enter to skip)");
|
|
73037
|
+
if (!chatId) {
|
|
73038
|
+
console.log(source_default.gray(` ${STEP_DONE} Skipped \u2014 no chat_id entered. See ${SUPERGROUP_DOC}.`));
|
|
73039
|
+
return;
|
|
73040
|
+
}
|
|
73041
|
+
if (!isValidSupergroupChatId(chatId)) {
|
|
73042
|
+
console.log(source_default.yellow(` "${chatId}" isn't a valid supergroup id (need a negative integer like`));
|
|
73043
|
+
console.log(source_default.yellow(` -1001234567890). Add it by hand later \u2014 ${SUPERGROUP_DOC}.`));
|
|
73044
|
+
return;
|
|
73045
|
+
}
|
|
73046
|
+
try {
|
|
73047
|
+
const fs4 = await import("node:fs");
|
|
73048
|
+
const { atomicWriteFileSync: atomicWriteFileSync3 } = await Promise.resolve().then(() => (init_atomic(), exports_atomic));
|
|
73049
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
73050
|
+
const after = setAgentSupergroupChatId(raw, agent, chatId);
|
|
73051
|
+
let mode = 420;
|
|
73052
|
+
try {
|
|
73053
|
+
mode = fs4.statSync(configPath).mode & 511;
|
|
73054
|
+
} catch {}
|
|
73055
|
+
atomicWriteFileSync3(configPath, after, mode);
|
|
73056
|
+
console.log(source_default.green(` ${STEP_DONE} ${agent} now owns supergroup ${chatId} \u2014 replies + ops route to its topics.`));
|
|
73057
|
+
console.log(source_default.gray(" Fallback topic defaults to General (1). Add default_topic_id /"));
|
|
73058
|
+
console.log(source_default.gray(` topic_aliases by hand \u2014 ${SUPERGROUP_DOC}. Run \`switchroom apply\` to activate.`));
|
|
73059
|
+
} catch (err) {
|
|
73060
|
+
console.log(source_default.yellow(` Could not write chat_id: ${err.message}`));
|
|
73061
|
+
console.log(source_default.gray(` Add it by hand \u2014 see ${SUPERGROUP_DOC}.`));
|
|
73062
|
+
}
|
|
73063
|
+
}
|
|
72969
73064
|
async function stepCreateTopics(config, botToken, nonInteractive) {
|
|
72970
73065
|
stepHeader(5, "Create topics", STEP_ACTIVE);
|
|
72971
73066
|
if (config.telegram.forum_chat_id === "0") {
|
|
@@ -73143,16 +73238,16 @@ async function stepScaffoldAgents(config, agentBots, userId, nonInteractive, swi
|
|
|
73143
73238
|
}
|
|
73144
73239
|
async function ensureAuthActiveDefault(configPath) {
|
|
73145
73240
|
const fs4 = await import("node:fs");
|
|
73146
|
-
const { parseDocument:
|
|
73241
|
+
const { parseDocument: parseDocument7, isMap: isMap7 } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
|
|
73147
73242
|
const { atomicWriteFileSync: atomicWriteFileSync3 } = await Promise.resolve().then(() => (init_atomic(), exports_atomic));
|
|
73148
73243
|
const { setAuthActive: setAuthActive2 } = await Promise.resolve().then(() => (init_auth_active_yaml(), exports_auth_active_yaml));
|
|
73149
73244
|
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
73150
|
-
const doc =
|
|
73245
|
+
const doc = parseDocument7(raw);
|
|
73151
73246
|
const root = doc.contents;
|
|
73152
|
-
if (!
|
|
73247
|
+
if (!isMap7(root))
|
|
73153
73248
|
return;
|
|
73154
73249
|
const existing = root.get("auth", true);
|
|
73155
|
-
if (
|
|
73250
|
+
if (isMap7(existing) && existing.has("active"))
|
|
73156
73251
|
return;
|
|
73157
73252
|
const after = setAuthActive2(raw, "default");
|
|
73158
73253
|
if (after === raw)
|
|
@@ -78222,6 +78317,32 @@ agents:
|
|
|
78222
78317
|
# admin: true
|
|
78223
78318
|
# system_prompt_append: |
|
|
78224
78319
|
# You are the fleet admin agent. Always respond concisely.
|
|
78320
|
+
|
|
78321
|
+
# \u2500\u2500 Supergroup mode (advanced, opt-in) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
78322
|
+
# By default every agent DMs you privately. Supergroup mode instead
|
|
78323
|
+
# has ONE agent OWN a Telegram forum supergroup: its replies and its
|
|
78324
|
+
# automated events (boot, alerts, cron, approvals) route into per-
|
|
78325
|
+
# topic threads inside that one group, so a busy agent's work stays
|
|
78326
|
+
# organised by subject instead of flooding a single DM.
|
|
78327
|
+
#
|
|
78328
|
+
# \`switchroom setup\` offers this as an optional step, or add it by
|
|
78329
|
+
# hand below. Setting \`chat_id\` is the minimum \u2014 \`default_topic_id\`
|
|
78330
|
+
# auto-defaults to General (topic 1). Create the forum supergroup in
|
|
78331
|
+
# Telegram first, add the bot as an admin, and find the numeric id
|
|
78332
|
+
# (the digits after t.me/c/ in a message link, written as -100\u2026).
|
|
78333
|
+
# Full guide: docs/supergroup-mode.md.
|
|
78334
|
+
#
|
|
78335
|
+
# social:
|
|
78336
|
+
# topic_name: "Social"
|
|
78337
|
+
# bot_token: "vault:telegram-social-bot-token" # its own bot
|
|
78338
|
+
# channels:
|
|
78339
|
+
# telegram:
|
|
78340
|
+
# chat_id: "-1001234567890" # the forum supergroup this agent owns
|
|
78341
|
+
# default_topic_id: 1 # fallback topic (1 = General); optional
|
|
78342
|
+
# topic_aliases: # name \u2192 topic id, for cron + ops routing
|
|
78343
|
+
# alerts: 12 # boot / watchdog / compaction events
|
|
78344
|
+
# admin: 7 # vault / permission / mutation events
|
|
78345
|
+
# planning: 3
|
|
78225
78346
|
`;
|
|
78226
78347
|
|
|
78227
78348
|
// examples/minimal.yaml
|
|
@@ -79839,7 +79960,7 @@ function registerAgentConfigCommands(program3) {
|
|
|
79839
79960
|
}
|
|
79840
79961
|
|
|
79841
79962
|
// src/cli/agent-config-write.ts
|
|
79842
|
-
var
|
|
79963
|
+
var import_yaml16 = __toESM(require_dist(), 1);
|
|
79843
79964
|
|
|
79844
79965
|
// src/config/overlay-writer.ts
|
|
79845
79966
|
init_paths();
|
|
@@ -80032,7 +80153,7 @@ function filterOverlaySecrets(doc, source) {
|
|
|
80032
80153
|
// src/agents/reconcile-dry-run.ts
|
|
80033
80154
|
init_overlay_schema();
|
|
80034
80155
|
init_schema();
|
|
80035
|
-
var
|
|
80156
|
+
var import_yaml15 = __toESM(require_dist(), 1);
|
|
80036
80157
|
init_lifecycle();
|
|
80037
80158
|
var MIN_CRON_INTERVAL_SECS = 5 * 60;
|
|
80038
80159
|
function violatesMinInterval(cron) {
|
|
@@ -80054,7 +80175,7 @@ function violatesMinInterval(cron) {
|
|
|
80054
80175
|
function dryRunReconcile(input) {
|
|
80055
80176
|
let parsed;
|
|
80056
80177
|
try {
|
|
80057
|
-
parsed =
|
|
80178
|
+
parsed = import_yaml15.parse(input.yamlText);
|
|
80058
80179
|
} catch (err) {
|
|
80059
80180
|
return {
|
|
80060
80181
|
ok: false,
|
|
@@ -80360,7 +80481,7 @@ function scheduleAdd(opts) {
|
|
|
80360
80481
|
]
|
|
80361
80482
|
};
|
|
80362
80483
|
const yamlText = (() => {
|
|
80363
|
-
const body =
|
|
80484
|
+
const body = import_yaml16.stringify(doc);
|
|
80364
80485
|
const header = opts.name ? `# name: ${opts.name}
|
|
80365
80486
|
` : "";
|
|
80366
80487
|
return header + body;
|
|
@@ -80470,7 +80591,7 @@ function scheduleAddOrStage(opts) {
|
|
|
80470
80591
|
]
|
|
80471
80592
|
};
|
|
80472
80593
|
const yamlText = (opts.name ? `# name: ${opts.name}
|
|
80473
|
-
` : "") +
|
|
80594
|
+
` : "") + import_yaml16.stringify(doc);
|
|
80474
80595
|
const summary = (() => {
|
|
80475
80596
|
const parts = [`cron=${opts.cronExpr}`];
|
|
80476
80597
|
if (opts.secrets?.length)
|
|
@@ -80520,7 +80641,7 @@ function scheduleRemove(opts) {
|
|
|
80520
80641
|
break;
|
|
80521
80642
|
}
|
|
80522
80643
|
try {
|
|
80523
|
-
const parsed =
|
|
80644
|
+
const parsed = import_yaml16.parse(e.raw);
|
|
80524
80645
|
if (parsed && parsed.name === opts.name) {
|
|
80525
80646
|
match = e;
|
|
80526
80647
|
break;
|
|
@@ -80730,10 +80851,10 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
80730
80851
|
}
|
|
80731
80852
|
|
|
80732
80853
|
// src/cli/agent-config-skill-write.ts
|
|
80733
|
-
var
|
|
80854
|
+
var import_yaml17 = __toESM(require_dist(), 1);
|
|
80734
80855
|
import { existsSync as existsSync78 } from "node:fs";
|
|
80735
80856
|
init_reconcile_default_skills();
|
|
80736
|
-
var
|
|
80857
|
+
var import_yaml18 = __toESM(require_dist(), 1);
|
|
80737
80858
|
import { join as join75 } from "node:path";
|
|
80738
80859
|
var MAX_SKILLS_PER_AGENT = 20;
|
|
80739
80860
|
var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
|
|
@@ -80783,7 +80904,7 @@ function countCurrentSkills(agent, opts) {
|
|
|
80783
80904
|
let total = 0;
|
|
80784
80905
|
for (const e of entries) {
|
|
80785
80906
|
try {
|
|
80786
|
-
const doc =
|
|
80907
|
+
const doc = import_yaml18.parse(e.raw);
|
|
80787
80908
|
total += (doc?.skills ?? []).length;
|
|
80788
80909
|
} catch {}
|
|
80789
80910
|
}
|
|
@@ -80813,7 +80934,7 @@ function skillInstall(opts) {
|
|
|
80813
80934
|
if (!existsSync78(skillPath)) {
|
|
80814
80935
|
return err("E_SKILL_NOT_FOUND", `bundled skill not found at ${skillPath}. The operator needs to ` + `place the skill at this path before the agent can opt in.`);
|
|
80815
80936
|
}
|
|
80816
|
-
const yamlText =
|
|
80937
|
+
const yamlText = import_yaml17.stringify({ skills: [skillName] });
|
|
80817
80938
|
let path8;
|
|
80818
80939
|
try {
|
|
80819
80940
|
path8 = writeSkillsOverlayEntry(agent, slug, yamlText, { root: opts.root });
|
|
@@ -80992,7 +81113,7 @@ import { dirname as dirname22, join as join76, relative as relative2, resolve as
|
|
|
80992
81113
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
80993
81114
|
|
|
80994
81115
|
// src/cli/skill-common.ts
|
|
80995
|
-
var
|
|
81116
|
+
var import_yaml19 = __toESM(require_dist(), 1);
|
|
80996
81117
|
var MAX_FILE_BYTES = 256 * 1024;
|
|
80997
81118
|
var MAX_SKILL_BYTES = 2 * 1024 * 1024;
|
|
80998
81119
|
var MAX_FILES_PER_SKILL = 50;
|
|
@@ -81096,7 +81217,7 @@ function validateSkillMd(content, expectedName) {
|
|
|
81096
81217
|
}
|
|
81097
81218
|
let parsed;
|
|
81098
81219
|
try {
|
|
81099
|
-
parsed =
|
|
81220
|
+
parsed = import_yaml19.parse(fmText);
|
|
81100
81221
|
} catch (e) {
|
|
81101
81222
|
return authorErr("E_SKILL_INVALID_FRONTMATTER", `frontmatter is not valid YAML: ${e.message}`);
|
|
81102
81223
|
}
|
|
@@ -82135,7 +82256,7 @@ function registerSkillPersonalCommands(program3) {
|
|
|
82135
82256
|
|
|
82136
82257
|
// src/cli/skill-search.ts
|
|
82137
82258
|
init_helpers();
|
|
82138
|
-
var
|
|
82259
|
+
var import_yaml20 = __toESM(require_dist(), 1);
|
|
82139
82260
|
import { existsSync as existsSync81, readdirSync as readdirSync32, readFileSync as readFileSync65, statSync as statSync31 } from "node:fs";
|
|
82140
82261
|
import { homedir as homedir45 } from "node:os";
|
|
82141
82262
|
import { join as join78, resolve as resolve48 } from "node:path";
|
|
@@ -82175,7 +82296,7 @@ function readSkillFrontmatter(skillDir) {
|
|
|
82175
82296
|
const fmText = rest.slice(0, endIdx);
|
|
82176
82297
|
let parsed;
|
|
82177
82298
|
try {
|
|
82178
|
-
parsed =
|
|
82299
|
+
parsed = import_yaml20.parse(fmText);
|
|
82179
82300
|
} catch (e) {
|
|
82180
82301
|
return { error: `yaml parse: ${e.message}` };
|
|
82181
82302
|
}
|
package/examples/switchroom.yaml
CHANGED
|
@@ -285,3 +285,29 @@ agents:
|
|
|
285
285
|
# admin: true
|
|
286
286
|
# system_prompt_append: |
|
|
287
287
|
# You are the fleet admin agent. Always respond concisely.
|
|
288
|
+
|
|
289
|
+
# ── Supergroup mode (advanced, opt-in) ───────────────────────────
|
|
290
|
+
# By default every agent DMs you privately. Supergroup mode instead
|
|
291
|
+
# has ONE agent OWN a Telegram forum supergroup: its replies and its
|
|
292
|
+
# automated events (boot, alerts, cron, approvals) route into per-
|
|
293
|
+
# topic threads inside that one group, so a busy agent's work stays
|
|
294
|
+
# organised by subject instead of flooding a single DM.
|
|
295
|
+
#
|
|
296
|
+
# `switchroom setup` offers this as an optional step, or add it by
|
|
297
|
+
# hand below. Setting `chat_id` is the minimum — `default_topic_id`
|
|
298
|
+
# auto-defaults to General (topic 1). Create the forum supergroup in
|
|
299
|
+
# Telegram first, add the bot as an admin, and find the numeric id
|
|
300
|
+
# (the digits after t.me/c/ in a message link, written as -100…).
|
|
301
|
+
# Full guide: docs/supergroup-mode.md.
|
|
302
|
+
#
|
|
303
|
+
# social:
|
|
304
|
+
# topic_name: "Social"
|
|
305
|
+
# bot_token: "vault:telegram-social-bot-token" # its own bot
|
|
306
|
+
# channels:
|
|
307
|
+
# telegram:
|
|
308
|
+
# chat_id: "-1001234567890" # the forum supergroup this agent owns
|
|
309
|
+
# default_topic_id: 1 # fallback topic (1 = General); optional
|
|
310
|
+
# topic_aliases: # name → topic id, for cron + ops routing
|
|
311
|
+
# alerts: 12 # boot / watchdog / compaction events
|
|
312
|
+
# admin: 7 # vault / permission / mutation events
|
|
313
|
+
# planning: 3
|
package/package.json
CHANGED
|
@@ -47833,6 +47833,7 @@ function buildVaultGrantApprovedInbound(opts) {
|
|
|
47833
47833
|
return {
|
|
47834
47834
|
type: "inbound",
|
|
47835
47835
|
chatId: opts.ctx.chat_id,
|
|
47836
|
+
...opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {},
|
|
47836
47837
|
messageId: ts,
|
|
47837
47838
|
user: "vault-broker",
|
|
47838
47839
|
userId: 0,
|
|
@@ -47841,6 +47842,7 @@ function buildVaultGrantApprovedInbound(opts) {
|
|
|
47841
47842
|
meta: {
|
|
47842
47843
|
source: "vault_grant_approved",
|
|
47843
47844
|
agent: opts.ctx.agent,
|
|
47845
|
+
...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
|
|
47844
47846
|
key: opts.ctx.key,
|
|
47845
47847
|
scope: opts.ctx.scope,
|
|
47846
47848
|
grant_id: opts.grantId,
|
|
@@ -47854,6 +47856,7 @@ function buildVaultGrantDeniedInbound(opts) {
|
|
|
47854
47856
|
return {
|
|
47855
47857
|
type: "inbound",
|
|
47856
47858
|
chatId: opts.ctx.chat_id,
|
|
47859
|
+
...opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {},
|
|
47857
47860
|
messageId: ts,
|
|
47858
47861
|
user: "vault-broker",
|
|
47859
47862
|
userId: 0,
|
|
@@ -47862,6 +47865,7 @@ function buildVaultGrantDeniedInbound(opts) {
|
|
|
47862
47865
|
meta: {
|
|
47863
47866
|
source: "vault_grant_denied",
|
|
47864
47867
|
agent: opts.ctx.agent,
|
|
47868
|
+
...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
|
|
47865
47869
|
key: opts.ctx.key,
|
|
47866
47870
|
scope: opts.ctx.scope,
|
|
47867
47871
|
stage_id: opts.stageId,
|
|
@@ -47874,6 +47878,7 @@ function buildVaultSaveCompletedInbound(opts) {
|
|
|
47874
47878
|
return {
|
|
47875
47879
|
type: "inbound",
|
|
47876
47880
|
chatId: opts.ctx.chat_id,
|
|
47881
|
+
...opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {},
|
|
47877
47882
|
messageId: ts,
|
|
47878
47883
|
user: "vault-broker",
|
|
47879
47884
|
userId: 0,
|
|
@@ -47882,6 +47887,7 @@ function buildVaultSaveCompletedInbound(opts) {
|
|
|
47882
47887
|
meta: {
|
|
47883
47888
|
source: "vault_save_completed",
|
|
47884
47889
|
agent: opts.ctx.agent,
|
|
47890
|
+
...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
|
|
47885
47891
|
key: opts.ctx.key,
|
|
47886
47892
|
stage_id: opts.stageId,
|
|
47887
47893
|
operator_id: opts.operatorId
|
|
@@ -47893,6 +47899,7 @@ function buildVaultSaveFailedInbound(opts) {
|
|
|
47893
47899
|
return {
|
|
47894
47900
|
type: "inbound",
|
|
47895
47901
|
chatId: opts.ctx.chat_id,
|
|
47902
|
+
...opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {},
|
|
47896
47903
|
messageId: ts,
|
|
47897
47904
|
user: "vault-broker",
|
|
47898
47905
|
userId: 0,
|
|
@@ -47901,6 +47908,7 @@ function buildVaultSaveFailedInbound(opts) {
|
|
|
47901
47908
|
meta: {
|
|
47902
47909
|
source: "vault_save_failed",
|
|
47903
47910
|
agent: opts.ctx.agent,
|
|
47911
|
+
...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
|
|
47904
47912
|
key: opts.ctx.key,
|
|
47905
47913
|
stage_id: opts.stageId,
|
|
47906
47914
|
operator_id: opts.operatorId
|
|
@@ -47912,6 +47920,7 @@ function buildVaultSaveDiscardedInbound(opts) {
|
|
|
47912
47920
|
return {
|
|
47913
47921
|
type: "inbound",
|
|
47914
47922
|
chatId: opts.ctx.chat_id,
|
|
47923
|
+
...opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {},
|
|
47915
47924
|
messageId: ts,
|
|
47916
47925
|
user: "vault-broker",
|
|
47917
47926
|
userId: 0,
|
|
@@ -47920,6 +47929,7 @@ function buildVaultSaveDiscardedInbound(opts) {
|
|
|
47920
47929
|
meta: {
|
|
47921
47930
|
source: "vault_save_discarded",
|
|
47922
47931
|
agent: opts.ctx.agent,
|
|
47932
|
+
...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
|
|
47923
47933
|
key: opts.ctx.key,
|
|
47924
47934
|
stage_id: opts.stageId,
|
|
47925
47935
|
operator_id: opts.operatorId
|
|
@@ -52024,10 +52034,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
52024
52034
|
}
|
|
52025
52035
|
|
|
52026
52036
|
// ../src/build-info.ts
|
|
52027
|
-
var VERSION = "0.14.
|
|
52028
|
-
var COMMIT_SHA = "
|
|
52029
|
-
var COMMIT_DATE = "2026-06-
|
|
52030
|
-
var LATEST_PR =
|
|
52037
|
+
var VERSION = "0.14.46";
|
|
52038
|
+
var COMMIT_SHA = "df3deacf";
|
|
52039
|
+
var COMMIT_DATE = "2026-06-03T05:00:14Z";
|
|
52040
|
+
var LATEST_PR = 2116;
|
|
52031
52041
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
52032
52042
|
|
|
52033
52043
|
// gateway/boot-version.ts
|
|
@@ -54091,13 +54101,15 @@ function emitGatewayOperatorEvent(event) {
|
|
|
54091
54101
|
return;
|
|
54092
54102
|
}
|
|
54093
54103
|
const opEventTopic = resolveAgentOutboundTopic({ kind: "compact-watchdog" });
|
|
54104
|
+
const opEventSupergroup = resolveAgentSupergroupChatId();
|
|
54094
54105
|
process.stderr.write(`telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)` + (opEventTopic != null ? ` topic=${opEventTopic}` : "") + `
|
|
54095
54106
|
`);
|
|
54096
54107
|
for (const chat_id of access.allowFrom) {
|
|
54108
|
+
const opEventThread = topicForRecipient({ recipientChatId: chat_id, resolvedTopic: opEventTopic, supergroupChatId: opEventSupergroup });
|
|
54097
54109
|
const opts = {
|
|
54098
54110
|
parse_mode: "HTML",
|
|
54099
54111
|
...renderedKeyboard ? { reply_markup: renderedKeyboard } : {},
|
|
54100
|
-
...
|
|
54112
|
+
...opEventThread != null ? { message_thread_id: opEventThread } : {}
|
|
54101
54113
|
};
|
|
54102
54114
|
bot.api.sendMessage(chat_id, renderedText, opts).catch((e) => {
|
|
54103
54115
|
process.stderr.write(`telegram gateway: operator-event send to ${chat_id} failed agent=${agent} kind=${kind}: ${e}
|
|
@@ -56081,6 +56093,8 @@ async function executeVaultRequestSave(args) {
|
|
|
56081
56093
|
sweepPendingVaultRequestSaves();
|
|
56082
56094
|
const text = renderVaultRequestSaveCard(pending2, agentSlug);
|
|
56083
56095
|
const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
|
|
56096
|
+
if (threadId != null)
|
|
56097
|
+
pending2.threadId = threadId;
|
|
56084
56098
|
const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text, {
|
|
56085
56099
|
parse_mode: "HTML",
|
|
56086
56100
|
reply_markup: buildVaultRequestSaveKeyboard(stageId),
|
|
@@ -56155,6 +56169,8 @@ async function executeRequestSecret(args) {
|
|
|
56155
56169
|
sweepSecretRequests();
|
|
56156
56170
|
const text = renderSecretRequestCard(pending2);
|
|
56157
56171
|
const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
|
|
56172
|
+
if (threadId != null)
|
|
56173
|
+
pending2.threadId = threadId;
|
|
56158
56174
|
const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text, {
|
|
56159
56175
|
parse_mode: "HTML",
|
|
56160
56176
|
reply_markup: buildSecretRequestKeyboard(stageId),
|
|
@@ -56205,12 +56221,19 @@ async function captureProvidedSecret(ctx, chat_id, msgId, value) {
|
|
|
56205
56221
|
const failMsg = {
|
|
56206
56222
|
type: "inbound",
|
|
56207
56223
|
chatId: chat_id,
|
|
56224
|
+
...armed.threadId != null ? { threadId: armed.threadId } : {},
|
|
56208
56225
|
messageId: fts,
|
|
56209
56226
|
user: "vault-broker",
|
|
56210
56227
|
userId: 0,
|
|
56211
56228
|
ts: fts,
|
|
56212
56229
|
text: `\u26A0\uFE0F The secret you requested for \`vault:${armed.key}\` could NOT be saved (vault write failed). Do not assume it is available; tell the operator or try request_secret again.`,
|
|
56213
|
-
meta: {
|
|
56230
|
+
meta: {
|
|
56231
|
+
source: "secret_provide_failed",
|
|
56232
|
+
agent: armed.agent,
|
|
56233
|
+
...armed.threadId != null ? { message_thread_id: String(armed.threadId) } : {},
|
|
56234
|
+
key: armed.key,
|
|
56235
|
+
stage_id: armed.stageId
|
|
56236
|
+
}
|
|
56214
56237
|
};
|
|
56215
56238
|
const fdelivered = ipcServer.sendToAgent(armed.agent, failMsg);
|
|
56216
56239
|
if (fdelivered)
|
|
@@ -56224,6 +56247,7 @@ async function captureProvidedSecret(ctx, chat_id, msgId, value) {
|
|
|
56224
56247
|
const synthetic = {
|
|
56225
56248
|
type: "inbound",
|
|
56226
56249
|
chatId: chat_id,
|
|
56250
|
+
...armed.threadId != null ? { threadId: armed.threadId } : {},
|
|
56227
56251
|
messageId: ts,
|
|
56228
56252
|
user: "vault-broker",
|
|
56229
56253
|
userId: 0,
|
|
@@ -56232,6 +56256,7 @@ async function captureProvidedSecret(ctx, chat_id, msgId, value) {
|
|
|
56232
56256
|
meta: {
|
|
56233
56257
|
source: "secret_provided",
|
|
56234
56258
|
agent: armed.agent,
|
|
56259
|
+
...armed.threadId != null ? { message_thread_id: String(armed.threadId) } : {},
|
|
56235
56260
|
key: armed.key,
|
|
56236
56261
|
stage_id: armed.stageId
|
|
56237
56262
|
}
|
|
@@ -56265,7 +56290,8 @@ async function handleSecretRequestCallback(ctx, data) {
|
|
|
56265
56290
|
key: pending2.key,
|
|
56266
56291
|
agent: pending2.agent,
|
|
56267
56292
|
stageId,
|
|
56268
|
-
armed_at: Date.now()
|
|
56293
|
+
armed_at: Date.now(),
|
|
56294
|
+
...pending2.threadId != null ? { threadId: pending2.threadId } : {}
|
|
56269
56295
|
});
|
|
56270
56296
|
await ctx.answerCallbackQuery({ text: "Send the value now \u2014 it auto-deletes." }).catch(() => {});
|
|
56271
56297
|
if (pending2.card_message_id != null) {
|
|
@@ -56287,12 +56313,19 @@ async function handleSecretRequestCallback(ctx, data) {
|
|
|
56287
56313
|
const synthetic = {
|
|
56288
56314
|
type: "inbound",
|
|
56289
56315
|
chatId: pending2.chat_id,
|
|
56316
|
+
...pending2.threadId != null ? { threadId: pending2.threadId } : {},
|
|
56290
56317
|
messageId: ts,
|
|
56291
56318
|
user: "vault-broker",
|
|
56292
56319
|
userId: 0,
|
|
56293
56320
|
ts,
|
|
56294
56321
|
text: `\uD83D\uDEAB Operator declined your request for \`vault:${pending2.key}\`. Proceed without it or ask how they'd like to handle the task.`,
|
|
56295
|
-
meta: {
|
|
56322
|
+
meta: {
|
|
56323
|
+
source: "secret_declined",
|
|
56324
|
+
agent: pending2.agent,
|
|
56325
|
+
...pending2.threadId != null ? { message_thread_id: String(pending2.threadId) } : {},
|
|
56326
|
+
key: pending2.key,
|
|
56327
|
+
stage_id: stageId
|
|
56328
|
+
}
|
|
56296
56329
|
};
|
|
56297
56330
|
const delivered = ipcServer.sendToAgent(pending2.agent, synthetic);
|
|
56298
56331
|
if (delivered)
|
|
@@ -56393,6 +56426,8 @@ async function executeVaultRequestAccess(args) {
|
|
|
56393
56426
|
sweepPendingVaultRequestAccesses();
|
|
56394
56427
|
const text = renderVaultRequestAccessCard(pending2);
|
|
56395
56428
|
const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
|
|
56429
|
+
if (threadId != null)
|
|
56430
|
+
pending2.threadId = threadId;
|
|
56396
56431
|
const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text, {
|
|
56397
56432
|
parse_mode: "HTML",
|
|
56398
56433
|
reply_markup: buildVaultRequestAccessKeyboard(stageId),
|
|
@@ -57874,7 +57909,7 @@ ${preBlock(write.output)}`;
|
|
|
57874
57909
|
}
|
|
57875
57910
|
}
|
|
57876
57911
|
}
|
|
57877
|
-
bot.api.sendChatAction(chat_id, "typing").catch(() => {});
|
|
57912
|
+
bot.api.sendChatAction(chat_id, "typing", messageThreadId != null ? { message_thread_id: messageThreadId } : {}).catch(() => {});
|
|
57878
57913
|
const parsedSteer = parseSteerPrefix(text);
|
|
57879
57914
|
const isSteerPrefix = parsedSteer.steering;
|
|
57880
57915
|
const parsedQueue = isSteerPrefix ? { queued: false, body: parsedSteer.body } : parseQueuePrefix(text);
|
|
@@ -58381,9 +58416,10 @@ function resolveBootChatId(marker, ageMs) {
|
|
|
58381
58416
|
};
|
|
58382
58417
|
}
|
|
58383
58418
|
const supergroupBootTopic = resolveAgentOutboundTopic({ kind: "boot" });
|
|
58419
|
+
const bootSupergroup = resolveAgentSupergroupChatId();
|
|
58384
58420
|
const envChat = process.env.SUBAGENT_OWNER_CHAT_ID;
|
|
58385
58421
|
if (envChat)
|
|
58386
|
-
return { chatId: envChat, threadId: supergroupBootTopic, ackMsgId: undefined };
|
|
58422
|
+
return { chatId: envChat, threadId: topicForRecipient({ recipientChatId: envChat, resolvedTopic: supergroupBootTopic, supergroupChatId: bootSupergroup }), ackMsgId: undefined };
|
|
58387
58423
|
if (HISTORY_ENABLED) {
|
|
58388
58424
|
try {
|
|
58389
58425
|
const access = loadAccess();
|
|
@@ -58391,7 +58427,7 @@ function resolveBootChatId(marker, ageMs) {
|
|
|
58391
58427
|
if (ownerChatId) {
|
|
58392
58428
|
const recent = query({ chat_id: ownerChatId, limit: 1 });
|
|
58393
58429
|
if (recent.length > 0)
|
|
58394
|
-
return { chatId: ownerChatId, threadId: supergroupBootTopic, ackMsgId: undefined };
|
|
58430
|
+
return { chatId: ownerChatId, threadId: topicForRecipient({ recipientChatId: ownerChatId, resolvedTopic: supergroupBootTopic, supergroupChatId: bootSupergroup }), ackMsgId: undefined };
|
|
58395
58431
|
}
|
|
58396
58432
|
} catch {}
|
|
58397
58433
|
}
|
|
@@ -60025,7 +60061,8 @@ async function performVaultAccessApproval(ctx, pending2, stageId, senderId, atte
|
|
|
60025
60061
|
key: pending2.key,
|
|
60026
60062
|
scope: pending2.scope,
|
|
60027
60063
|
chat_id: pending2.chat_id,
|
|
60028
|
-
ttl_seconds: pending2.ttl_seconds
|
|
60064
|
+
ttl_seconds: pending2.ttl_seconds,
|
|
60065
|
+
...pending2.threadId != null ? { threadId: pending2.threadId } : {}
|
|
60029
60066
|
},
|
|
60030
60067
|
grantId: id,
|
|
60031
60068
|
stageId,
|
|
@@ -60074,7 +60111,8 @@ async function handleVaultRequestAccessCallback(ctx, data) {
|
|
|
60074
60111
|
key: pending2.key,
|
|
60075
60112
|
scope: pending2.scope,
|
|
60076
60113
|
chat_id: pending2.chat_id,
|
|
60077
|
-
ttl_seconds: pending2.ttl_seconds
|
|
60114
|
+
ttl_seconds: pending2.ttl_seconds,
|
|
60115
|
+
...pending2.threadId != null ? { threadId: pending2.threadId } : {}
|
|
60078
60116
|
},
|
|
60079
60117
|
stageId,
|
|
60080
60118
|
operatorId: senderId
|
|
@@ -60169,7 +60207,12 @@ async function handleVaultRequestSaveCallback(ctx, data) {
|
|
|
60169
60207
|
await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `\uD83D\uDEAB <i>Discarded. The secret was not written to the vault.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
60170
60208
|
}
|
|
60171
60209
|
const discardInbound = buildVaultSaveDiscardedInbound({
|
|
60172
|
-
ctx: {
|
|
60210
|
+
ctx: {
|
|
60211
|
+
agent: pending2.agent,
|
|
60212
|
+
key: pending2.key,
|
|
60213
|
+
chat_id: pending2.chat_id,
|
|
60214
|
+
...pending2.threadId != null ? { threadId: pending2.threadId } : {}
|
|
60215
|
+
},
|
|
60173
60216
|
stageId,
|
|
60174
60217
|
operatorId: senderId
|
|
60175
60218
|
});
|
|
@@ -60230,7 +60273,12 @@ async function handleVaultRequestSaveCallback(ctx, data) {
|
|
|
60230
60273
|
const failReason = (write.output || "vault write error").split(`
|
|
60231
60274
|
`)[0].slice(0, 200);
|
|
60232
60275
|
const failInbound = buildVaultSaveFailedInbound({
|
|
60233
|
-
ctx: {
|
|
60276
|
+
ctx: {
|
|
60277
|
+
agent: pending2.agent,
|
|
60278
|
+
key: pending2.key,
|
|
60279
|
+
chat_id: pending2.chat_id,
|
|
60280
|
+
...pending2.threadId != null ? { threadId: pending2.threadId } : {}
|
|
60281
|
+
},
|
|
60234
60282
|
stageId,
|
|
60235
60283
|
operatorId: senderId,
|
|
60236
60284
|
reason: failReason
|
|
@@ -60250,7 +60298,12 @@ async function handleVaultRequestSaveCallback(ctx, data) {
|
|
|
60250
60298
|
<i>The agent can now reference this as <code>vault:${escapeHtmlForTg(pending2.key)}</code>.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
60251
60299
|
}
|
|
60252
60300
|
const okInbound = buildVaultSaveCompletedInbound({
|
|
60253
|
-
ctx: {
|
|
60301
|
+
ctx: {
|
|
60302
|
+
agent: pending2.agent,
|
|
60303
|
+
key: pending2.key,
|
|
60304
|
+
chat_id: pending2.chat_id,
|
|
60305
|
+
...pending2.threadId != null ? { threadId: pending2.threadId } : {}
|
|
60306
|
+
},
|
|
60254
60307
|
stageId,
|
|
60255
60308
|
operatorId: senderId
|
|
60256
60309
|
});
|
|
@@ -2913,6 +2913,10 @@ interface PendingVaultRequestSave {
|
|
|
2913
2913
|
chat_id: string
|
|
2914
2914
|
/** Card message id (filled in after we send the card). */
|
|
2915
2915
|
card_message_id?: number
|
|
2916
|
+
/** Supergroup forum topic the agent was working in when it requested the
|
|
2917
|
+
* save — carried into the save-outcome inbound so the resumed reply lands
|
|
2918
|
+
* back in that topic, not General. */
|
|
2919
|
+
threadId?: number
|
|
2916
2920
|
/** Currently-suggested slug; may be renamed by the user. */
|
|
2917
2921
|
key: string
|
|
2918
2922
|
/** Storage shape — 'string' (default) or 'binary'. */
|
|
@@ -2954,6 +2958,10 @@ interface PendingVaultRequestAccess {
|
|
|
2954
2958
|
chat_id: string
|
|
2955
2959
|
/** Card message id (filled in after we send the card). */
|
|
2956
2960
|
card_message_id?: number
|
|
2961
|
+
/** Supergroup forum topic the agent was working in when it requested (the
|
|
2962
|
+
* card's originating thread). Carried into the grant-outcome inbound so the
|
|
2963
|
+
* resumed reply lands back in that topic, not General. */
|
|
2964
|
+
threadId?: number
|
|
2957
2965
|
/** Vault key the agent wants to read. */
|
|
2958
2966
|
key: string
|
|
2959
2967
|
/** 'read' (default) or 'write'. */
|
|
@@ -3537,18 +3545,24 @@ function emitGatewayOperatorEvent(event: OperatorEvent): void {
|
|
|
3537
3545
|
// Fleet-shared / DM agents see `undefined` → no `message_thread_id`
|
|
3538
3546
|
// is added to the broadcast opts → behavior unchanged.
|
|
3539
3547
|
const opEventTopic = resolveAgentOutboundTopic({ kind: 'compact-watchdog' })
|
|
3548
|
+
const opEventSupergroup = resolveAgentSupergroupChatId()
|
|
3540
3549
|
|
|
3541
3550
|
process.stderr.write(
|
|
3542
3551
|
`telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)` +
|
|
3543
3552
|
(opEventTopic != null ? ` topic=${opEventTopic}` : '') + '\n',
|
|
3544
3553
|
)
|
|
3545
3554
|
for (const chat_id of access.allowFrom) {
|
|
3555
|
+
// The resolved topic is valid ONLY in the agent's supergroup — attaching
|
|
3556
|
+
// it to an operator DM recipient yields 400 "message thread not found" and
|
|
3557
|
+
// the event silently fails to deliver (the marko #2096 class). Guard it:
|
|
3558
|
+
// DM recipients get a thread-less send; the supergroup owner gets the lane.
|
|
3559
|
+
const opEventThread = topicForRecipient({ recipientChatId: chat_id, resolvedTopic: opEventTopic, supergroupChatId: opEventSupergroup })
|
|
3546
3560
|
// grammy's Other<...> opts type is generated and stricter than our
|
|
3547
3561
|
// call shape; runtime accepts both. Cast through unknown.
|
|
3548
3562
|
const opts = {
|
|
3549
3563
|
parse_mode: 'HTML' as const,
|
|
3550
3564
|
...(renderedKeyboard ? { reply_markup: renderedKeyboard } : {}),
|
|
3551
|
-
...(
|
|
3565
|
+
...(opEventThread != null ? { message_thread_id: opEventThread } : {}),
|
|
3552
3566
|
}
|
|
3553
3567
|
// Comment-only context for the reader; the lint marker on the
|
|
3554
3568
|
// very next line is what unlocks the raw bot.api call.
|
|
@@ -7074,6 +7088,8 @@ async function executeVaultRequestSave(args: Record<string, unknown>): Promise<{
|
|
|
7074
7088
|
// crashing the tool call.
|
|
7075
7089
|
const text = renderVaultRequestSaveCard(pending, agentSlug)
|
|
7076
7090
|
const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
|
|
7091
|
+
// Remember the agent's working topic so the save-outcome inbound resumes in it.
|
|
7092
|
+
if (threadId != null) pending.threadId = threadId
|
|
7077
7093
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
7078
7094
|
robustApiCall,
|
|
7079
7095
|
(tid) =>
|
|
@@ -7114,12 +7130,16 @@ interface PendingSecretRequest {
|
|
|
7114
7130
|
reason?: string
|
|
7115
7131
|
staged_at: number
|
|
7116
7132
|
card_message_id?: number
|
|
7133
|
+
/** Supergroup forum topic the agent was working in — carried into the
|
|
7134
|
+
* provide/decline/fail outcome inbounds so the resumed reply lands back
|
|
7135
|
+
* in that topic, not General. */
|
|
7136
|
+
threadId?: number
|
|
7117
7137
|
}
|
|
7118
7138
|
// stageId -> request (lives until tapped or TTL).
|
|
7119
7139
|
const pendingSecretRequests = new Map<string, PendingSecretRequest>()
|
|
7120
7140
|
// chat_id -> the armed capture: the operator's NEXT message in this chat is
|
|
7121
7141
|
// the value for `key`. Set when [Provide securely] is tapped.
|
|
7122
|
-
interface ArmedSecretCapture { key: string; agent: string; stageId: string; armed_at: number }
|
|
7142
|
+
interface ArmedSecretCapture { key: string; agent: string; stageId: string; armed_at: number; threadId?: number }
|
|
7123
7143
|
const armedSecretCaptures = new Map<string, ArmedSecretCapture>()
|
|
7124
7144
|
const PENDING_SECRET_REQUEST_TTL_MS = 30 * 60_000 // card lifetime
|
|
7125
7145
|
const ARMED_SECRET_CAPTURE_TTL_MS = 10 * 60_000 // window to send the value after tapping
|
|
@@ -7188,6 +7208,8 @@ async function executeRequestSecret(args: Record<string, unknown>): Promise<{ co
|
|
|
7188
7208
|
|
|
7189
7209
|
const text = renderSecretRequestCard(pending)
|
|
7190
7210
|
const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
|
|
7211
|
+
// Remember the agent's working topic so the provide/decline/fail inbound resumes in it.
|
|
7212
|
+
if (threadId != null) pending.threadId = threadId
|
|
7191
7213
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
7192
7214
|
robustApiCall,
|
|
7193
7215
|
(tid) =>
|
|
@@ -7263,12 +7285,19 @@ async function captureProvidedSecret(
|
|
|
7263
7285
|
const failMsg: InboundMessage = {
|
|
7264
7286
|
type: 'inbound',
|
|
7265
7287
|
chatId: chat_id,
|
|
7288
|
+
...(armed.threadId != null ? { threadId: armed.threadId } : {}),
|
|
7266
7289
|
messageId: fts,
|
|
7267
7290
|
user: 'vault-broker',
|
|
7268
7291
|
userId: 0,
|
|
7269
7292
|
ts: fts,
|
|
7270
7293
|
text: `⚠️ The secret you requested for \`vault:${armed.key}\` could NOT be saved (vault write failed). Do not assume it is available; tell the operator or try request_secret again.`,
|
|
7271
|
-
meta: {
|
|
7294
|
+
meta: {
|
|
7295
|
+
source: 'secret_provide_failed',
|
|
7296
|
+
agent: armed.agent,
|
|
7297
|
+
...(armed.threadId != null ? { message_thread_id: String(armed.threadId) } : {}),
|
|
7298
|
+
key: armed.key,
|
|
7299
|
+
stage_id: armed.stageId,
|
|
7300
|
+
},
|
|
7272
7301
|
}
|
|
7273
7302
|
const fdelivered = ipcServer.sendToAgent(armed.agent, failMsg)
|
|
7274
7303
|
if (fdelivered) markClaudeBusyForInbound(failMsg)
|
|
@@ -7288,6 +7317,7 @@ async function captureProvidedSecret(
|
|
|
7288
7317
|
const synthetic: InboundMessage = {
|
|
7289
7318
|
type: 'inbound',
|
|
7290
7319
|
chatId: chat_id,
|
|
7320
|
+
...(armed.threadId != null ? { threadId: armed.threadId } : {}),
|
|
7291
7321
|
messageId: ts,
|
|
7292
7322
|
user: 'vault-broker',
|
|
7293
7323
|
userId: 0,
|
|
@@ -7299,6 +7329,7 @@ async function captureProvidedSecret(
|
|
|
7299
7329
|
meta: {
|
|
7300
7330
|
source: 'secret_provided',
|
|
7301
7331
|
agent: armed.agent,
|
|
7332
|
+
...(armed.threadId != null ? { message_thread_id: String(armed.threadId) } : {}),
|
|
7302
7333
|
key: armed.key,
|
|
7303
7334
|
stage_id: armed.stageId,
|
|
7304
7335
|
},
|
|
@@ -7339,6 +7370,7 @@ async function handleSecretRequestCallback(ctx: Context, data: string): Promise<
|
|
|
7339
7370
|
agent: pending.agent,
|
|
7340
7371
|
stageId,
|
|
7341
7372
|
armed_at: Date.now(),
|
|
7373
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
7342
7374
|
})
|
|
7343
7375
|
await ctx.answerCallbackQuery({ text: 'Send the value now — it auto-deletes.' }).catch(() => {})
|
|
7344
7376
|
if (pending.card_message_id != null) {
|
|
@@ -7371,12 +7403,19 @@ async function handleSecretRequestCallback(ctx: Context, data: string): Promise<
|
|
|
7371
7403
|
const synthetic: InboundMessage = {
|
|
7372
7404
|
type: 'inbound',
|
|
7373
7405
|
chatId: pending.chat_id,
|
|
7406
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
7374
7407
|
messageId: ts,
|
|
7375
7408
|
user: 'vault-broker',
|
|
7376
7409
|
userId: 0,
|
|
7377
7410
|
ts,
|
|
7378
7411
|
text: `🚫 Operator declined your request for \`vault:${pending.key}\`. Proceed without it or ask how they'd like to handle the task.`,
|
|
7379
|
-
meta: {
|
|
7412
|
+
meta: {
|
|
7413
|
+
source: 'secret_declined',
|
|
7414
|
+
agent: pending.agent,
|
|
7415
|
+
...(pending.threadId != null ? { message_thread_id: String(pending.threadId) } : {}),
|
|
7416
|
+
key: pending.key,
|
|
7417
|
+
stage_id: stageId,
|
|
7418
|
+
},
|
|
7380
7419
|
}
|
|
7381
7420
|
const delivered = ipcServer.sendToAgent(pending.agent, synthetic)
|
|
7382
7421
|
if (delivered) markClaudeBusyForInbound(synthetic)
|
|
@@ -7524,6 +7563,8 @@ async function executeVaultRequestAccess(args: Record<string, unknown>): Promise
|
|
|
7524
7563
|
|
|
7525
7564
|
const text = renderVaultRequestAccessCard(pending)
|
|
7526
7565
|
const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
|
|
7566
|
+
// Remember the agent's working topic so the grant-outcome inbound resumes in it.
|
|
7567
|
+
if (threadId != null) pending.threadId = threadId
|
|
7527
7568
|
// #1075: deleted-topic safe — fall back to main chat.
|
|
7528
7569
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
7529
7570
|
robustApiCall,
|
|
@@ -10159,7 +10200,14 @@ async function handleInbound(
|
|
|
10159
10200
|
// No staged entry to act on — fall through to normal handling.
|
|
10160
10201
|
}
|
|
10161
10202
|
|
|
10162
|
-
|
|
10203
|
+
// Typing indicator in the ORIGINATING topic — on a supergroup-topic inbound,
|
|
10204
|
+
// an un-threaded sendChatAction shows "typing" in General, not the topic the
|
|
10205
|
+
// user is in. messageThreadId is the inbound's thread (undefined in a DM).
|
|
10206
|
+
void bot.api.sendChatAction(
|
|
10207
|
+
chat_id,
|
|
10208
|
+
'typing',
|
|
10209
|
+
messageThreadId != null ? { message_thread_id: messageThreadId } : {},
|
|
10210
|
+
).catch(() => {})
|
|
10163
10211
|
|
|
10164
10212
|
// Parse explicit prefixes first. `/steer ` / `/s ` opts IN to steering;
|
|
10165
10213
|
// `/queue ` / `/q ` are legacy aliases that opt in to the new default (queued).
|
|
@@ -11088,10 +11136,14 @@ function resolveBootChatId(
|
|
|
11088
11136
|
// → behavior unchanged (lands at chat-root as today). PR4b of
|
|
11089
11137
|
// supergroup-mode rollout (docs/rfcs/supergroup-mode.md).
|
|
11090
11138
|
const supergroupBootTopic = resolveAgentOutboundTopic({ kind: 'boot' })
|
|
11139
|
+
const bootSupergroup = resolveAgentSupergroupChatId()
|
|
11140
|
+
// The boot topic is valid only in the agent's supergroup — attach it per
|
|
11141
|
+
// recipient so a DM owner doesn't 400 (marko #2096 class); the supergroup
|
|
11142
|
+
// owner gets the boot/alerts lane, a DM gets a thread-less boot card.
|
|
11091
11143
|
|
|
11092
11144
|
// 2. Env var
|
|
11093
11145
|
const envChat = process.env.SUBAGENT_OWNER_CHAT_ID
|
|
11094
|
-
if (envChat) return { chatId: envChat, threadId: supergroupBootTopic, ackMsgId: undefined }
|
|
11146
|
+
if (envChat) return { chatId: envChat, threadId: topicForRecipient({ recipientChatId: envChat, resolvedTopic: supergroupBootTopic, supergroupChatId: bootSupergroup }), ackMsgId: undefined }
|
|
11095
11147
|
// 3. Most-recent inbound from history
|
|
11096
11148
|
if (HISTORY_ENABLED) {
|
|
11097
11149
|
try {
|
|
@@ -11099,7 +11151,7 @@ function resolveBootChatId(
|
|
|
11099
11151
|
const ownerChatId = access.allowFrom[0]
|
|
11100
11152
|
if (ownerChatId) {
|
|
11101
11153
|
const recent = queryHistory({ chat_id: ownerChatId, limit: 1 })
|
|
11102
|
-
if (recent.length > 0) return { chatId: ownerChatId, threadId: supergroupBootTopic, ackMsgId: undefined }
|
|
11154
|
+
if (recent.length > 0) return { chatId: ownerChatId, threadId: topicForRecipient({ recipientChatId: ownerChatId, resolvedTopic: supergroupBootTopic, supergroupChatId: bootSupergroup }), ackMsgId: undefined }
|
|
11103
11155
|
}
|
|
11104
11156
|
} catch {}
|
|
11105
11157
|
}
|
|
@@ -14006,6 +14058,7 @@ async function performVaultAccessApproval(
|
|
|
14006
14058
|
scope: pending.scope,
|
|
14007
14059
|
chat_id: pending.chat_id,
|
|
14008
14060
|
ttl_seconds: pending.ttl_seconds,
|
|
14061
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14009
14062
|
},
|
|
14010
14063
|
grantId: id,
|
|
14011
14064
|
stageId,
|
|
@@ -14087,6 +14140,7 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
|
|
|
14087
14140
|
scope: pending.scope,
|
|
14088
14141
|
chat_id: pending.chat_id,
|
|
14089
14142
|
ttl_seconds: pending.ttl_seconds,
|
|
14143
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14090
14144
|
},
|
|
14091
14145
|
stageId,
|
|
14092
14146
|
operatorId: senderId,
|
|
@@ -14261,7 +14315,12 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
14261
14315
|
// tool returned "waiting for operator", the turn ended, and a
|
|
14262
14316
|
// Discard left the agent silently idle forever.
|
|
14263
14317
|
const discardInbound = buildVaultSaveDiscardedInbound({
|
|
14264
|
-
ctx: {
|
|
14318
|
+
ctx: {
|
|
14319
|
+
agent: pending.agent,
|
|
14320
|
+
key: pending.key,
|
|
14321
|
+
chat_id: pending.chat_id,
|
|
14322
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14323
|
+
},
|
|
14265
14324
|
stageId,
|
|
14266
14325
|
operatorId: senderId,
|
|
14267
14326
|
})
|
|
@@ -14384,7 +14443,12 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
14384
14443
|
const failReason =
|
|
14385
14444
|
(write.output || 'vault write error').split('\n')[0]!.slice(0, 200)
|
|
14386
14445
|
const failInbound = buildVaultSaveFailedInbound({
|
|
14387
|
-
ctx: {
|
|
14446
|
+
ctx: {
|
|
14447
|
+
agent: pending.agent,
|
|
14448
|
+
key: pending.key,
|
|
14449
|
+
chat_id: pending.chat_id,
|
|
14450
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14451
|
+
},
|
|
14388
14452
|
stageId,
|
|
14389
14453
|
operatorId: senderId,
|
|
14390
14454
|
reason: failReason,
|
|
@@ -14415,7 +14479,12 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
14415
14479
|
// task that was blocked on this credential (symmetric with the
|
|
14416
14480
|
// vra: approve path; buffered if the bridge is mid-reconnect).
|
|
14417
14481
|
const okInbound = buildVaultSaveCompletedInbound({
|
|
14418
|
-
ctx: {
|
|
14482
|
+
ctx: {
|
|
14483
|
+
agent: pending.agent,
|
|
14484
|
+
key: pending.key,
|
|
14485
|
+
chat_id: pending.chat_id,
|
|
14486
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14487
|
+
},
|
|
14419
14488
|
stageId,
|
|
14420
14489
|
operatorId: senderId,
|
|
14421
14490
|
})
|
|
@@ -34,6 +34,10 @@ export interface VaultGrantInboundContext {
|
|
|
34
34
|
chat_id: string
|
|
35
35
|
/** Seconds. For approved grants; ignored for deny. */
|
|
36
36
|
ttl_seconds: number
|
|
37
|
+
/** Supergroup forum topic (message_thread_id) the agent was working in
|
|
38
|
+
* when it requested the credential — so the resumed turn's reply lands
|
|
39
|
+
* back in that topic, not General. Undefined for DM / non-topic requests. */
|
|
40
|
+
threadId?: number
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -62,6 +66,7 @@ export function buildVaultGrantApprovedInbound(opts: {
|
|
|
62
66
|
return {
|
|
63
67
|
type: 'inbound',
|
|
64
68
|
chatId: opts.ctx.chat_id,
|
|
69
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
65
70
|
messageId: ts, // synthetic — no Telegram message id exists
|
|
66
71
|
user: 'vault-broker',
|
|
67
72
|
userId: 0,
|
|
@@ -76,6 +81,7 @@ export function buildVaultGrantApprovedInbound(opts: {
|
|
|
76
81
|
meta: {
|
|
77
82
|
source: 'vault_grant_approved',
|
|
78
83
|
agent: opts.ctx.agent,
|
|
84
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
79
85
|
key: opts.ctx.key,
|
|
80
86
|
scope: opts.ctx.scope,
|
|
81
87
|
grant_id: opts.grantId,
|
|
@@ -103,6 +109,7 @@ export function buildVaultGrantDeniedInbound(opts: {
|
|
|
103
109
|
return {
|
|
104
110
|
type: 'inbound',
|
|
105
111
|
chatId: opts.ctx.chat_id,
|
|
112
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
106
113
|
messageId: ts,
|
|
107
114
|
user: 'vault-broker',
|
|
108
115
|
userId: 0,
|
|
@@ -116,6 +123,7 @@ export function buildVaultGrantDeniedInbound(opts: {
|
|
|
116
123
|
meta: {
|
|
117
124
|
source: 'vault_grant_denied',
|
|
118
125
|
agent: opts.ctx.agent,
|
|
126
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
119
127
|
key: opts.ctx.key,
|
|
120
128
|
scope: opts.ctx.scope,
|
|
121
129
|
stage_id: opts.stageId,
|
|
@@ -133,6 +141,9 @@ export interface VaultSaveInboundContext {
|
|
|
133
141
|
/** Telegram chat the save card lived in — keeps the synthesized
|
|
134
142
|
* resume-turn associated with the originating conversation. */
|
|
135
143
|
chat_id: string
|
|
144
|
+
/** Supergroup forum topic the agent was working in when it requested the
|
|
145
|
+
* save — so the resumed reply lands in that topic, not General. */
|
|
146
|
+
threadId?: number
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
/**
|
|
@@ -154,6 +165,7 @@ export function buildVaultSaveCompletedInbound(opts: {
|
|
|
154
165
|
return {
|
|
155
166
|
type: 'inbound',
|
|
156
167
|
chatId: opts.ctx.chat_id,
|
|
168
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
157
169
|
messageId: ts,
|
|
158
170
|
user: 'vault-broker',
|
|
159
171
|
userId: 0,
|
|
@@ -165,6 +177,7 @@ export function buildVaultSaveCompletedInbound(opts: {
|
|
|
165
177
|
meta: {
|
|
166
178
|
source: 'vault_save_completed',
|
|
167
179
|
agent: opts.ctx.agent,
|
|
180
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
168
181
|
key: opts.ctx.key,
|
|
169
182
|
stage_id: opts.stageId,
|
|
170
183
|
operator_id: opts.operatorId,
|
|
@@ -187,6 +200,7 @@ export function buildVaultSaveFailedInbound(opts: {
|
|
|
187
200
|
return {
|
|
188
201
|
type: 'inbound',
|
|
189
202
|
chatId: opts.ctx.chat_id,
|
|
203
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
190
204
|
messageId: ts,
|
|
191
205
|
user: 'vault-broker',
|
|
192
206
|
userId: 0,
|
|
@@ -200,6 +214,7 @@ export function buildVaultSaveFailedInbound(opts: {
|
|
|
200
214
|
meta: {
|
|
201
215
|
source: 'vault_save_failed',
|
|
202
216
|
agent: opts.ctx.agent,
|
|
217
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
203
218
|
key: opts.ctx.key,
|
|
204
219
|
stage_id: opts.stageId,
|
|
205
220
|
operator_id: opts.operatorId,
|
|
@@ -221,6 +236,7 @@ export function buildVaultSaveDiscardedInbound(opts: {
|
|
|
221
236
|
return {
|
|
222
237
|
type: 'inbound',
|
|
223
238
|
chatId: opts.ctx.chat_id,
|
|
239
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
224
240
|
messageId: ts,
|
|
225
241
|
user: 'vault-broker',
|
|
226
242
|
userId: 0,
|
|
@@ -234,6 +250,7 @@ export function buildVaultSaveDiscardedInbound(opts: {
|
|
|
234
250
|
meta: {
|
|
235
251
|
source: 'vault_save_discarded',
|
|
236
252
|
agent: opts.ctx.agent,
|
|
253
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
237
254
|
key: opts.ctx.key,
|
|
238
255
|
stage_id: opts.stageId,
|
|
239
256
|
operator_id: opts.operatorId,
|
|
@@ -133,8 +133,9 @@ export function computeLabel(toolName, input) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
// MCP
|
|
136
|
+
// MCP tools.
|
|
137
137
|
if (typeof toolName === 'string' && toolName.startsWith('mcp__')) {
|
|
138
|
+
// Explicit labels / suppressions for the built-in servers.
|
|
138
139
|
switch (toolName) {
|
|
139
140
|
case 'mcp__switchroom-telegram__reply':
|
|
140
141
|
case 'mcp__switchroom-telegram__stream_reply':
|
|
@@ -150,15 +151,46 @@ export function computeLabel(toolName, input) {
|
|
|
150
151
|
return 'Searching memory'
|
|
151
152
|
case 'mcp__hindsight__retain':
|
|
152
153
|
return 'Saving memory'
|
|
153
|
-
// Explicit suppressions — return null so we don't emit a sidecar
|
|
154
|
-
// line at all. (Falling through to the default below produces the
|
|
155
|
-
// same effect, but listing these makes the intent obvious.)
|
|
154
|
+
// Explicit suppressions — return null so we don't emit a sidecar line.
|
|
156
155
|
case 'mcp__switchroom-telegram__send_typing':
|
|
157
156
|
case 'mcp__hindsight__sync_retain':
|
|
158
157
|
return null
|
|
159
158
|
}
|
|
160
|
-
//
|
|
161
|
-
|
|
159
|
+
// Generic fallback for ANY other MCP tool (operator-configured servers
|
|
160
|
+
// — perplexity, webkite, gdrive, notion, …). These previously returned
|
|
161
|
+
// null → invisible in the live activity feed, so a research turn driven
|
|
162
|
+
// entirely by MCP tools read as pure silence (only a typing dot + the
|
|
163
|
+
// 👀 reaction) — the "I can't see what it's doing" report. Mirror the
|
|
164
|
+
// gateway's describeToolUse: friendly per-server labels, else a
|
|
165
|
+
// model-authored field, else a humanized tool name. NEVER label
|
|
166
|
+
// switchroom-telegram surface/control tools (they ARE the conversation).
|
|
167
|
+
const m = /^mcp__(.+?)__(.+)$/.exec(toolName)
|
|
168
|
+
if (!m) return null
|
|
169
|
+
const server = m[1].toLowerCase()
|
|
170
|
+
const tool = m[2].toLowerCase()
|
|
171
|
+
if (server === 'switchroom-telegram') return null
|
|
172
|
+
if (server === 'hindsight') return 'Working with memory'
|
|
173
|
+
if (server === 'google-workspace' || server === 'claude_ai_google_calendar')
|
|
174
|
+
return 'Checking your calendar'
|
|
175
|
+
if (server === 'claude_ai_gmail') return 'Checking your email'
|
|
176
|
+
if (server === 'claude_ai_google_drive' || server === 'gdrive')
|
|
177
|
+
return 'Looking through your files'
|
|
178
|
+
if (server === 'notion' || server === 'claude_ai_notion') return 'Checking your notes'
|
|
179
|
+
if (server === 'perplexity') {
|
|
180
|
+
const q = clip(String(i.query ?? i.description ?? ''), 60).trim()
|
|
181
|
+
return q ? `Searching the web for ${q}` : 'Searching the web'
|
|
182
|
+
}
|
|
183
|
+
if (server === 'webkite') {
|
|
184
|
+
const u = clip(urlHostPath(i.url ?? ''), 60).trim()
|
|
185
|
+
return u ? `Reading ${u}` : 'Reading the web'
|
|
186
|
+
}
|
|
187
|
+
// Unknown MCP server: prefer a model-authored field, else humanized tool.
|
|
188
|
+
const desc =
|
|
189
|
+
clip(String(i.description ?? ''), 60).trim() ||
|
|
190
|
+
clip(String(i.query ?? ''), 50).trim() ||
|
|
191
|
+
clip(String(i.title ?? ''), 50).trim()
|
|
192
|
+
if (desc) return desc
|
|
193
|
+
return `Using ${tool.replace(/[-_]+/g, ' ')}`
|
|
162
194
|
}
|
|
163
195
|
|
|
164
196
|
return null
|
|
@@ -16,7 +16,11 @@ import { describe, it, expect } from 'vitest'
|
|
|
16
16
|
import {
|
|
17
17
|
buildVaultGrantApprovedInbound,
|
|
18
18
|
buildVaultGrantDeniedInbound,
|
|
19
|
+
buildVaultSaveCompletedInbound,
|
|
20
|
+
buildVaultSaveFailedInbound,
|
|
21
|
+
buildVaultSaveDiscardedInbound,
|
|
19
22
|
type VaultGrantInboundContext,
|
|
23
|
+
type VaultSaveInboundContext,
|
|
20
24
|
} from '../gateway/vault-grant-inbound-builders.js'
|
|
21
25
|
|
|
22
26
|
const FIXED_NOW = 1_700_000_000_000
|
|
@@ -224,3 +228,83 @@ describe('approve vs deny shape invariants', () => {
|
|
|
224
228
|
expect(String(deny.meta?.source)).toMatch(/^vault_grant_denied$/)
|
|
225
229
|
})
|
|
226
230
|
})
|
|
231
|
+
|
|
232
|
+
// ── Supergroup topic routing (gap #2) ──────────────────────────────────────
|
|
233
|
+
//
|
|
234
|
+
// When the agent requested the credential from inside a forum topic, the
|
|
235
|
+
// grant/save-outcome inbound must carry that topic so the resumed turn's
|
|
236
|
+
// reply lands back in it — not General. The carrier is two-fold and BOTH
|
|
237
|
+
// halves are load-bearing:
|
|
238
|
+
// - top-level `threadId` → the gateway's per-topic busy-key / deliver-
|
|
239
|
+
// until-acked keying (markClaudeBusyForInbound reads it).
|
|
240
|
+
// - `meta.message_thread_id` (stringified) → rendered into the
|
|
241
|
+
// `<channel message_thread_id="…">` XML, which session-tail's
|
|
242
|
+
// parseChannelMeta re-extracts to set currentTurn.sessionThreadId, which
|
|
243
|
+
// the reply tool defaults to. Drop either and the reply mis-routes.
|
|
244
|
+
//
|
|
245
|
+
// DM / non-topic requests must leave BOTH absent (an empty-string or 0
|
|
246
|
+
// thread is a Telegram 400 "message thread not found").
|
|
247
|
+
describe('grant/save outcome topic routing', () => {
|
|
248
|
+
const SAVE_CTX: VaultSaveInboundContext = {
|
|
249
|
+
agent: 'marko',
|
|
250
|
+
key: 'brevo/api-key',
|
|
251
|
+
chat_id: '-1001234567890',
|
|
252
|
+
}
|
|
253
|
+
const THREAD = 4242
|
|
254
|
+
|
|
255
|
+
const grantBuilders = [
|
|
256
|
+
{
|
|
257
|
+
name: 'approved',
|
|
258
|
+
build: (ctx: VaultGrantInboundContext) =>
|
|
259
|
+
buildVaultGrantApprovedInbound({ ctx, grantId: 'vg_x', stageId: 's', operatorId: '1' }),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'denied',
|
|
263
|
+
build: (ctx: VaultGrantInboundContext) =>
|
|
264
|
+
buildVaultGrantDeniedInbound({ ctx, stageId: 's', operatorId: '1' }),
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
const saveBuilders = [
|
|
268
|
+
{
|
|
269
|
+
name: 'save-completed',
|
|
270
|
+
build: (ctx: VaultSaveInboundContext) =>
|
|
271
|
+
buildVaultSaveCompletedInbound({ ctx, stageId: 's', operatorId: '1' }),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'save-failed',
|
|
275
|
+
build: (ctx: VaultSaveInboundContext) =>
|
|
276
|
+
buildVaultSaveFailedInbound({ ctx, stageId: 's', operatorId: '1', reason: 'disk full' }),
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'save-discarded',
|
|
280
|
+
build: (ctx: VaultSaveInboundContext) =>
|
|
281
|
+
buildVaultSaveDiscardedInbound({ ctx, stageId: 's', operatorId: '1' }),
|
|
282
|
+
},
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
for (const { name, build } of grantBuilders) {
|
|
286
|
+
it(`${name}: threadId set → top-level threadId + meta.message_thread_id (stringified)`, () => {
|
|
287
|
+
const msg = build({ ...CTX_READ, threadId: THREAD })
|
|
288
|
+
expect(msg.threadId).toBe(THREAD)
|
|
289
|
+
expect(msg.meta?.message_thread_id).toBe(String(THREAD))
|
|
290
|
+
})
|
|
291
|
+
it(`${name}: threadId absent → both omitted (DM stays thread-less)`, () => {
|
|
292
|
+
const msg = build(CTX_READ)
|
|
293
|
+
expect(msg.threadId).toBeUndefined()
|
|
294
|
+
expect(msg.meta?.message_thread_id).toBeUndefined()
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const { name, build } of saveBuilders) {
|
|
299
|
+
it(`${name}: threadId set → top-level threadId + meta.message_thread_id (stringified)`, () => {
|
|
300
|
+
const msg = build({ ...SAVE_CTX, threadId: THREAD })
|
|
301
|
+
expect(msg.threadId).toBe(THREAD)
|
|
302
|
+
expect(msg.meta?.message_thread_id).toBe(String(THREAD))
|
|
303
|
+
})
|
|
304
|
+
it(`${name}: threadId absent → both omitted (DM stays thread-less)`, () => {
|
|
305
|
+
const msg = build(SAVE_CTX)
|
|
306
|
+
expect(msg.threadId).toBeUndefined()
|
|
307
|
+
expect(msg.meta?.message_thread_id).toBeUndefined()
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
})
|