switchroom 0.19.7 → 0.19.9
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/auth-broker/index.js +9 -8
- package/dist/cli/switchroom.js +903 -695
- package/dist/host-control/main.js +15 -14
- package/dist/vault/approvals/kernel-server.js +5 -4
- package/dist/vault/broker/server.js +9 -8
- package/package.json +1 -1
- package/profiles/default/CLAUDE.md.hbs +4 -4
- package/skills/telegram-formatting/SKILL.md +147 -0
- package/telegram-plugin/dist/gateway/gateway.js +30 -11
- package/telegram-plugin/gateway/gateway.ts +2 -2
- package/telegram-plugin/render/ir.ts +34 -26
- package/telegram-plugin/render/render.ts +12 -3
- package/telegram-plugin/rich-send.ts +16 -10
- package/telegram-plugin/shared/bot-runtime.ts +57 -0
- package/telegram-plugin/tests/format-guard-pins.test.ts +93 -0
- package/telegram-plugin/tests/render/underline-wire-outcome.test.ts +32 -0
- package/telegram-plugin/tests/rich-markdown-guard-transformer.test.ts +121 -0
|
@@ -20614,7 +20614,7 @@ async function waitForConfigRecovery(opts) {
|
|
|
20614
20614
|
}
|
|
20615
20615
|
|
|
20616
20616
|
// src/agents/scaffold.ts
|
|
20617
|
-
import { join as join5, resolve as resolve5 } from "node:path";
|
|
20617
|
+
import { dirname, isAbsolute, join as join5, relative, resolve as resolve5 } from "node:path";
|
|
20618
20618
|
init_atomic();
|
|
20619
20619
|
|
|
20620
20620
|
// src/agents/agent-uid.ts
|
|
@@ -20748,6 +20748,7 @@ var PROFILE_MEMORY_DEFAULTS = {
|
|
|
20748
20748
|
|
|
20749
20749
|
// src/agents/reconcile-default-skills.ts
|
|
20750
20750
|
var warnedMissingPool = new Set;
|
|
20751
|
+
var warnedMissingDefault = new Set;
|
|
20751
20752
|
|
|
20752
20753
|
// src/vault/vault.ts
|
|
20753
20754
|
var KNOWN_VAULT_ARTIFACT_NAMES = new Set([
|
|
@@ -21239,7 +21240,7 @@ import {
|
|
|
21239
21240
|
copyFileSync as copyFileSync2,
|
|
21240
21241
|
unlinkSync as unlinkSync2
|
|
21241
21242
|
} from "node:fs";
|
|
21242
|
-
import { join as join10, dirname as
|
|
21243
|
+
import { join as join10, dirname as dirname7, resolve as resolve8 } from "node:path";
|
|
21243
21244
|
import { createHash as createHash5, randomUUID as randomUUID2, randomBytes } from "node:crypto";
|
|
21244
21245
|
|
|
21245
21246
|
// src/host-control/protocol.ts
|
|
@@ -21488,7 +21489,7 @@ function deniedResponse(request_id, error, duration_ms = 0) {
|
|
|
21488
21489
|
import { createHash as createHash2 } from "node:crypto";
|
|
21489
21490
|
|
|
21490
21491
|
// node_modules/.bun/posthog-node@5.29.2/node_modules/posthog-node/dist/extensions/error-tracking/modifiers/module.node.mjs
|
|
21491
|
-
import { dirname, posix, sep } from "path";
|
|
21492
|
+
import { dirname as dirname2, posix, sep } from "path";
|
|
21492
21493
|
function createModulerModifier() {
|
|
21493
21494
|
const getModuleFromFileName = createGetModuleFromFilename();
|
|
21494
21495
|
return async (frames) => {
|
|
@@ -21497,7 +21498,7 @@ function createModulerModifier() {
|
|
|
21497
21498
|
return frames;
|
|
21498
21499
|
};
|
|
21499
21500
|
}
|
|
21500
|
-
function createGetModuleFromFilename(basePath = process.argv[1] ?
|
|
21501
|
+
function createGetModuleFromFilename(basePath = process.argv[1] ? dirname2(process.argv[1]) : process.cwd(), isWindows = sep === "\\") {
|
|
21501
21502
|
const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
|
|
21502
21503
|
return (filename) => {
|
|
21503
21504
|
if (!filename)
|
|
@@ -25934,7 +25935,7 @@ import {
|
|
|
25934
25935
|
readFileSync as readFileSync6,
|
|
25935
25936
|
writeFileSync as writeFileSync3
|
|
25936
25937
|
} from "node:fs";
|
|
25937
|
-
import { dirname as
|
|
25938
|
+
import { dirname as dirname3 } from "node:path";
|
|
25938
25939
|
import { randomUUID } from "node:crypto";
|
|
25939
25940
|
var DEFAULT_KEY = "phc_qKY87cKWZm6ZyCtk7LcRd2cU8Sg42u7Ywhui5stYCegd";
|
|
25940
25941
|
var DEFAULT_HOST = "https://us.i.posthog.com";
|
|
@@ -25961,7 +25962,7 @@ function getDistinctId() {
|
|
|
25961
25962
|
const id = randomUUID();
|
|
25962
25963
|
cachedDistinctId = id;
|
|
25963
25964
|
try {
|
|
25964
|
-
mkdirSync3(
|
|
25965
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
25965
25966
|
writeFileSync3(path, id, "utf-8");
|
|
25966
25967
|
} catch {}
|
|
25967
25968
|
return id;
|
|
@@ -26660,10 +26661,10 @@ init_atomic();
|
|
|
26660
26661
|
|
|
26661
26662
|
// src/cli/resolve-version.ts
|
|
26662
26663
|
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
26663
|
-
import { dirname as
|
|
26664
|
+
import { dirname as dirname5, join as join7 } from "node:path";
|
|
26664
26665
|
|
|
26665
26666
|
// src/build-info.ts
|
|
26666
|
-
var VERSION = "0.19.
|
|
26667
|
+
var VERSION = "0.19.9";
|
|
26667
26668
|
|
|
26668
26669
|
// src/cli/resolve-version.ts
|
|
26669
26670
|
function readPackageVersion() {
|
|
@@ -26678,7 +26679,7 @@ function readPackageVersion() {
|
|
|
26678
26679
|
}
|
|
26679
26680
|
} catch {}
|
|
26680
26681
|
}
|
|
26681
|
-
dir =
|
|
26682
|
+
dir = dirname5(dir);
|
|
26682
26683
|
}
|
|
26683
26684
|
return null;
|
|
26684
26685
|
}
|
|
@@ -26863,7 +26864,7 @@ function parseRolloutResultLine(stdout) {
|
|
|
26863
26864
|
var import__2 = __toESM(require_commander(), 1);
|
|
26864
26865
|
|
|
26865
26866
|
// src/cli/update.ts
|
|
26866
|
-
import { join as join8, dirname as
|
|
26867
|
+
import { join as join8, dirname as dirname6, resolve as resolve7 } from "node:path";
|
|
26867
26868
|
import { homedir as homedir5 } from "node:os";
|
|
26868
26869
|
|
|
26869
26870
|
// src/agents/lifecycle.ts
|
|
@@ -26985,7 +26986,7 @@ function parseUpdateResultLine(stdout) {
|
|
|
26985
26986
|
var import_yaml5 = __toESM(require_dist(), 1);
|
|
26986
26987
|
import { mkdtempSync, writeFileSync as writeFileSync4, rmSync as rmSync2, existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
|
|
26987
26988
|
import { tmpdir } from "node:os";
|
|
26988
|
-
import { join as join9, isAbsolute as
|
|
26989
|
+
import { join as join9, isAbsolute as isAbsolute3, normalize, basename as basename2 } from "node:path";
|
|
26989
26990
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
26990
26991
|
import { isDeepStrictEqual } from "node:util";
|
|
26991
26992
|
var MAX_PATCH_BYTES = 1024 * 1024;
|
|
@@ -26998,12 +26999,12 @@ function isTargetPathHeader(headerPath, targetBasename) {
|
|
|
26998
26999
|
let p = headerPath;
|
|
26999
27000
|
if (p.startsWith("a/") || p.startsWith("b/"))
|
|
27000
27001
|
p = p.slice(2);
|
|
27001
|
-
if (
|
|
27002
|
+
if (isAbsolute3(p))
|
|
27002
27003
|
return false;
|
|
27003
27004
|
if (p.includes(".."))
|
|
27004
27005
|
return false;
|
|
27005
27006
|
const norm = normalize(p);
|
|
27006
|
-
if (norm.includes("..") ||
|
|
27007
|
+
if (norm.includes("..") || isAbsolute3(norm))
|
|
27007
27008
|
return false;
|
|
27008
27009
|
return norm === targetBasename || basename2(norm) === targetBasename;
|
|
27009
27010
|
}
|
|
@@ -29579,7 +29580,7 @@ ${output.recovery.stderr}` : "";
|
|
|
29579
29580
|
appendAuditRow(row) {
|
|
29580
29581
|
const path2 = this.auditLogPath();
|
|
29581
29582
|
this.auditAppendChain = this.auditAppendChain.then(async () => {
|
|
29582
|
-
await mkdir(
|
|
29583
|
+
await mkdir(dirname7(path2), { recursive: true }).catch(() => {
|
|
29583
29584
|
return;
|
|
29584
29585
|
});
|
|
29585
29586
|
if (this.auditChainState === undefined) {
|
|
@@ -18215,7 +18215,7 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
18215
18215
|
// src/vault/approvals/kernel-server.ts
|
|
18216
18216
|
import * as net from "node:net";
|
|
18217
18217
|
import { mkdirSync as mkdirSync2, chmodSync, chownSync, existsSync as existsSync5, unlinkSync, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
18218
|
-
import { dirname, resolve as resolve6, basename as basename2 } from "node:path";
|
|
18218
|
+
import { dirname as dirname2, resolve as resolve6, basename as basename2 } from "node:path";
|
|
18219
18219
|
import { Database } from "bun:sqlite";
|
|
18220
18220
|
|
|
18221
18221
|
// src/vault/broker/protocol.ts
|
|
@@ -19017,7 +19017,7 @@ function getNonce(db, request_id) {
|
|
|
19017
19017
|
init_schema();
|
|
19018
19018
|
|
|
19019
19019
|
// src/agents/scaffold.ts
|
|
19020
|
-
import { join as join4, resolve as resolve5 } from "node:path";
|
|
19020
|
+
import { dirname, isAbsolute, join as join4, relative, resolve as resolve5 } from "node:path";
|
|
19021
19021
|
init_atomic();
|
|
19022
19022
|
|
|
19023
19023
|
// src/agents/agent-uid.ts
|
|
@@ -19132,6 +19132,7 @@ var PROFILE_MEMORY_DEFAULTS = {
|
|
|
19132
19132
|
};
|
|
19133
19133
|
// src/agents/reconcile-default-skills.ts
|
|
19134
19134
|
var warnedMissingPool = new Set;
|
|
19135
|
+
var warnedMissingDefault = new Set;
|
|
19135
19136
|
|
|
19136
19137
|
// src/telegram/state.ts
|
|
19137
19138
|
init_paths();
|
|
@@ -19224,7 +19225,7 @@ function checkApprovalAclByAgent(listenerAgent, claimedAgentUnit) {
|
|
|
19224
19225
|
var DEFAULT_SOCKET_PARENT = "/run/switchroom/kernel";
|
|
19225
19226
|
var DEFAULT_DB_PATH = "/state/approvals/kernel.db";
|
|
19226
19227
|
function openKernelDb(dbPath) {
|
|
19227
|
-
const dir =
|
|
19228
|
+
const dir = dirname2(dbPath);
|
|
19228
19229
|
mkdirSync2(dir, { recursive: true });
|
|
19229
19230
|
const db = new Database(dbPath, { create: true });
|
|
19230
19231
|
try {
|
|
@@ -19603,7 +19604,7 @@ async function bootstrap(opts) {
|
|
|
19603
19604
|
}
|
|
19604
19605
|
async function main() {
|
|
19605
19606
|
const socketEnv = process.env.SWITCHROOM_KERNEL_SOCKET ?? `${DEFAULT_SOCKET_PARENT}/approval-kernel.sock`;
|
|
19606
|
-
const socketParent =
|
|
19607
|
+
const socketParent = dirname2(resolve6(socketEnv));
|
|
19607
19608
|
const dbPath = process.env.SWITCHROOM_KERNEL_DB_PATH ?? DEFAULT_DB_PATH;
|
|
19608
19609
|
const configPath = process.env.SWITCHROOM_CONFIG;
|
|
19609
19610
|
const operatorUidRaw = process.env.SWITCHROOM_KERNEL_OPERATOR_UID;
|
|
@@ -18220,7 +18220,7 @@ import { mkdirSync as mkdirSync7, chmodSync as chmodSync5, chownSync, existsSync
|
|
|
18220
18220
|
init_schema();
|
|
18221
18221
|
|
|
18222
18222
|
// src/agents/scaffold.ts
|
|
18223
|
-
import { join as join4, resolve as resolve6 } from "node:path";
|
|
18223
|
+
import { dirname as dirname2, isAbsolute, join as join4, relative, resolve as resolve6 } from "node:path";
|
|
18224
18224
|
init_atomic();
|
|
18225
18225
|
|
|
18226
18226
|
// src/agents/agent-uid.ts
|
|
@@ -18840,6 +18840,7 @@ var PROFILE_MEMORY_DEFAULTS = {
|
|
|
18840
18840
|
};
|
|
18841
18841
|
// src/agents/reconcile-default-skills.ts
|
|
18842
18842
|
var warnedMissingPool = new Set;
|
|
18843
|
+
var warnedMissingDefault = new Set;
|
|
18843
18844
|
|
|
18844
18845
|
// src/telegram/state.ts
|
|
18845
18846
|
init_paths();
|
|
@@ -19719,7 +19720,7 @@ function migrateLegacyGrantsDbLocation(newDbPath = getGrantsDbPath(), deps) {
|
|
|
19719
19720
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
19720
19721
|
|
|
19721
19722
|
// src/vault/broker/server.ts
|
|
19722
|
-
import { dirname as
|
|
19723
|
+
import { dirname as dirname7, resolve as resolve9, basename as basename4 } from "node:path";
|
|
19723
19724
|
import * as os3 from "node:os";
|
|
19724
19725
|
import * as path4 from "node:path";
|
|
19725
19726
|
|
|
@@ -19740,7 +19741,7 @@ import {
|
|
|
19740
19741
|
unlinkSync as unlinkSync3
|
|
19741
19742
|
} from "node:fs";
|
|
19742
19743
|
import { createHash as createHash2 } from "node:crypto";
|
|
19743
|
-
import { basename as basename3, dirname as
|
|
19744
|
+
import { basename as basename3, dirname as dirname4, join as join6 } from "node:path";
|
|
19744
19745
|
function vaultLayoutPaths(home2) {
|
|
19745
19746
|
const switchroomRoot = join6(home2, ".switchroom");
|
|
19746
19747
|
return {
|
|
@@ -19827,7 +19828,7 @@ function sha256File(path2) {
|
|
|
19827
19828
|
return createHash2("sha256").update(data).digest("hex");
|
|
19828
19829
|
}
|
|
19829
19830
|
function atomicReplaceWithSymlink(target, linkTarget) {
|
|
19830
|
-
const tmp = join6(
|
|
19831
|
+
const tmp = join6(dirname4(target), `.${basename3(target)}.symlink-tmp`);
|
|
19831
19832
|
if (existsSync8(tmp)) {
|
|
19832
19833
|
try {
|
|
19833
19834
|
unlinkSync3(tmp);
|
|
@@ -22614,7 +22615,7 @@ class VaultBroker {
|
|
|
22614
22615
|
this.passphrase = this.testOpts._testPassphrase;
|
|
22615
22616
|
}
|
|
22616
22617
|
process.umask(63);
|
|
22617
|
-
const parentDir =
|
|
22618
|
+
const parentDir = dirname7(this.socketPath);
|
|
22618
22619
|
mkdirSync7(parentDir, { recursive: true, mode: 448 });
|
|
22619
22620
|
try {
|
|
22620
22621
|
chmodSync5(parentDir, 448);
|
|
@@ -24159,15 +24160,15 @@ class VaultBroker {
|
|
|
24159
24160
|
}
|
|
24160
24161
|
}
|
|
24161
24162
|
function detectVaultLayoutDrift(vaultPath) {
|
|
24162
|
-
const dir =
|
|
24163
|
+
const dir = dirname7(vaultPath);
|
|
24163
24164
|
if (basename4(dir) !== "vault")
|
|
24164
24165
|
return;
|
|
24165
24166
|
if (basename4(vaultPath) !== "vault.enc")
|
|
24166
24167
|
return;
|
|
24167
|
-
const switchroomDir =
|
|
24168
|
+
const switchroomDir = dirname7(dir);
|
|
24168
24169
|
if (basename4(switchroomDir) !== ".switchroom")
|
|
24169
24170
|
return;
|
|
24170
|
-
const home2 =
|
|
24171
|
+
const home2 = dirname7(switchroomDir);
|
|
24171
24172
|
const result = inspectVaultLayout(home2);
|
|
24172
24173
|
if (result.kind === "divergent") {
|
|
24173
24174
|
throw new VaultError(`Vault layout divergence detected at boot: ${result.details.oldPath} and ${result.details.newPath} are both regular files with different content. An older switchroom CLI may have written to the legacy path after migration ran. Run \`switchroom apply\` from the host to surface the recovery recipe (state E refusal with literal \`mv\` commands). See docs/operators/state-e-recovery.md.`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.19.
|
|
4
|
+
"version": "0.19.9",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -72,11 +72,11 @@ The plugin's auto-retain (Stop hook) fires every turn, but in chunked mode each
|
|
|
72
72
|
|
|
73
73
|
### When to synthesize — concrete triggers
|
|
74
74
|
|
|
75
|
-
Auto-recall and auto-retain
|
|
75
|
+
Auto-recall and auto-retain feed the bank but never *synthesize* — that's on you, only if you act on these triggers. Each has a backstop:
|
|
76
76
|
|
|
77
|
-
- **Reflect instead of hand-assembling.**
|
|
78
|
-
- **Propose a model when you keep re-deriving.**
|
|
79
|
-
- **Merge or retire directives when they pile up.** Directives
|
|
77
|
+
- **Reflect instead of hand-assembling.** About to fire 2+ manual `recall`s for one answer ("summarize where Y stands")? Call `mcp__hindsight__reflect` instead. (Backstop: auto-recall injects the top hits every turn — reflect is the escalation.)
|
|
78
|
+
- **Propose a model when you keep re-deriving.** Rebuilt the *same standing answer* across sessions? Propose a mental model via `mcp__switchroom-telegram__mental_model_propose(name, source_query)` (or run the `mental-model-curator` skill). Not for a one-off fact (`retain`) or identity (profile banks own that).
|
|
79
|
+
- **Merge or retire directives when they pile up.** Directives cap at `MAX_DIRECTIVES=15` active per bank — past that the lowest-priority ones drop silently from recall. When they overlap or read stale, run the `mental-model-curator` merge/retire pass (deletes stay operator-approved). (Backstop: `switchroom doctor` WARNs at >12, FAILs at >15.)
|
|
80
80
|
|
|
81
81
|
## Sub-Agent Delegation
|
|
82
82
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telegram-formatting
|
|
3
|
+
description: >
|
|
4
|
+
Use when composing a rich or long Telegram reply and you want the full
|
|
5
|
+
formatting palette with exact syntax — expandable blockquotes, spoilers,
|
|
6
|
+
highlight, code-fence language hints, GFM tables, nested lists — plus the
|
|
7
|
+
escaping rules and the framework's send-time chunking/normalizer behaviour.
|
|
8
|
+
Load it when a message genuinely needs structure, NOT for everyday short
|
|
9
|
+
replies (plain prose already wins there). Teaches judgment first: which
|
|
10
|
+
construct helps the reader vs when plain text is better. Do NOT use for
|
|
11
|
+
deciding whether to reply, or for non-Telegram output.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Telegram formatting — the full palette
|
|
15
|
+
|
|
16
|
+
Every outbound Switchroom message renders as raw GFM markdown over Telegram
|
|
17
|
+
Bot API 10.1 rich messages (`telegram-plugin/rich-send.ts` `richMessage(md)` →
|
|
18
|
+
`{ markdown }` → `sendRichMessage` / `editMessageText({ markdown })`). No HTML,
|
|
19
|
+
no `parse_mode`. This skill is the depth reference behind the boot-injected
|
|
20
|
+
floor card: the full construct vocabulary, correct syntax, escaping, and the
|
|
21
|
+
send-time behaviour you can rely on.
|
|
22
|
+
|
|
23
|
+
## Judgment first — reach for structure only when it helps the reader
|
|
24
|
+
|
|
25
|
+
The floor card's stance is the law here too: **structure exists for the reader,
|
|
26
|
+
not the writer.** Loading this skill does not mean "use everything below." Match
|
|
27
|
+
the construct to the message.
|
|
28
|
+
|
|
29
|
+
- **Short answers (a line or two): plain prose, no formatting.** "on it,
|
|
30
|
+
pulling the logs now" is already perfect. No bold, no bullets, no headings.
|
|
31
|
+
Most replies live here — don't dress them up.
|
|
32
|
+
- **Default: light structure.** Bold ONLY the one key fact or answer, never
|
|
33
|
+
more. A list only for 3+ genuinely parallel items the reader will scan or
|
|
34
|
+
compare; two items or a flowing thought stay prose. `code spans` for
|
|
35
|
+
identifiers (filenames, commands, config keys, error codes) — tap-to-copy.
|
|
36
|
+
- **Long / multi-section answers may add the rich constructs below** — tables,
|
|
37
|
+
headings, blockquotes, expandable blocks, fences — but only when they cut the
|
|
38
|
+
reader's effort. If the structure doesn't reduce scanning effort, drop it. A
|
|
39
|
+
two-item bullet list is worse than a sentence; a heading on a three-line reply
|
|
40
|
+
is noise. When in doubt, shorter and plainer wins.
|
|
41
|
+
|
|
42
|
+
Over-bolded messages (most of the text bold, whole paragraphs/lists bolded) get
|
|
43
|
+
their bold stripped at send time — so bold sparingly and deliberately.
|
|
44
|
+
|
|
45
|
+
## Full rich vocabulary
|
|
46
|
+
|
|
47
|
+
### Inline spans
|
|
48
|
+
|
|
49
|
+
| Effect | Markdown | When / notes |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| Bold | `**text**` | The one key fact or answer, not decoration. |
|
|
52
|
+
| Italic | `*text*` or `_text_` | Light emphasis, labels, asides. |
|
|
53
|
+
| Strikethrough | `~~text~~` | Retractions, "was X now Y". |
|
|
54
|
+
| Spoiler | `\|\|text\|\|` | Only for an opt-in surprise or a reveal the reader chose to wait for (a punchline they want suspended) — NEVER to hide an answer someone is asking for or anxious about; when in doubt, show it plainly. Surfaces as a `spoiler` entity on the wire (live-verified 2026-07). |
|
|
55
|
+
| Highlight / marked | `==text==` | Surfaces as a `marked` entity on the wire (live-verified 2026-07). `=` is an `escapeMarkdown` special, so dynamic text won't trigger it by accident. |
|
|
56
|
+
| Inline code | `` `text` `` | Identifiers, tap-to-copy. Content is literal — no escaping inside. |
|
|
57
|
+
| Link | `[label](https://…)` | Standard GFM link. |
|
|
58
|
+
|
|
59
|
+
**Do NOT rely on these — they don't render as intended:**
|
|
60
|
+
|
|
61
|
+
- **Underline** — there is NO underline token on this path. `__text__` renders
|
|
62
|
+
as **bold** (Telegram's rich-message markdown parser reads a `__…__` run
|
|
63
|
+
identically to `**…**`, live-verified against the Bot API 2026-07). Use `**`
|
|
64
|
+
for bold and don't reach for underline.
|
|
65
|
+
- **Subscript** `~text~` (single tilde) and **superscript** `^text^` fall back
|
|
66
|
+
to literal text in rich messages — avoid (write "squared", not `x^2^`).
|
|
67
|
+
- **Custom emoji** (premium custom-emoji entity) renders as a normal emoji for
|
|
68
|
+
non-premium viewers — don't rely on it to carry meaning.
|
|
69
|
+
|
|
70
|
+
(Inline math `$…$`, HTML `<details>`/collapsible, and footnotes `[^1]` are NOT
|
|
71
|
+
supported on this path — do not emit them; they degrade to literal or neutralised
|
|
72
|
+
text.)
|
|
73
|
+
|
|
74
|
+
### Block types
|
|
75
|
+
|
|
76
|
+
- **Code fence** — ` ```lang ` … ` ``` `. Multi-line literal output (diffs,
|
|
77
|
+
logs, JSON, command blocks). The language hint (`diff`, `json`, `bash`, …)
|
|
78
|
+
sharpens syntax rendering — use it. Content inside is verbatim, never escape
|
|
79
|
+
it; the only hazard is an embedded ` ``` ` closing the block early, which the
|
|
80
|
+
framework defuses (`preBlock` in `shared/bot-runtime.ts`).
|
|
81
|
+
- **Preformatted block** — a code fence with NO language, for fixed-width
|
|
82
|
+
non-code (ASCII tables, aligned columns).
|
|
83
|
+
- **Bulleted list** — `- item` (also `*` / `+`). 3+ parallel items only.
|
|
84
|
+
- **Numbered list** — `1. item`. Ordered steps or ranked items.
|
|
85
|
+
- **Nested lists** — indent sub-items; tight (no blank lines) vs loose (blank
|
|
86
|
+
lines between items) both render. 3-level nesting is live-verified.
|
|
87
|
+
- **Task list** — `- [ ] todo` / `- [x] done`.
|
|
88
|
+
- **Table** — GFM pipe table (`| col | col |` + `| --- | --- |` separator),
|
|
89
|
+
optional per-column alignment (`:---`, `:---:`, `---:`). 2-D data ONLY (rows ×
|
|
90
|
+
columns) — not a substitute for prose. Chunk-safe: `splitMarkdownChunks` never
|
|
91
|
+
bisects a row.
|
|
92
|
+
- **Blockquote** — `> quoted`. Quoted text or an indented continuation; the
|
|
93
|
+
right way to indent, because Telegram drops leading whitespace.
|
|
94
|
+
- **Expandable blockquote** — `**> …` (Bot API 10.1). A long quote/aside the
|
|
95
|
+
reader can collapse and expand. The flagship rich construct — use it for a
|
|
96
|
+
long quotation, a stack trace, or a detailed aside you don't want dominating
|
|
97
|
+
the message. First line carries the `**> ` marker; continuation lines use `> `.
|
|
98
|
+
- **Section heading** — `#` … `######`. Only in a genuinely long, multi-section
|
|
99
|
+
answer. Never on a short reply.
|
|
100
|
+
- **Divider** — `---` (thematic break). Heavy horizontal rule between genuinely
|
|
101
|
+
separate sections. Use sparingly.
|
|
102
|
+
- **Collage / album** — multiple images grouped in one message (media group).
|
|
103
|
+
Send via the attachment path, not markdown.
|
|
104
|
+
|
|
105
|
+
## Escaping rules
|
|
106
|
+
|
|
107
|
+
Dynamic content (filenames, ids, arbitrary user text) interpolated into a
|
|
108
|
+
hand-built markdown card MUST be escaped so it renders LITERALLY instead of
|
|
109
|
+
being parsed as formatting. Use `escapeMarkdown(value)` from
|
|
110
|
+
`telegram-plugin/format.ts`.
|
|
111
|
+
|
|
112
|
+
`escapeMarkdown` escapes exactly the characters that trigger INLINE formatting:
|
|
113
|
+
backslash, `` ` ``, `*`, `_`, `~`, `=`, `[`, `]`, `|` — the set `` \`*_~=[]| ``.
|
|
114
|
+
The backslash is escaped first so it never double-escapes a following special.
|
|
115
|
+
It deliberately does **not** escape `.` `-` `+` `#` `(` `)` `{` `}` `!` `>`:
|
|
116
|
+
those are only meaningful at line-start or in link/structure context, and
|
|
117
|
+
escaping them mid-word would litter filenames (`foo.ts`), versions (`v1.2-rc`),
|
|
118
|
+
and URLs with visible backslashes.
|
|
119
|
+
|
|
120
|
+
- **Bold/italic a dynamic value:** `` `**${escapeMarkdown(value)}**` ``.
|
|
121
|
+
- **Code-span a dynamic value:** `` `\`${value}\`` `` — code spans need NO
|
|
122
|
+
escaping (backtick content is already literal). This is the preferred, safest
|
|
123
|
+
way to render any identifier.
|
|
124
|
+
|
|
125
|
+
## Send-time behaviour you can rely on
|
|
126
|
+
|
|
127
|
+
- **Chunking.** Hard cap is `RICH_MESSAGE_MAX_CHARS = 32768` (32768 accepted,
|
|
128
|
+
32769 rejected — the single constant, never re-derive it). A longer body is
|
|
129
|
+
split by `splitMarkdownChunks(text, 32768)` in `format.ts`: it cuts at the
|
|
130
|
+
largest safe boundary (blank line → newline → space), **never bisects a fenced
|
|
131
|
+
code block** (`backOffOpenFence`) and **never bisects a table row**
|
|
132
|
+
(`backOffTableRow`). A single indivisible region larger than the cap is
|
|
133
|
+
emitted whole and re-split / hard-sliced at send time rather than hanging.
|
|
134
|
+
Long before 32768, ask whether a wall of text is the right answer at all.
|
|
135
|
+
- **Typography normalizer (deterministic, every message).** Block spacing (one
|
|
136
|
+
blank line between distinct blocks), em/en dashes, and `•` bullet markers are
|
|
137
|
+
rewritten at send time. A LONE `\n` between two prose paragraphs is promoted
|
|
138
|
+
to a real visual break; runs of 3+ newlines collapse to `\n\n`; lists, tables,
|
|
139
|
+
code, and existing `\n\n` gaps are left exactly as written. Don't hand-tune
|
|
140
|
+
spacing or fight the normalizer — write the content, the gateway makes the
|
|
141
|
+
typography consistent.
|
|
142
|
+
|
|
143
|
+
## The one rule that outranks everything here
|
|
144
|
+
|
|
145
|
+
You loaded this skill to format a rich message well — but the best formatting is
|
|
146
|
+
still the least that serves the reader. Use the palette to make a genuinely
|
|
147
|
+
complex answer scannable, never to decorate a simple one.
|
|
@@ -35216,7 +35216,7 @@ function renderAuthLine(state7, agentName3, now = Date.now()) {
|
|
|
35216
35216
|
|
|
35217
35217
|
// gateway/quota-cache.ts
|
|
35218
35218
|
import { existsSync as existsSync41, readFileSync as readFileSync42, writeFileSync as writeFileSync35, mkdirSync as mkdirSync36 } from "fs";
|
|
35219
|
-
import { join as join46, dirname as
|
|
35219
|
+
import { join as join46, dirname as dirname15 } from "path";
|
|
35220
35220
|
function defaultCachePath() {
|
|
35221
35221
|
return process.env.SWITCHROOM_QUOTA_CACHE_PATH ?? join46(process.env.HOME ?? "/tmp", ".switchroom", "quota-cache.json");
|
|
35222
35222
|
}
|
|
@@ -35253,7 +35253,7 @@ function writeQuotaCache(result, opts = {}) {
|
|
|
35253
35253
|
result
|
|
35254
35254
|
};
|
|
35255
35255
|
try {
|
|
35256
|
-
mkdirSync36(
|
|
35256
|
+
mkdirSync36(dirname15(path2), { recursive: true });
|
|
35257
35257
|
writeFileSync35(path2, JSON.stringify(entry, null, 2), { mode: 384 });
|
|
35258
35258
|
} catch {}
|
|
35259
35259
|
}
|
|
@@ -36070,7 +36070,7 @@ var init_boot_probes = __esm(() => {
|
|
|
36070
36070
|
|
|
36071
36071
|
// gateway/boot-issue-cache.ts
|
|
36072
36072
|
import { existsSync as existsSync43, readFileSync as readFileSync44, writeFileSync as writeFileSync36, mkdirSync as mkdirSync37, renameSync as renameSync17 } from "fs";
|
|
36073
|
-
import { dirname as
|
|
36073
|
+
import { dirname as dirname16 } from "path";
|
|
36074
36074
|
function fingerprintProbe(key, r) {
|
|
36075
36075
|
if (r.status === "ok")
|
|
36076
36076
|
return `${key}:ok`;
|
|
@@ -36196,7 +36196,7 @@ function applyAndSave(path2, cache, diff) {
|
|
|
36196
36196
|
}
|
|
36197
36197
|
}
|
|
36198
36198
|
try {
|
|
36199
|
-
mkdirSync37(
|
|
36199
|
+
mkdirSync37(dirname16(path2), { recursive: true });
|
|
36200
36200
|
const tmp = `${path2}.tmp`;
|
|
36201
36201
|
writeFileSync36(tmp, JSON.stringify(next), { mode: 384 });
|
|
36202
36202
|
renameSync17(tmp, path2);
|
|
@@ -36213,7 +36213,7 @@ var init_boot_issue_cache = __esm(() => {
|
|
|
36213
36213
|
// gateway/config-snapshot.ts
|
|
36214
36214
|
import { createHash as createHash3 } from "crypto";
|
|
36215
36215
|
import { existsSync as existsSync44, readFileSync as readFileSync45, writeFileSync as writeFileSync37, mkdirSync as mkdirSync38, renameSync as renameSync18 } from "fs";
|
|
36216
|
-
import { dirname as
|
|
36216
|
+
import { dirname as dirname17 } from "path";
|
|
36217
36217
|
function hashStringArray(items) {
|
|
36218
36218
|
if (!items || items.length === 0)
|
|
36219
36219
|
return null;
|
|
@@ -36311,7 +36311,7 @@ function loadSnapshot(path2, now = Date.now) {
|
|
|
36311
36311
|
}
|
|
36312
36312
|
function persistSnapshot(path2, snapshot) {
|
|
36313
36313
|
try {
|
|
36314
|
-
mkdirSync38(
|
|
36314
|
+
mkdirSync38(dirname17(path2), { recursive: true });
|
|
36315
36315
|
const tmp = `${path2}.tmp`;
|
|
36316
36316
|
writeFileSync37(tmp, JSON.stringify(snapshot), { mode: 384 });
|
|
36317
36317
|
renameSync18(tmp, path2);
|
|
@@ -41612,6 +41612,7 @@ function shouldEmitShadowTrace(eventKind, effectCount, globalKind, verbose = gwT
|
|
|
41612
41612
|
}
|
|
41613
41613
|
|
|
41614
41614
|
// shared/bot-runtime.ts
|
|
41615
|
+
init_rich_send();
|
|
41615
41616
|
var tgPostTagStore = new AsyncLocalStorage;
|
|
41616
41617
|
function escapeHtmlForTg(text) {
|
|
41617
41618
|
return text.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
@@ -62511,6 +62512,7 @@ import { createHash as createHash2 } from "crypto";
|
|
|
62511
62512
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
62512
62513
|
init_flood_circuit_breaker();
|
|
62513
62514
|
init_format();
|
|
62515
|
+
init_rich_send();
|
|
62514
62516
|
var tgPostTagStore2 = new AsyncLocalStorage2;
|
|
62515
62517
|
function _getTgPostTags() {
|
|
62516
62518
|
return tgPostTagStore2.getStore();
|
|
@@ -62555,6 +62557,21 @@ function installTgPostLogger(bot) {
|
|
|
62555
62557
|
}
|
|
62556
62558
|
});
|
|
62557
62559
|
}
|
|
62560
|
+
function installRichMarkdownGuard(bot) {
|
|
62561
|
+
bot.api.config.use(async (prev, method, payload, signal) => {
|
|
62562
|
+
if ((method === "sendRichMessage" || method === "editMessageText") && payload != null) {
|
|
62563
|
+
const p = payload;
|
|
62564
|
+
const rich = p.rich_message;
|
|
62565
|
+
if (rich != null && typeof rich.markdown === "string") {
|
|
62566
|
+
const guarded = guardAccidentalFormatting(rich.markdown);
|
|
62567
|
+
if (guarded !== rich.markdown) {
|
|
62568
|
+
p.rich_message = { ...rich, markdown: guarded };
|
|
62569
|
+
}
|
|
62570
|
+
}
|
|
62571
|
+
}
|
|
62572
|
+
return prev(method, payload, signal);
|
|
62573
|
+
});
|
|
62574
|
+
}
|
|
62558
62575
|
|
|
62559
62576
|
// flood-circuit-breaker.ts
|
|
62560
62577
|
import {
|
|
@@ -80984,7 +81001,7 @@ async function discoverModels(agentName3, opts = {}) {
|
|
|
80984
81001
|
}
|
|
80985
81002
|
|
|
80986
81003
|
// ../src/agents/scaffold.ts
|
|
80987
|
-
import { join as join38, resolve as resolve8 } from "node:path";
|
|
81004
|
+
import { dirname as dirname14, isAbsolute, join as join38, relative, resolve as resolve8 } from "node:path";
|
|
80988
81005
|
init_atomic();
|
|
80989
81006
|
|
|
80990
81007
|
// ../src/agents/agent-uid.ts
|
|
@@ -81088,6 +81105,7 @@ var PROFILE_MEMORY_DEFAULTS = {
|
|
|
81088
81105
|
};
|
|
81089
81106
|
// ../src/agents/reconcile-default-skills.ts
|
|
81090
81107
|
var warnedMissingPool = new Set;
|
|
81108
|
+
var warnedMissingDefault = new Set;
|
|
81091
81109
|
|
|
81092
81110
|
// ../src/telegram/state.ts
|
|
81093
81111
|
init_paths();
|
|
@@ -93551,10 +93569,10 @@ function startGatewayHeartbeat(stateDir, intervalMs = GATEWAY_HEARTBEAT_INTERVAL
|
|
|
93551
93569
|
}
|
|
93552
93570
|
|
|
93553
93571
|
// ../src/build-info.ts
|
|
93554
|
-
var VERSION = "0.19.
|
|
93555
|
-
var COMMIT_SHA = "
|
|
93556
|
-
var COMMIT_DATE = "2026-07-
|
|
93557
|
-
var LATEST_PR =
|
|
93572
|
+
var VERSION = "0.19.9";
|
|
93573
|
+
var COMMIT_SHA = "9d791e63";
|
|
93574
|
+
var COMMIT_DATE = "2026-07-21T06:12:01Z";
|
|
93575
|
+
var LATEST_PR = 3492;
|
|
93558
93576
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
93559
93577
|
|
|
93560
93578
|
// gateway/boot-version.ts
|
|
@@ -106426,6 +106444,7 @@ async function initGatewayBot() {
|
|
|
106426
106444
|
}
|
|
106427
106445
|
bot = new import_grammy15.Bot(TOKEN);
|
|
106428
106446
|
installTgPostLogger(bot);
|
|
106447
|
+
installRichMarkdownGuard(bot);
|
|
106429
106448
|
installUpdateTap(bot, (line) => process.stderr.write(line));
|
|
106430
106449
|
bot.api.config.use(async (prev, method, payload, signal) => {
|
|
106431
106450
|
try {
|
|
@@ -295,7 +295,7 @@ import {
|
|
|
295
295
|
} from '../retry-api-call.js'
|
|
296
296
|
import { createSendGate, sendGateConfigFromEnv, isSendGateShed } from '../send-gate.js'
|
|
297
297
|
import { createStatsLogger, createFloodWindowObserver } from '../send-gate-observability.js'
|
|
298
|
-
import { installTgPostLogger, withTgPostTags } from '../shared/bot-runtime.js'
|
|
298
|
+
import { installTgPostLogger, installRichMarkdownGuard, withTgPostTags } from '../shared/bot-runtime.js'
|
|
299
299
|
import {
|
|
300
300
|
floodStatePath,
|
|
301
301
|
floodWindowsPath,
|
|
@@ -22970,7 +22970,7 @@ async function initGatewayBot(): Promise<void> {
|
|
|
22970
22970
|
}
|
|
22971
22971
|
|
|
22972
22972
|
bot = new Bot(TOKEN)
|
|
22973
|
-
installTgPostLogger(bot)
|
|
22973
|
+
installTgPostLogger(bot); installRichMarkdownGuard(bot) // #3252/#3463: universal fmt guard installed after logger (composes outermost); see installRichMarkdownGuard docblock
|
|
22974
22974
|
|
|
22975
22975
|
// Diagnostic update tap (#3300): one compact line per received update, logged
|
|
22976
22976
|
// BEFORE any specific handler runs, so a routing-layer drop is diagnosable
|
|
@@ -1,38 +1,44 @@
|
|
|
1
|
-
// Typed intermediate representation (IR) for the Telegram
|
|
1
|
+
// Typed intermediate representation (IR) for the Telegram rich-markdown render
|
|
2
|
+
// engine. (Historical note: this file and render.ts were named for an "HTML
|
|
3
|
+
// render engine" during Increment 1, before the Bot API 10.1 migration (#2669)
|
|
4
|
+
// made GFM `{ markdown }` the live send path. There is NO HTML anywhere on the
|
|
5
|
+
// outbound path today — the renderer in render.ts emits raw GFM markdown for
|
|
6
|
+
// the `markdown` field of `InputRichMessageMarkdown`.)
|
|
2
7
|
//
|
|
3
8
|
// This is the parser <-> renderer contract. `parse()` (parse.ts) folds an
|
|
4
|
-
// mdast tree into this shape;
|
|
5
|
-
//
|
|
6
|
-
// is no renderer yet.
|
|
9
|
+
// mdast tree into this shape; `render.ts` walks it and emits Telegram
|
|
10
|
+
// rich-message GFM markdown.
|
|
7
11
|
//
|
|
8
12
|
// Every node carries `{ start, end }` UTF-16 source offsets copied verbatim
|
|
9
13
|
// from mdast `position.start.offset` / `position.end.offset`. They are UTF-16
|
|
10
14
|
// code-unit indices into the original markdown string, so
|
|
11
15
|
// `source.slice(node.start, node.end)` round-trips to the node's source text.
|
|
12
16
|
//
|
|
13
|
-
//
|
|
17
|
+
// IR node -> emitted GFM markdown (see render.ts `renderInline`/block render):
|
|
14
18
|
//
|
|
15
19
|
// Inline
|
|
16
|
-
// plain ->
|
|
17
|
-
// bold ->
|
|
18
|
-
// italic ->
|
|
19
|
-
// underline ->
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
20
|
+
// plain -> raw text (escapeMarkdown'd)
|
|
21
|
+
// bold -> `**…**`
|
|
22
|
+
// italic -> `*…*`
|
|
23
|
+
// underline -> `__…__` — NOTE: the wire renders `__…__` as BOLD, not
|
|
24
|
+
// underline. Telegram's rich-message markdown has no underline
|
|
25
|
+
// token (live-verified, see reference/telegram-formatting-guide.md).
|
|
26
|
+
// The node preserves the author's `__` bytes faithfully; it is
|
|
27
|
+
// a distinct IR node but NOT a distinct wire style.
|
|
28
|
+
// strike -> `~~…~~`
|
|
29
|
+
// spoiler -> `||…||`
|
|
30
|
+
// highlight -> `==…==` (Bot API 10.1 marked entity)
|
|
31
|
+
// code -> `` `…` ``
|
|
32
|
+
// link -> `[…](…)`
|
|
25
33
|
//
|
|
26
34
|
// Block
|
|
27
35
|
// paragraph -> children joined; blocks separated by "\n\n"
|
|
28
|
-
// heading ->
|
|
29
|
-
// blockquote ->
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// thematic-break -> a horizontal-rule text line (e.g. "───")
|
|
35
|
-
// table -> monospaced <pre> table (Telegram HTML has no <table>)
|
|
36
|
+
// heading -> `#`…`######` line
|
|
37
|
+
// blockquote -> `> …` (expandable === true -> `**> …` expandable blockquote)
|
|
38
|
+
// code-block -> ```` ```lang … ``` ````
|
|
39
|
+
// list -> line-per-item with `-`/`1.` markers
|
|
40
|
+
// thematic-break -> `---` thematic break
|
|
41
|
+
// table -> GFM pipe table
|
|
36
42
|
|
|
37
43
|
export interface Pos {
|
|
38
44
|
/** UTF-16 code-unit offset of the node's first char (mdast position.start.offset). */
|
|
@@ -60,10 +66,12 @@ export interface ItalicNode extends Pos {
|
|
|
60
66
|
children: Inline[];
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
65
|
-
* GFM/micromark folds both into a single `strong` mdast node.
|
|
66
|
-
*
|
|
69
|
+
/** A `__…__` double-underscore run. `parse.ts` keeps it as a distinct node
|
|
70
|
+
* (separate from `**…**` bold) by looking at the source delimiter, even though
|
|
71
|
+
* GFM/micromark folds both into a single `strong` mdast node. NOTE: on the
|
|
72
|
+
* Telegram wire this renders as BOLD, not a distinct underline style — Bot API
|
|
73
|
+
* 10.1 rich markdown has no underline entity here, so the round-trip is faithful
|
|
74
|
+
* but the delivered text is bold. Kept distinct only to preserve authoring intent. */
|
|
67
75
|
export interface UnderlineNode extends Pos {
|
|
68
76
|
type: "underline";
|
|
69
77
|
children: Inline[];
|