switchroom 0.21.10 → 0.21.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cli/switchroom.js +143 -27
  2. package/dist/host-control/main.js +1 -1
  3. package/package.json +5 -2
  4. package/skills/dev-protocol/SKILL.md +14 -10
  5. package/skills/switchroom-release/SKILL.md +11 -5
  6. package/telegram-plugin/dist/gateway/gateway.js +385 -35
  7. package/telegram-plugin/format.ts +56 -0
  8. package/telegram-plugin/gateway/answer-route-overrides.ts +163 -0
  9. package/telegram-plugin/gateway/answer-thread-resolve.test.ts +175 -1
  10. package/telegram-plugin/gateway/answer-thread-resolve.ts +58 -6
  11. package/telegram-plugin/gateway/escalation-staleness.ts +526 -0
  12. package/telegram-plugin/gateway/gateway.ts +61 -68
  13. package/telegram-plugin/gateway/obligation-wiring.ts +91 -3
  14. package/telegram-plugin/gateway/outbound-send-path.ts +55 -1
  15. package/telegram-plugin/gateway/reply-route-log.test.ts +134 -0
  16. package/telegram-plugin/gateway/reply-route-log.ts +118 -0
  17. package/telegram-plugin/gateway/stream-render.ts +1 -1
  18. package/telegram-plugin/history.ts +21 -0
  19. package/telegram-plugin/registry/subagents-bugs.test.ts +3 -3
  20. package/telegram-plugin/render/html-fold.ts +22 -4
  21. package/telegram-plugin/render/parse.ts +9 -1
  22. package/telegram-plugin/render/render.ts +14 -13
  23. package/telegram-plugin/tests/answer-route-side-effect.test.ts +111 -0
  24. package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +3 -3
  25. package/telegram-plugin/tests/escalation-staleness.test.ts +1275 -0
  26. package/telegram-plugin/tests/forwarded-rich-message-coalesce.test.ts +6 -6
  27. package/telegram-plugin/tests/forwarded-rich-message.test.ts +8 -8
  28. package/telegram-plugin/tests/history.test.ts +78 -0
  29. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +29 -1
  30. package/telegram-plugin/tests/orphaned-db-sweep.test.ts +17 -1
  31. package/telegram-plugin/tests/render/html-dialect-content-loss.test.ts +76 -3
  32. package/telegram-plugin/tests/send-reply-golden.test.ts +192 -3
  33. package/telegram-plugin/tests/status-pin.test.ts +2 -2
  34. package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +2 -2
  35. package/telegram-plugin/tests/subagent-progress-inbound-builder.test.ts +2 -2
  36. package/telegram-plugin/tests/telegram-format.test.ts +52 -0
  37. package/telegram-plugin/tests/turn-supersede-finalizes-prior-card.test.ts +1 -1
  38. package/telegram-plugin/tests/worker-origin-gap-dispatch.test.ts +1 -1
  39. package/telegram-plugin/uat/scenarios/jtbd-supergroup-reply-channel.test.ts +1 -1
  40. package/telegram-plugin/worker-activity-feed.ts +2 -2
@@ -20,10 +20,62 @@ import {
20
20
  escapeMarkdown,
21
21
  splitMarkdownChunks,
22
22
  addParagraphSpacers,
23
+ codeFenceFor,
23
24
  PARAGRAPH_SPACER,
24
25
  RICH_MESSAGE_MAX_CHARS,
25
26
  } from '../format.js'
26
27
 
