switchroom 0.15.44 → 0.16.4

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 (150) hide show
  1. package/dist/agent-scheduler/index.js +122 -88
  2. package/dist/auth-broker/index.js +463 -177
  3. package/dist/cli/autoaccept-poll.js +4842 -35
  4. package/dist/cli/drive-write-pretool.mjs +17 -14
  5. package/dist/cli/notion-write-pretool.mjs +117 -86
  6. package/dist/cli/self-improve-apply-guard-pretool.mjs +626 -0
  7. package/dist/cli/self-improve-stop.mjs +428 -0
  8. package/dist/cli/skill-validate-pretool.mjs +72 -72
  9. package/dist/cli/switchroom.js +3249 -1241
  10. package/dist/cli/ui/index.html +1 -1
  11. package/dist/host-control/main.js +2833 -355
  12. package/dist/vault/approvals/kernel-server.js +7482 -7439
  13. package/dist/vault/broker/server.js +11315 -11272
  14. package/examples/minimal.yaml +1 -0
  15. package/examples/switchroom.yaml +1 -0
  16. package/package.json +3 -3
  17. package/profiles/_base/start.sh.hbs +88 -1
  18. package/profiles/_shared/execution-discipline.md.hbs +18 -0
  19. package/profiles/default/CLAUDE.md.hbs +3 -22
  20. package/telegram-plugin/.claude-plugin/plugin.json +2 -2
  21. package/telegram-plugin/answer-stream-flag.ts +12 -49
  22. package/telegram-plugin/answer-stream.ts +5 -150
  23. package/telegram-plugin/auth-snapshot-format.ts +280 -48
  24. package/telegram-plugin/auto-fallback-fleet.ts +44 -1
  25. package/telegram-plugin/context-exhaustion.ts +12 -0
  26. package/telegram-plugin/demo-mask.ts +154 -0
  27. package/telegram-plugin/dist/bridge/bridge.js +167 -124
  28. package/telegram-plugin/dist/gateway/gateway.js +3039 -1159
  29. package/telegram-plugin/dist/server.js +215 -172
  30. package/telegram-plugin/docs/waiting-ux-spec.md +2 -2
  31. package/telegram-plugin/draft-stream.ts +47 -410
  32. package/telegram-plugin/final-answer-detect.ts +17 -12
  33. package/telegram-plugin/fleet-fallback-resume.ts +131 -0
  34. package/telegram-plugin/format.ts +56 -19
  35. package/telegram-plugin/gateway/auth-add-flow.ts +332 -127
  36. package/telegram-plugin/gateway/auth-broker-client.ts +2 -2
  37. package/telegram-plugin/gateway/auth-command.ts +70 -14
  38. package/telegram-plugin/gateway/clean-shutdown-marker.ts +44 -0
  39. package/telegram-plugin/gateway/config-approval-handler.test.ts +91 -4
  40. package/telegram-plugin/gateway/config-approval-handler.ts +94 -13
  41. package/telegram-plugin/gateway/current-turn-map.ts +188 -0
  42. package/telegram-plugin/gateway/disconnect-flush.ts +3 -1
  43. package/telegram-plugin/gateway/effort-command.ts +8 -3
  44. package/telegram-plugin/gateway/emission-authority.ts +369 -0
  45. package/telegram-plugin/gateway/feed-open-gate.ts +292 -0
  46. package/telegram-plugin/gateway/gateway.ts +1837 -291
  47. package/telegram-plugin/gateway/inject-handler.test.ts +2 -1
  48. package/telegram-plugin/gateway/ms365-write-approval.test.ts +4 -4
  49. package/telegram-plugin/gateway/represent-guard.ts +72 -0
  50. package/telegram-plugin/gateway/status-surface-log.test.ts +5 -4
  51. package/telegram-plugin/gateway/status-surface-log.ts +14 -3
  52. package/telegram-plugin/history.ts +33 -11
  53. package/telegram-plugin/hooks/repo-context-pretool.mjs +26 -0
  54. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +5 -0
  55. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -0
  56. package/telegram-plugin/hooks/tool-label-pretool.mjs +39 -15
  57. package/telegram-plugin/issues-card.ts +4 -0
  58. package/telegram-plugin/model-unavailable.ts +124 -0
  59. package/telegram-plugin/narrative-dedup.ts +69 -0
  60. package/telegram-plugin/over-ping-safety-net.ts +70 -4
  61. package/telegram-plugin/package.json +3 -3
  62. package/telegram-plugin/pending-work-progress.ts +12 -0
  63. package/telegram-plugin/permission-rule.ts +32 -5
  64. package/telegram-plugin/permission-title.ts +152 -9
  65. package/telegram-plugin/quota-check.ts +13 -0
  66. package/telegram-plugin/quota-watch.ts +135 -7
  67. package/telegram-plugin/registry/turns-schema.test.ts +24 -0
  68. package/telegram-plugin/registry/turns-schema.ts +9 -0
  69. package/telegram-plugin/runtime-metrics.ts +13 -0
  70. package/telegram-plugin/session-tail.ts +96 -11
  71. package/telegram-plugin/silence-poke.ts +170 -24
  72. package/telegram-plugin/slot-banner-driver.ts +3 -0
  73. package/telegram-plugin/status-no-truncate.ts +44 -0
  74. package/telegram-plugin/status-reactions.ts +20 -3
  75. package/telegram-plugin/stream-controller.ts +4 -23
  76. package/telegram-plugin/stream-reply-handler.ts +6 -24
  77. package/telegram-plugin/streaming-metrics.ts +91 -0
  78. package/telegram-plugin/subagent-watcher.ts +212 -66
  79. package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +47 -0
  80. package/telegram-plugin/tests/answer-stream-dedup.test.ts +9 -26
  81. package/telegram-plugin/tests/answer-stream-flag.test.ts +25 -58
  82. package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +41 -51
  83. package/telegram-plugin/tests/answer-stream.test.ts +2 -411
  84. package/telegram-plugin/tests/auth-add-flow.test.ts +488 -253
  85. package/telegram-plugin/tests/auth-command-format2.test.ts +71 -1
  86. package/telegram-plugin/tests/auth-snapshot-format.test.ts +376 -6
  87. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +120 -0
  88. package/telegram-plugin/tests/cross-turn-card-gate.test.ts +424 -0
  89. package/telegram-plugin/tests/demo-mask.test.ts +127 -0
  90. package/telegram-plugin/tests/draft-stream.test.ts +0 -827
  91. package/telegram-plugin/tests/emission-authority-card-drain-gate.test.ts +236 -0
  92. package/telegram-plugin/tests/emission-authority-facade.test.ts +488 -0
  93. package/telegram-plugin/tests/emission-authority-open-gate.test.ts +179 -0
  94. package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +395 -0
  95. package/telegram-plugin/tests/emission-determinism-wiring.test.ts +177 -0
  96. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +146 -0
  97. package/telegram-plugin/tests/feed-open-gate.test.ts +259 -0
  98. package/telegram-plugin/tests/feed-survival.test.ts +526 -0
  99. package/telegram-plugin/tests/fleet-fallback-resume.test.ts +197 -0
  100. package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +117 -0
  101. package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +4 -11
  102. package/telegram-plugin/tests/history.test.ts +60 -0
  103. package/telegram-plugin/tests/model-unavailable.test.ts +118 -0
  104. package/telegram-plugin/tests/narrative-dedup.test.ts +118 -0
  105. package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +285 -0
  106. package/telegram-plugin/tests/over-ping-final-answer-decoupling.test.ts +194 -0
  107. package/telegram-plugin/tests/over-ping-safety-net.test.ts +2 -2
  108. package/telegram-plugin/tests/per-topic-current-turn.test.ts +373 -0
  109. package/telegram-plugin/tests/permission-card-origin-kill-switch.test.ts +42 -0
  110. package/telegram-plugin/tests/permission-rule.test.ts +17 -0
  111. package/telegram-plugin/tests/permission-title.test.ts +206 -17
  112. package/telegram-plugin/tests/quota-watch.test.ts +252 -9
  113. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -1
  114. package/telegram-plugin/tests/repo-context-pretool.test.ts +62 -0
  115. package/telegram-plugin/tests/represent-guard.test.ts +162 -0
  116. package/telegram-plugin/tests/session-tail.test.ts +147 -3
  117. package/telegram-plugin/tests/silence-liveness-wiring.test.ts +18 -0
  118. package/telegram-plugin/tests/status-card-budget-parity.test.ts +72 -0
  119. package/telegram-plugin/tests/status-surface-log.test.ts +146 -0
  120. package/telegram-plugin/tests/subagent-watcher-clip-narrative.test.ts +58 -0
  121. package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +102 -0
  122. package/telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts +225 -0
  123. package/telegram-plugin/tests/subagent-watcher.test.ts +147 -0
  124. package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +597 -0
  125. package/telegram-plugin/tests/telegram-format.test.ts +101 -6
  126. package/telegram-plugin/tests/tool-activity-summary.test.ts +550 -15
  127. package/telegram-plugin/tests/tool-label-pretool.test.ts +73 -0
  128. package/telegram-plugin/tests/tool-label-sidecar.test.ts +44 -0
  129. package/telegram-plugin/tests/tool-labels.test.ts +67 -0
  130. package/telegram-plugin/tests/turn-liveness-floor.test.ts +196 -0
  131. package/telegram-plugin/tests/turn-liveness-invariant.test.ts +340 -0
  132. package/telegram-plugin/tests/welcome-text.test.ts +32 -3
  133. package/telegram-plugin/tests/worker-activity-feed.test.ts +470 -22
  134. package/telegram-plugin/tool-activity-summary.ts +375 -58
  135. package/telegram-plugin/turn-liveness-floor.ts +240 -0
  136. package/telegram-plugin/uat/assertions.ts +115 -0
  137. package/telegram-plugin/uat/driver.ts +68 -0
  138. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +119 -133
  139. package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +94 -0
  140. package/telegram-plugin/uat/scenarios/jtbd-cross-turn-card-dm.test.ts +109 -0
  141. package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-thinkgap-dm.test.ts +478 -0
  142. package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +396 -0
  143. package/telegram-plugin/uat/scenarios/jtbd-liveness-feed-open-dm.test.ts +202 -0
  144. package/telegram-plugin/uat/scenarios/jtbd-reply-is-last-dm.test.ts +202 -0
  145. package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +93 -87
  146. package/telegram-plugin/welcome-text.ts +13 -1
  147. package/telegram-plugin/worker-activity-feed.ts +157 -82
  148. package/telegram-plugin/draft-transport.ts +0 -122
  149. package/telegram-plugin/tests/draft-retirement-wiring.test.ts +0 -82
  150. package/telegram-plugin/tests/draft-transport.test.ts +0 -211
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Resume-the-turn-after-swap gate (THE load-bearing half of the
3
+ * auth-failover-stall fix).
4
+ *
5
+ * Background — the stall this kills:
6
+ * When a Claude account hits a rate limit MID-TURN (HTTP 429, including a
7
+ * *session* cap like "You've hit your session limit · resets 5pm"), the
8
+ * failover machinery correctly detects it (session-tail → model-unavailable
9
+ * → gateway.fireFleetAutoFallback → doFireFleetAutoFallback) and swaps the
10
+ * fleet to a spare account. But the swap only takes effect on the NEXT
11
+ * claude invocation — the turn that died on the 429 is never resumed. The
12
+ * agent goes silent until the user manually sends a fresh message
13
+ * ("Resume"). Symptom: a mid-conversation stall that needs a manual re-poke.
14
+ *
15
+ * The fix: after a SUCCESSFUL swap (outcome.kind === 'switched'), the gateway
16
+ * re-runs the dead turn on the freshly-active account.
17
+ *
18
+ * Mechanism choice — restart, NOT redeliver:
19
+ * The inbound that died on the 429 was already DELIVERED (the turn started,
20
+ * then the model rejected it). It is therefore NOT sitting in the
21
+ * pending-inbound buffer (`redeliverBufferedInbound` only drains inbounds
22
+ * that never reached a live bridge). The canonical way to re-run an
23
+ * already-delivered, interrupted turn is `triggerSelfRestart`: on the next
24
+ * boot the gateway's boot-resume path re-injects the LATEST interrupted turn
25
+ * from the turn registry — exactly the turn the 429 killed — and the fresh
26
+ * claude process picks up the swapped-to account. `redeliverBufferedInbound`
27
+ * would have nothing to redeliver here, so it is the wrong seam.
28
+ *
29
+ * This module owns ONLY the decision + the guards, kept pure so it is testable
30
+ * without restarting a process (the gateway module-state pattern mirrors
31
+ * `fleet-fallback-gate.ts`). The gateway wires the actual `triggerSelfRestart`
32
+ * call to `decide()` returning `'resume'`.
33
+ *
34
+ * Guards (all REQUIRED by the fix spec):
35
+ * 1. all-blocked / no-swap no-op — `decide()` is only ever consulted by the
36
+ * caller on `outcome.kind === 'switched'`; on any other outcome the caller
37
+ * does not call us, preserving the existing all-blocked cooldown path.
38
+ * 2. Single-flight — once a resume is armed, a second swap (a 429 storm, a
39
+ * repeated quota event) within the same window does NOT re-arm. Only ONE
40
+ * restart fires per swap. Without this a swap-storm could loop-restart the
41
+ * agent.
42
+ * NOTE: this latch is IN-MEMORY and does NOT survive the self-restart it
43
+ * arms — so it only dedups multiple swaps within a SINGLE process
44
+ * lifetime; it cannot by itself bound a cross-restart loop. Cross-restart
45
+ * loops are bounded separately, broker-side, by account exhaustion:
46
+ * `markExhausted` persists across restarts, so once every spare is benched
47
+ * the outcome becomes all-blocked, which does NOT restart.
48
+ * 3. Staleness failsafe — an ancient interrupted turn is NOT resurrected. If
49
+ * the failed turn started longer ago than `maxAgeMs` (default 3h, matching
50
+ * the boot-resume RESUME_MAX_AGE_MS failsafe), `decide()` returns
51
+ * `'skip-stale'` so a day-old buffered turn isn't replayed unprompted.
52
+ * `null` failedTurnStartedAtMs (no turn timestamp known) is treated as
53
+ * "resume" — the boot-resume path applies its own 3h guard as a second
54
+ * line of defence.
55
+ */
56
+
57
+ export interface FleetFallbackResumeOptions {
58
+ /** Max age of the failed turn before a resume is suppressed as stale.
59
+ * Default 3h — mirrors the gateway boot-resume RESUME_MAX_AGE_MS. */
60
+ maxAgeMs?: number;
61
+ /** Time source (overridable in tests). */
62
+ nowFn?: () => number;
63
+ /** Cooldown after an armed resume during which a second swap will NOT
64
+ * re-arm. Dedups a 429 storm WITHIN one process lifetime (this latch is
65
+ * in-memory and does not survive the restart it arms — cross-restart loops
66
+ * are bounded broker-side by persisted account exhaustion, not here).
67
+ * Default 60s — comfortably longer than a restart + boot takes, so the
68
+ * in-flight restart settles before another resume can arm. */
69
+ singleFlightMs?: number;
70
+ }
71
+
72
+ export type ResumeDecision =
73
+ /** Fire the resume (restart so the boot path replays the dead turn). */
74
+ | 'resume'
75
+ /** Suppressed: a resume is already in flight (single-flight guard). */
76
+ | 'skip-inflight'
77
+ /** Suppressed: the failed turn is older than maxAgeMs (staleness guard). */
78
+ | 'skip-stale';
79
+
80
+ export interface FleetFallbackResumeGate {
81
+ /**
82
+ * Decide whether to resume the dead turn after a successful swap. Call ONLY
83
+ * when the swap outcome was `'switched'`. Records the arm time on a 'resume'
84
+ * verdict so a follow-on swap within `singleFlightMs` is suppressed.
85
+ *
86
+ * @param failedTurnStartedAtMs epoch-ms the failed turn began, or null when
87
+ * unknown (then the staleness guard is deferred to the boot path).
88
+ */
89
+ decide(failedTurnStartedAtMs: number | null): ResumeDecision;
90
+ /** Test seam — reset to fresh state. Production code should not call this. */
91
+ reset(): void;
92
+ /** Test/debug — current internal state. */
93
+ inspect(): { lastResumedAtMs: number };
94
+ }
95
+
96
+ export const DEFAULT_RESUME_MAX_AGE_MS = 10_800_000; // 3h
97
+ export const DEFAULT_RESUME_SINGLE_FLIGHT_MS = 60_000; // 60s
98
+
99
+ export function createFleetFallbackResumeGate(
100
+ opts: FleetFallbackResumeOptions = {},
101
+ ): FleetFallbackResumeGate {
102
+ const nowFn = opts.nowFn ?? (() => Date.now());
103
+ const maxAgeMs = opts.maxAgeMs ?? DEFAULT_RESUME_MAX_AGE_MS;
104
+ const singleFlightMs = opts.singleFlightMs ?? DEFAULT_RESUME_SINGLE_FLIGHT_MS;
105
+ // -Infinity = never resumed. A concrete value arms the single-flight window.
106
+ let lastResumedAtMs = Number.NEGATIVE_INFINITY;
107
+
108
+ function decide(failedTurnStartedAtMs: number | null): ResumeDecision {
109
+ const now = nowFn();
110
+ // Guard 2 — single-flight. A second swap within the window does not
111
+ // re-arm; the in-flight restart owns the resume.
112
+ if (now - lastResumedAtMs < singleFlightMs) return 'skip-inflight';
113
+ // Guard 3 — staleness. A known-old failed turn is not resurrected. An
114
+ // unknown (null) timestamp defers to the boot-resume 3h failsafe.
115
+ if (failedTurnStartedAtMs != null && now - failedTurnStartedAtMs > maxAgeMs) {
116
+ return 'skip-stale';
117
+ }
118
+ lastResumedAtMs = now;
119
+ return 'resume';
120
+ }
121
+
122
+ return {
123
+ decide,
124
+ reset() {
125
+ lastResumedAtMs = Number.NEGATIVE_INFINITY;
126
+ },
127
+ inspect() {
128
+ return { lastResumedAtMs };
129
+ },
130
+ };
131
+ }
@@ -563,34 +563,71 @@ function escapeUnescapedEntities(inner: string): string {
563
563
  * The message then ships to Telegram intact and the user sees literal
564
564
  * `\n\n` in the chat instead of paragraph breaks.
565
565
  *
566
- * Heuristic: if the text contains ZERO real newlines AND has at least one
567
- * literal `\n`, `\r`, or `\t` escape sequence, the caller almost certainly
568
- * intended those as real whitespace and the client serializer ate them.
569
- * Unescape them (also `\\` and `\"`). If the text has any real newline,
570
- * trust the caller exactly as given and do nothing — legitimate content
571
- * may contain a literal `\n` inside a shell snippet or regex.
566
+ * Unescape literal `\n`, `\r`, `\t`, and `\"` sequences everywhere EXCEPT
567
+ * inside code spans (inline backtick spans and fenced ``` blocks). Those
568
+ * regions are masked with placeholders before the unescape pass so that a
569
+ * literal `\n` a user typed inside a shell snippet or regex is preserved
570
+ * verbatim. The genuine escaped-backslash sequence `\\n` (which the user
571
+ * intended as a literal backslash + n, not a newline) is handled by
572
+ * protecting `\\` before touching `\n`.
572
573
  *
573
- * This is intentionally narrow: it only fires on the clear bug signature
574
- * (multi-line-looking content collapsed to one physical line). False
575
- * positives on a single-line message that legitimately contains `\n` are
576
- * possible but rare users writing single-line shell snippets typically
577
- * wrap them in backticks, and this runs before markdown→HTML so the
578
- * unescape has no effect on text inside fenced code blocks if it already
579
- * has real newlines around them.
574
+ * This deliberately fires even when the message contains real newlines
575
+ * the old whole-message heuristic ("bail if any real newline exists") was
576
+ * too broad and prevented repair of mixed messages that had both real
577
+ * newlines and stray literal `\n` escape sequences outside code spans.
580
578
  */
581
579
  export function repairEscapedWhitespace(text: string): string {
582
- if (text.includes('\n') || text.includes('\r')) return text
583
580
  if (!/\\[nrt"\\]/.test(text)) return text
584
- // Order matters: protect existing `\\` first so `\\n` stays as `\n`
585
- // literal and doesn't become a newline.
586
- const BACKSLASH_PH = '\x00BKSL\x00'
587
- return text
581
+
582
+ // Per-call random nonce prevents sentinel collision: if user text happens to
583
+ // contain our placeholder bytes, the restore step would look up an out-of-range
584
+ // index and produce "undefined" in the Telegram output. A nonce that is unique
585
+ // per invocation makes the sentinel statistically impossible to collide with.
586
+ const nonce = Math.random().toString(36).slice(2)
587
+ const CODE_MASK_PH = `\x00RM${nonce}_`
588
+ const BACKSLASH_PH = `\x00BK${nonce}_`
589
+
590
+ // Mask fenced code blocks (``` ... ```) and inline code spans (` ... `)
591
+ // so the unescape pass never touches their content.
592
+ //
593
+ // Fenced blocks are extracted first. Only CLOSED fenced blocks (with a
594
+ // matching closing ```) are masked — an unclosed fence is left as-is so the
595
+ // inline-code pass below won't misparse the two leading backticks as an empty
596
+ // inline span and expose the block's interior.
597
+ //
598
+ // Inline code uses `[^\`\n]+` (one or more non-backtick, non-newline chars)
599
+ // matching the same definition that markdownToHtml uses, so the masked regions
600
+ // are consistent with what the downstream pipeline treats as code.
601
+ const codeMasks: string[] = []
602
+
603
+ const masked = text
604
+ // Closed fenced code blocks only (``` ... ``` with a matching closer).
605
+ .replace(/```[\s\S]*?```/g, (m) => {
606
+ const idx = codeMasks.length
607
+ codeMasks.push(m)
608
+ return `${CODE_MASK_PH}${idx}\x00`
609
+ })
610
+ // Inline code spans: at least one character between backticks, no embedded
611
+ // backtick or newline (matches markdownToHtml's /`([^`\n]+)`/ definition).
612
+ .replace(/`[^`\n]+`/g, (m) => {
613
+ const idx = codeMasks.length
614
+ codeMasks.push(m)
615
+ return `${CODE_MASK_PH}${idx}\x00`
616
+ })
617
+
618
+ // Order matters: protect existing `\\` first so `\\n` stays as a literal
619
+ // backslash + n and doesn't become a newline.
620
+ const unescaped = masked
588
621
  .replace(/\\\\/g, BACKSLASH_PH)
589
622
  .replace(/\\n/g, '\n')
590
623
  .replace(/\\r/g, '\r')
591
624
  .replace(/\\t/g, '\t')
592
625
  .replace(/\\"/g, '"')
593
- .replace(new RegExp(BACKSLASH_PH, 'g'), '\\')
626
+ .replace(new RegExp(BACKSLASH_PH.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '\\')
627
+
628
+ // Restore masked code spans verbatim.
629
+ const restoreRe = new RegExp(`${CODE_MASK_PH.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\d+)\x00`, 'g')
630
+ return unescaped.replace(restoreRe, (_m, idx) => codeMasks[Number(idx)] ?? _m)
594
631
  }
595
632
 
596
633
  // ---------------------------------------------------------------------------