switchroom 0.18.17 → 0.18.19

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 (67) hide show
  1. package/dist/agent-scheduler/index.js +13 -0
  2. package/dist/auth-broker/index.js +13 -0
  3. package/dist/cli/notion-write-pretool.mjs +13 -0
  4. package/dist/cli/switchroom.js +605 -479
  5. package/dist/host-control/main.js +17 -1
  6. package/dist/vault/approvals/kernel-server.js +13 -0
  7. package/dist/vault/broker/server.js +13 -0
  8. package/package.json +1 -1
  9. package/telegram-plugin/bridge/bridge.ts +7 -1
  10. package/telegram-plugin/dist/bridge/bridge.js +26 -1
  11. package/telegram-plugin/dist/gateway/gateway.js +1544 -619
  12. package/telegram-plugin/dist/server.js +32 -1
  13. package/telegram-plugin/fleet-fallback-resume.ts +26 -3
  14. package/telegram-plugin/format.ts +137 -213
  15. package/telegram-plugin/gateway/approval-hold.ts +49 -0
  16. package/telegram-plugin/gateway/bridge-dead-watchdog.ts +61 -18
  17. package/telegram-plugin/gateway/gateway.ts +399 -85
  18. package/telegram-plugin/gateway/linear-activity.ts +20 -4
  19. package/telegram-plugin/gateway/outbound-send-path.ts +9 -7
  20. package/telegram-plugin/gateway/premium-recovery-wiring.ts +122 -0
  21. package/telegram-plugin/gateway/session-model-file.ts +103 -0
  22. package/telegram-plugin/gateway/tier-downgrade-wiring.ts +121 -0
  23. package/telegram-plugin/gateway/unhandled-rejection-policy.ts +14 -1
  24. package/telegram-plugin/llm-error-present.ts +474 -0
  25. package/telegram-plugin/operator-events.ts +7 -1
  26. package/telegram-plugin/permission-title.ts +172 -10
  27. package/telegram-plugin/premium-recovery.ts +101 -0
  28. package/telegram-plugin/raw-error-scrub.ts +73 -0
  29. package/telegram-plugin/retry-api-call.ts +8 -2
  30. package/telegram-plugin/send-gate-degraded.test.ts +152 -1
  31. package/telegram-plugin/send-gate-observability.test.ts +140 -0
  32. package/telegram-plugin/send-gate-observability.ts +65 -20
  33. package/telegram-plugin/send-gate.test.ts +143 -1
  34. package/telegram-plugin/send-gate.ts +212 -19
  35. package/telegram-plugin/session-tail.ts +16 -0
  36. package/telegram-plugin/shared/local-time.ts +69 -0
  37. package/telegram-plugin/stream-reply-handler.ts +5 -14
  38. package/telegram-plugin/tests/approval-hold-harness.ts +6 -6
  39. package/telegram-plugin/tests/approval-hold-outcome.test.ts +10 -2
  40. package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +61 -0
  41. package/telegram-plugin/tests/fleet-fallback-resume.test.ts +39 -0
  42. package/telegram-plugin/tests/flood-windows-persistence.test.ts +3 -2
  43. package/telegram-plugin/tests/format-consistency.test.ts +68 -53
  44. package/telegram-plugin/tests/formatting-parse-regression.test.ts +5 -6
  45. package/telegram-plugin/tests/formatting-torture-set.ts +1 -1
  46. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +26 -0
  47. package/telegram-plugin/tests/linear-create-issue.test.ts +30 -2
  48. package/telegram-plugin/tests/llm-error-present.test.ts +481 -0
  49. package/telegram-plugin/tests/outbound-send-path.test.ts +4 -3
  50. package/telegram-plugin/tests/paragraph-normalizer.test.ts +42 -100
  51. package/telegram-plugin/tests/permission-title.test.ts +167 -4
  52. package/telegram-plugin/tests/premium-recovery-wiring.test.ts +150 -0
  53. package/telegram-plugin/tests/premium-recovery.test.ts +165 -0
  54. package/telegram-plugin/tests/reaction-gate-routing.test.ts +6 -1
  55. package/telegram-plugin/tests/retry-api-call.test.ts +21 -0
  56. package/telegram-plugin/tests/stream-reply-handler.test.ts +9 -12
  57. package/telegram-plugin/tests/telegram-format.test.ts +86 -31
  58. package/telegram-plugin/tests/tier-downgrade-wiring.test.ts +165 -0
  59. package/telegram-plugin/tests/tier-downgrade.test.ts +141 -0
  60. package/telegram-plugin/tests/turn-flush-safety.test.ts +17 -21
  61. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +27 -1
  62. package/telegram-plugin/tests/worker-activity-feed.test.ts +5 -2
  63. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +492 -0
  64. package/telegram-plugin/tier-downgrade.ts +198 -0
  65. package/telegram-plugin/tool-activity-summary.ts +99 -0
  66. package/telegram-plugin/turn-flush-safety.ts +4 -3
  67. package/telegram-plugin/worker-activity-feed.ts +509 -409