28
+ // ---------------------------------------------------------------------------
29
+ // codeFenceFor — the single shared fence-width rule (#4702 follow-up)
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe('codeFenceFor', () => {
33
+ test('defaults to three backticks when the body carries no long run', () => {
34
+ expect(codeFenceFor('')).toBe('```')
35
+ expect(codeFenceFor('plain body')).toBe('```')
36
+ expect(codeFenceFor('a ` b `` c')).toBe('```')
37
+ })
38
+
39
+ test('widens to one longer than the longest run in the body', () => {
40
+ expect(codeFenceFor('a\n```\nb')).toBe('````')
41
+ expect(codeFenceFor('a\n````\nb')).toBe('`````')
42
+ expect(codeFenceFor('` `` ``` ````')).toBe('`````')
43
+ expect(codeFenceFor('x```y')).toBe('````')
44
+ })
45
+
46
+ test('the delimiter is strictly longer than every run in the body', () => {
47
+ // The property the wire depends on: with the open/close pair the only
48
+ // runs of that width, nothing in the body can close the fence early or
49
+ // leave one open.
50
+ for (const body of ['no ticks', '`', '```', '``````', 'a ``` b ````` c']) {
51
+ const fence = codeFenceFor(body)
52
+ for (const run of body.match(/`+/g) ?? []) {
53
+ expect(run.length).toBeLessThan(fence.length)
54
+ }
55
+ }
56
+ })
57
+
58
+ test('does not blow the stack on a body with very many backtick runs', () => {
59
+ // The former `Math.max(0, ...runs.map(…))` form passed one argument per
60
+ // run and threw `RangeError: Maximum call stack size exceeded` past the
61
+ // engine's argument limit, from inside the render path.
62
+ //
63
+ // THE COUNT IS LOAD-BEARING and has to clear the argument limit of EVERY
64
+ // engine that runs this file, because the limit is engine-dependent and
65
+ // this file is collected by BOTH runners: `bun test` (the shipping
66
+ // runtime, via the `tests/` positional in scripts/bun-test-ci.sh) and
67
+ // vitest/Node. Measured on the pinned toolchain by bisecting the former
68
+ // spread form on this exact body shape — highest surviving run count is
69
+ // 638,621 on Bun 1.3.13 and 125,289 on Node 22.23.2. So the 200_000 this
70
+ // started at passed on Bun WITH the bug reinstated: it asserted nothing
71
+ // in the shipping runtime. 1M is ~1.57x the higher (Bun) limit, and the
72
+ // shipping loop scans the resulting 2.9 MB body in single-digit
73
+ // milliseconds, so the margin is nearly free.
74
+ const body = 'x `'.repeat(1_000_000)
75
+ expect(codeFenceFor(body)).toBe('```')
76
+ })
77
+ })
78
+
27
79
  // ---------------------------------------------------------------------------
28
80
  // escapeMarkdown — narrowed inline-special escaper (#2669)
29
81
  // ---------------------------------------------------------------------------
@@ -7,7 +7,7 @@
7
7
  * card was never finalized, never unpinned (`fg:<statusKey>` stayed claimed
8
8
  * forever), froze on its last landed edit, and left NO `turn-lifecycle clear`
9
9
  * line to explain it. Proof from the field: carrie's turn
10
- * `-1004223464247:_#1078` has a `turn-lifecycle set reason=enqueue` at
10
+ * `-1004444444444:_#1078` has a `turn-lifecycle set reason=enqueue` at
11
11
  * 2026-07-28T18:13:37.554Z and no `clear` anywhere in the log; its stale pinned
12
12
  * card was still on screen in the operator's screenshot.
13
13
  *
@@ -47,7 +47,7 @@ import { decideSubagentHandback } from '../gateway/subagent-handback-inbound-bui
47
47
 
48
48
  type Db = ReturnType<typeof openTurnsDbInMemory>
49
49
 
50
- const TOPIC_CHAT = '-1004223464247'
50
+ const TOPIC_CHAT = '-1004444444444'
51
51
  const TOPIC_THREAD = '77'
52
52
  const OWNER_DM = '555'
53
53
 
@@ -35,7 +35,7 @@ import { expectMessage } from "../assertions.js";
35
35
 
36
36
  const AGENT = "test-harness";
37
37
 
38
- /** Bot API marked id of the test supergroup, e.g. -1005164217975. */
38
+ /** Bot API marked id of the test supergroup, e.g. -1006666666666. */
39
39
  const SUPERGROUP_ID = Number.parseInt(process.env.SWITCHROOM_UAT_CHAT_ID ?? "", 10);
40
40
 
41
41
  /** A supergroup turn is a full inbound→claude→outbound round-trip; give
@@ -850,8 +850,8 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
850
850
  *
851
851
  * The separator and the topic-less sentinel are both load-bearing. This used
852
852
  * to be `` `${chatId} ${threadId ?? ''}` ``, which for a topic-less group
853
- * rendered as `-1004223464247 ` — a key ending in a bare space. On disk that
854
- * became `"pinKey": "wk:group:-1004223464247 "`: trailing whitespace that
853
+ * rendered as `-1004444444444 ` — a key ending in a bare space. On disk that
854
+ * became `"pinKey": "wk:group:-1004444444444 "`: trailing whitespace that
855
855
  * survives JSON round-trips, is invisible in every log line and grep, and
856
856
  * cannot be distinguished by eye from the same chat WITH a topic. Any operator
857
857
  * or tool reconciling rows by chat would silently treat the two as one.