switchroom 0.19.41 → 0.19.43

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 (90) hide show
  1. package/dist/agent-scheduler/index.js +23 -3
  2. package/dist/auth-broker/index.js +54 -9
  3. package/dist/cli/notion-write-pretool.mjs +23 -3
  4. package/dist/cli/switchroom.js +1956 -900
  5. package/dist/host-control/main.js +227 -30
  6. package/dist/vault/approvals/kernel-server.js +54 -9
  7. package/dist/vault/broker/server.js +54 -9
  8. package/package.json +3 -3
  9. package/profiles/_base/start.sh.hbs +82 -4
  10. package/telegram-plugin/bridge/bridge.ts +1 -1
  11. package/telegram-plugin/card-layout.ts +63 -1
  12. package/telegram-plugin/dist/bridge/bridge.js +4 -1
  13. package/telegram-plugin/dist/gateway/gateway.js +1318 -693
  14. package/telegram-plugin/dist/server.js +5 -2
  15. package/telegram-plugin/flood-429-ledger.ts +5 -3
  16. package/telegram-plugin/flood-circuit-breaker.ts +11 -3
  17. package/telegram-plugin/format.ts +223 -13
  18. package/telegram-plugin/gateway/approval-callback-consume-record.test.ts +132 -0
  19. package/telegram-plugin/gateway/boot-card.ts +7 -2
  20. package/telegram-plugin/gateway/flood-reply-queue.ts +5 -0
  21. package/telegram-plugin/gateway/gateway.ts +56 -58
  22. package/telegram-plugin/gateway/handback-orphan-recovery.ts +69 -0
  23. package/telegram-plugin/gateway/handback-preturn-signal.ts +170 -32
  24. package/telegram-plugin/gateway/ipc-protocol.ts +31 -3
  25. package/telegram-plugin/gateway/ipc-server.ts +13 -4
  26. package/telegram-plugin/gateway/liveness-wiring.ts +12 -0
  27. package/telegram-plugin/gateway/model-command.ts +21 -109
  28. package/telegram-plugin/gateway/obligation-store.ts +55 -0
  29. package/telegram-plugin/gateway/outbound-send-path.ts +127 -11
  30. package/telegram-plugin/gateway/outbox-sweep.ts +371 -61
  31. package/telegram-plugin/gateway/rollout-status-edit.ts +175 -0
  32. package/telegram-plugin/gateway/stream-render.ts +62 -0
  33. package/telegram-plugin/gateway/update-announce.ts +11 -4
  34. package/telegram-plugin/hooks/audience-classify.d.mts +50 -0
  35. package/telegram-plugin/hooks/audience-classify.mjs +364 -0
  36. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +86 -3
  37. package/telegram-plugin/hooks/silent-end-scan.mjs +80 -4
  38. package/telegram-plugin/outbox.ts +103 -2
  39. package/telegram-plugin/render/rich-render.ts +10 -2
  40. package/telegram-plugin/reply-quote.ts +150 -0
  41. package/telegram-plugin/retry-api-call.ts +254 -11
  42. package/telegram-plugin/shared/bot-runtime.ts +69 -34
  43. package/telegram-plugin/shared/gw-trace-gate.ts +14 -2
  44. package/telegram-plugin/silent-end.ts +34 -2
  45. package/telegram-plugin/status-no-truncate.ts +31 -16
  46. package/telegram-plugin/stream-controller.ts +44 -9
  47. package/telegram-plugin/stream-reply-handler.ts +5 -3
  48. package/telegram-plugin/tests/boot-card-render.test.ts +2 -1
  49. package/telegram-plugin/tests/card-budget-never-overflows.test.ts +165 -0
  50. package/telegram-plugin/tests/card-hard-truncate-entity-safe.test.ts +153 -0
  51. package/telegram-plugin/tests/card-variants.golden.txt +9 -9
  52. package/telegram-plugin/tests/flood-reply-queue.test.ts +89 -4
  53. package/telegram-plugin/tests/format-consistency.test.ts +160 -14
  54. package/telegram-plugin/tests/framework-fallback-drains-parked.test.ts +134 -0
  55. package/telegram-plugin/tests/handback-orphan-recovery.test.ts +131 -0
  56. package/telegram-plugin/tests/handback-preturn-signal.test.ts +212 -17
  57. package/telegram-plugin/tests/helpers/liveness-wiring-fixture.ts +3 -0
  58. package/telegram-plugin/tests/nested-indent-idiom.test.ts +162 -0
  59. package/telegram-plugin/tests/nested-prefix-indent.test.ts +115 -0
  60. package/telegram-plugin/tests/no-robust-api-call-factory.test.ts +62 -0
  61. package/telegram-plugin/tests/obligation-store.test.ts +51 -0
  62. package/telegram-plugin/tests/outbox-audience-3865.test.ts +579 -0
  63. package/telegram-plugin/tests/outbox-provenance-4141.test.ts +1126 -0
  64. package/telegram-plugin/tests/outbox-sweep-single-flight.test.ts +138 -0
  65. package/telegram-plugin/tests/pinned-card-collapse.test.ts +12 -10
  66. package/telegram-plugin/tests/plain-fallback-4096-cap.test.ts +509 -0
  67. package/telegram-plugin/tests/reply-quote-wire.test.ts +433 -0
  68. package/telegram-plugin/tests/retry-grammy-http-error.test.ts +404 -0
  69. package/telegram-plugin/tests/rollout-narration-edit-socket.test.ts +170 -0
  70. package/telegram-plugin/tests/rollout-status-edit-retry-policy.test.ts +366 -0
  71. package/telegram-plugin/tests/rollout-status-edit.test.ts +140 -0
  72. package/telegram-plugin/tests/rollout-status-wiring.test.ts +40 -7
  73. package/telegram-plugin/tests/tg-post-logger-error-shape.test.ts +4 -0
  74. package/telegram-plugin/tests/tg-post-retry-attribution.test.ts +196 -0
  75. package/telegram-plugin/tests/tool-activity-summary.test.ts +23 -17
  76. package/telegram-plugin/tool-activity-summary.ts +58 -30
  77. package/telegram-plugin/uat/scenarios/jtbd-foreground-subagent-activity-dm.test.ts +3 -2
  78. package/vendor/hindsight-memory/CHANGELOG.md +28 -0
  79. package/vendor/hindsight-memory/docs/measurements/subagent-volume-gate-3994.md +84 -0
  80. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +254 -40
  81. package/vendor/hindsight-memory/scripts/lib/content.py +11 -2
  82. package/vendor/hindsight-memory/scripts/recall.py +18 -3
  83. package/vendor/hindsight-memory/scripts/subagent_retain.py +59 -38
  84. package/vendor/hindsight-memory/scripts/tests/data/replay_volume_gate_3994.py +295 -0
  85. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +109 -0
  86. package/vendor/hindsight-memory/scripts/tests/test_overlap_tokens.py +135 -0
  87. package/vendor/hindsight-memory/scripts/tests/test_recall_no_lexical_gate.py +20 -23
  88. package/vendor/hindsight-memory/scripts/tests/test_recall_query_shaping.py +48 -0
  89. package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +94 -1
  90. package/vendor/hindsight-memory/scripts/tests/test_subagent_retain_learnings.py +98 -42
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Regression: switchroom #3864 — the outbox sweep tick had no overlap guard.
3
+ *
4
+ * `startOutboxSweep` fires a tick every `OUTBOX_SWEEP_INTERVAL_MS` and `void`s
5
+ * the sweep it starts. A sweep slower than the interval (a flood-throttled
6
+ * Telegram, a big backlog, slow disk) therefore had the NEXT tick start a
7
+ * SECOND concurrent sweep body over the same outbox directory — and the one
8
+ * after that a third. The per-record rename mutex in `../outbox.ts` stopped a
9
+ * single record being double-SENT, but nothing stopped the bodies from
10
+ * multiplying: they race the same journal / dedup / backoff state and multiply
11
+ * wire pressure at exactly the moment the sweep should be issuing less.
12
+ *
13
+ * `createOutboxSweepTick` now single-flights: while a sweep is in flight, a
14
+ * tick is skipped outright.
15
+ *
16
+ * Outcome asserted: the number of sweep BODIES actually started. Without the
17
+ * guard the second tick starts a second body and `runs` is 2 — this test fails.
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest'
21
+ import { createOutboxSweepTick, createSweepBackoff } from '../gateway/outbox-sweep.js'
22
+ import type { OutboxSweepSummary } from '../gateway/outbox-sweep.js'
23
+
24
+ const OK: OutboxSweepSummary = { scanned: 0, delivered: 0, skipped: 0, sendFailures: 0 }
25
+
26
+ /** A sweep whose completion the test controls. */
27
+ function controllableSweep() {
28
+ let runs = 0
29
+ const pending: Array<(s: OutboxSweepSummary) => void> = []
30
+ const runSweep = () => {
31
+ runs++
32
+ return new Promise<OutboxSweepSummary>((resolve) => pending.push(resolve))
33
+ }
34
+ return {
35
+ runSweep,
36
+ runs: () => runs,
37
+ /** Complete the oldest in-flight sweep and let its .then/.finally settle. */
38
+ finish: async (summary: OutboxSweepSummary = OK) => {
39
+ pending.shift()?.(summary)
40
+ // Drain the .then → .catch → .finally microtask chain.
41
+ for (let i = 0; i < 8; i++) await Promise.resolve()
42
+ },
43
+ }
44
+ }
45
+
46
+ describe('outbox sweep tick — single flight (#3864)', () => {
47
+ it('a tick that fires while a sweep is still running starts NO second sweep', async () => {
48
+ const sweep = controllableSweep()
49
+ const logs: string[] = []
50
+ const tick = createOutboxSweepTick({
51
+ getBot: () => ({}),
52
+ backoff: createSweepBackoff(),
53
+ runSweep: sweep.runSweep,
54
+ log: (l) => logs.push(l),
55
+ })
56
+
57
+ tick()
58
+ expect(sweep.runs()).toBe(1)
59
+ expect(tick.inFlight()).toBe(true)
60
+
61
+ // Two more interval ticks land while the first sweep is still running.
62
+ tick()
63
+ tick()
64
+ expect(sweep.runs()).toBe(1)
65
+
66
+ // Once it completes, the next tick sweeps again — the guard defers, it
67
+ // does not disable the sweep.
68
+ await sweep.finish()
69
+ expect(tick.inFlight()).toBe(false)
70
+ tick()
71
+ expect(sweep.runs()).toBe(2)
72
+
73
+ // And it says so in the log, once per overlap streak plus a summary.
74
+ expect(logs.some((l) => l.includes('still in flight'))).toBe(true)
75
+ expect(logs.some((l) => l.includes('2 tick(s) were skipped'))).toBe(true)
76
+ })
77
+
78
+ it('releases the guard when the sweep REJECTS (a throwing sweep must not wedge it)', async () => {
79
+ let runs = 0
80
+ let reject: ((e: Error) => void) | null = null
81
+ const tick = createOutboxSweepTick({
82
+ getBot: () => ({}),
83
+ backoff: createSweepBackoff(),
84
+ runSweep: () => {
85
+ runs++
86
+ return new Promise<OutboxSweepSummary>((_res, rej) => {
87
+ reject = rej
88
+ })
89
+ },
90
+ log: () => {},
91
+ })
92
+
93
+ tick()
94
+ expect(runs).toBe(1)
95
+ reject!(new Error('disk on fire'))
96
+ for (let i = 0; i < 8; i++) await Promise.resolve()
97
+
98
+ expect(tick.inFlight()).toBe(false)
99
+ tick()
100
+ expect(runs).toBe(2)
101
+ })
102
+
103
+ it('does not sweep at all before the bot exists', () => {
104
+ const sweep = controllableSweep()
105
+ const tick = createOutboxSweepTick({
106
+ getBot: () => undefined,
107
+ backoff: createSweepBackoff(),
108
+ runSweep: sweep.runSweep,
109
+ })
110
+ tick()
111
+ expect(sweep.runs()).toBe(0)
112
+ })
113
+
114
+ it('honours the failure backoff independently of the in-flight guard', async () => {
115
+ const sweep = controllableSweep()
116
+ let now = 1_000_000
117
+ const tick = createOutboxSweepTick({
118
+ getBot: () => ({}),
119
+ backoff: createSweepBackoff(),
120
+ runSweep: sweep.runSweep,
121
+ now: () => now,
122
+ log: () => {},
123
+ })
124
+
125
+ tick()
126
+ await sweep.finish({ ...OK, sendFailures: 1 })
127
+ expect(sweep.runs()).toBe(1)
128
+ expect(tick.inFlight()).toBe(false)
129
+
130
+ // Guard released, but the backoff window is open → still no sweep.
131
+ tick()
132
+ expect(sweep.runs()).toBe(1)
133
+
134
+ now += 10 * 60 * 1000
135
+ tick()
136
+ expect(sweep.runs()).toBe(2)
137
+ })
138
+ })
@@ -30,6 +30,7 @@ import {
30
30
  import { renderWorkerActivity } from '../worker-activity-feed.js'
31
31
  import { stackCardLines, COLLAPSE_SAFE_SEPARATOR as NB } from '../card-format.js'
32
32
  import {
33
+ NESTED_PREFIX,
33
34
  STATUS_CARD_CHAR_BUDGET,
34
35
  STATUS_LINE_MAX,
35
36
  WORKER_STEP_INDENT,
@@ -49,10 +50,9 @@ import {
49
50
  * indent on the assumption that only ASCII was stripped, and a live phone
50
51
  * check on 2026-07-26 proved otherwise — the card rendered flat. Modelling
51
52
  * this is what makes the nested-card assertion below DISCRIMINATING instead
52
- * of vacuous: `NESTED_PREFIX` is three ASCII spaces (#3668, still open), so
53
- * it cannot hold a collapsed seam apart only the separator can.
54
- * `WORKER_STEP_INDENT` is now U+2800 (category So, not whitespace at all),
55
- * which survives this strip.
53
+ * of vacuous. Both card indents `WORKER_STEP_INDENT` and, since #3668,
54
+ * `NESTED_PREFIX` are U+2800 runs (category So, not whitespace at all),
55
+ * so they survive this strip; anything ASCII or Zs would not.
56
56
  * - the newline itself is dropped with no substitute (this is the defect)
57
57
  */
58
58
  function previewLines(body: string): string[] {
@@ -226,11 +226,13 @@ describe('single-worker / agent status card survives the collapse too (#3666)',
226
226
  expect(collapsed).toContain(`0 tools${NB}starting`)
227
227
  })
228
228
 
229
- it('the nested child block stays separated even though its indent is ASCII (#3668)', () => {
230
- // NESTED_PREFIX is three ASCII SPACES, which Telegram's parser drops (see
231
- // previewLines), so on this surface the collapse separator is the ONLY
232
- // thing holding the parent->child seam apart. Unlike the combined card,
233
- // there is no U+2800 indent to mask a regression here.
229
+ it('the nested child block stays separated AND keeps its indent through the collapse (#3668)', () => {
230
+ // Two distinct guarantees on one line, both load-bearing:
231
+ // 1. the collapse separator holds the parent->child seam apart, and
232
+ // 2. NESTED_PREFIX's U+2800 run SURVIVES `previewLines`' whitespace strip
233
+ // which is exactly what the three ASCII spaces it replaced did not
234
+ // do. Before #3668 the child line collapsed to a bare `↳ …` with no
235
+ // indent, on the pin bar and in the feed alike.
234
236
  const body = renderActivityFeedWithNested(
235
237
  ['Reading gateway.ts'],
236
238
  ['Searching memory', 'Running tests'],
@@ -238,7 +240,7 @@ describe('single-worker / agent status card survives the collapse too (#3666)',
238
240
  expectNoMashedSeams(body)
239
241
  const collapsed = collapsePreview(body)
240
242
  expect(collapsed).not.toContain('gateway.ts↳')
241
- expect(collapsed).toContain(`gateway.ts${NB}↳`)
243
+ expect(collapsed).toContain(`gateway.ts${NB}${NESTED_PREFIX}`)
242
244
  })
243
245
 
244
246
  it('the char-budget backstop keeps the separator on every line it emits', () => {