@@ -557,6 +557,105 @@ export function renderActivityFeedWithNested(
557
557
  })
558
558
  }
559
559
 
560
+ // ─── Combined multi-worker feed (coalesced one-message-per-chat) ────────────
561
+ //
562
+ // When 2+ background workers are live in the SAME chat/thread, they render into
563
+ // ONE shared message rather than one message each — so their edits form a
564
+ // single per-message edit stream under the send gate's 1/sec per-chat ceiling
565
+ // instead of N streams that contend and shed (#3084 per-chat bucket). Each
566
+ // worker is a compact two-line row (header + live → step); the body is capped
567
+ // at `maxRows` with a `+M more working…` spill so it stays compact/legible and
568
+ // under the rich-message wire ceiling (STATUS_CARD_CHAR_BUDGET). A single-worker
569
+ // chat still renders through
570
+ // `renderStatusCard` (the 🛠 Worker card) — this primitive owns the N≥2 case.
571
+
572
+ /** Dispatch-time task description cap for a combined-feed worker row. */
573
+ const COMBINED_ROW_DESC_MAX = 72
574
+
575
+ /** One live worker's render-relevant snapshot for the combined feed. */
576
+ export interface CombinedWorkerRow {
577
+ /** Dispatch-time task description (raw, unescaped). */
578
+ description: string
579
+ /** Wall-clock since dispatch, ms — climbs on every heartbeat re-render. */
580
+ elapsedMs: number
581
+ /** Tool calls observed so far. */
582
+ toolCount: number
583
+ /** The worker's latest step line (raw prose or friendly tool label). */
584
+ currentStep: string
585
+ /** Live model id (raw, e.g. `claude-opus-4-8`); omitted when unknown. */
586
+ model?: string
587
+ }
588
+
589
+ export interface CombinedWorkerFeedOpts {
590
+ /** Max worker rows rendered before the `+M more working…` spill line.
591
+ * The overflow is ordered oldest-hidden-first (the newest/most-recently
592
+ * active workers stay visible). */
593
+ maxRows: number
594
+ }
595
+
596
+ /**
597
+ * Render N≥1 live workers into ONE combined feed body (ready Telegram
598
+ * markdown; callers send verbatim — do NOT re-escape). Layout:
599
+ *
600
+ * 🛠 **Workers** · _N running_
601
+ * **{desc1}** _· {elapsed} · {n} tools_
602
+ * → _{step1}_
603
+ * **{desc2}** _· {elapsed} · {n} tools_
604
+ * → _{step2}_
605
+ * _+M more working…_
606
+ *
607
+ * Pure. Rows are rendered in the order supplied (the manager passes them
608
+ * dispatch-order, oldest first). `maxRows` caps the visible rows; the hidden
609
+ * remainder collapses to a single `+M more working…` line. A total-budget
610
+ * backstop drops the OLDEST visible rows one at a time (growing the spill)
611
+ * until the body fits STATUS_CARD_CHAR_BUDGET, so a burst of long descriptions
612
+ * can never overflow the wire limit. Returns null only when `rows` is empty.
613
+ */
614
+ export function renderCombinedWorkerFeed(
615
+ rows: CombinedWorkerRow[],
616
+ opts: CombinedWorkerFeedOpts,
617
+ ): string | null {
618
+ if (rows.length === 0) return null
619
+ const maxRows = Math.max(1, Math.floor(opts.maxRows))
620
+
621
+ const rowLines = (r: CombinedWorkerRow): [string, string] => {
622
+ const desc = escapeMarkdown(
623
+ truncate(stripMarkdown(r.description).replace(/\s+/g, ' ').trim() || 'background task', COMBINED_ROW_DESC_MAX),
624
+ )
625
+ const toolWord = r.toolCount === 1 ? 'tool' : 'tools'
626
+ const modelLabel = formatModelLabel(r.model)
627
+ const modelPart = modelLabel != null ? ` · ${escapeMarkdown(modelLabel)}` : ''
628
+ const header = `**${desc}** _· ${formatFeedElapsed(r.elapsedMs)} · ${r.toolCount} ${toolWord}${modelPart}_`
629
+ const stepClean = stripMarkdown(r.currentStep).replace(/\s+/g, ' ').trim()
630
+ const step =
631
+ stepClean.length > 0
632
+ ? `→ _${escapeMarkdown(truncate(stepClean, STATUS_LINE_MAX))}_`
633
+ : `→ _starting…_`
634
+ return [header, step]
635
+ }
636
+
637
+ const compose = (visibleCount: number): string => {
638
+ const shown = rows.slice(0, visibleCount)
639
+ const hidden = rows.length - shown.length
640
+ const out: string[] = [`🛠 **Workers** · _${rows.length} running_`]
641
+ for (const r of shown) {
642
+ const [h, s] = rowLines(r)
643
+ out.push(h, s)
644
+ }
645
+ if (hidden > 0) out.push(`_+${hidden} more working…_`)
646
+ return stackCardLines(out)
647
+ }
648
+
649
+ // Cap to maxRows first, then shrink further only if the char budget demands.
650
+ let visible = Math.min(rows.length, maxRows)
651
+ let body = compose(visible)
652
+ while (body.length > STATUS_CARD_CHAR_BUDGET && visible > 1) {
653
+ visible -= 1
654
+ body = compose(visible)
655
+ }
656
+ return body
657
+ }
658
+
560
659
  /**
561
660
  * Like appendActivityLine, but for a pre-computed label (from the
562
661
  * real-time PreToolUse sidecar / `tool_label` event) — the hook already
@@ -197,9 +197,10 @@ export function decideTurnFlush(input: FlushDecisionInput): FlushDecision {
197
197
  // lone `\n` collapses adjacent blocks into one run — on the Bot API 10.1
198
198
  // rich-markdown path (#2669) a single newline is a soft break, so the blocks
199
199
  // render as an undifferentiated wall-of-text. `\n\n` is the GFM paragraph
200
- // separator; the gateway send path then wedges visible spacers into those
201
- // gaps via addParagraphSpacers (mirroring the reply path, #2692) so the
202
- // paragraphs render with real separation.
200
+ // separator; the Bot API 10.1 rich renderer shows it as one visible blank
201
+ // line, so the paragraphs render with real separation on their own (the
202
+ // former NBSP spacer pass was removed in the #2669 follow-up — it added a
203
+ // spurious second blank line).
203
204
  //
204
205
  // The silent-marker guards below are unaffected by this change:
205
206
  // isSilentFlushMarker length-guards the whole joined string; the composite /