switchroom 0.17.10 → 0.18.6

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 (149) hide show
  1. package/bin/workspace-dynamic-hook.sh +12 -13
  2. package/dist/agent-scheduler/index.js +29 -2
  3. package/dist/auth-broker/index.js +6163 -152
  4. package/dist/cli/notion-write-pretool.mjs +31 -3
  5. package/dist/cli/switchroom.js +695 -526
  6. package/dist/host-control/main.js +6184 -173
  7. package/dist/vault/approvals/kernel-server.js +5893 -165
  8. package/dist/vault/broker/server.js +6666 -921
  9. package/package.json +1 -1
  10. package/profiles/_base/settings.json.hbs +2 -2
  11. package/profiles/_base/start.sh.hbs +170 -21
  12. package/profiles/coding/CLAUDE.md.hbs +1 -1
  13. package/profiles/default/CLAUDE.md.hbs +2 -2
  14. package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
  15. package/profiles/health-coach/CLAUDE.md.hbs +1 -1
  16. package/skills/switchroom-release/SKILL.md +78 -0
  17. package/telegram-plugin/auth-snapshot-format.ts +37 -25
  18. package/telegram-plugin/context-exhaustion.ts +124 -0
  19. package/telegram-plugin/dist/gateway/gateway.js +25025 -9203
  20. package/telegram-plugin/gateway/activity-card-store.ts +76 -0
  21. package/telegram-plugin/gateway/gateway.ts +740 -106
  22. package/telegram-plugin/gateway/inbound-delivery-gate.ts +26 -0
  23. package/telegram-plugin/gateway/model-command.ts +70 -10
  24. package/telegram-plugin/gateway/resolve-person.ts +304 -0
  25. package/telegram-plugin/gateway/unhandled-rejection-policy.ts +21 -1
  26. package/telegram-plugin/hooks/silent-end-scan.mjs +164 -40
  27. package/telegram-plugin/operator-events.ts +21 -0
  28. package/telegram-plugin/package.json +6 -0
  29. package/telegram-plugin/pending-work-progress.ts +42 -7
  30. package/telegram-plugin/quota-bar-format.ts +360 -0
  31. package/telegram-plugin/quota-watch.ts +4 -6
  32. package/telegram-plugin/registry/turns-schema.test.ts +97 -0
  33. package/telegram-plugin/registry/turns-schema.ts +78 -0
  34. package/telegram-plugin/render/ir.ts +209 -0
  35. package/telegram-plugin/render/parse.ts +363 -0
  36. package/telegram-plugin/render/render.ts +440 -0
  37. package/telegram-plugin/render/rich-render.ts +72 -0
  38. package/telegram-plugin/stream-controller.ts +14 -3
  39. package/telegram-plugin/subagent-watcher.ts +27 -9
  40. package/telegram-plugin/tests/activity-card-store.test.ts +94 -0
  41. package/telegram-plugin/tests/auth-command-format2.test.ts +1 -1
  42. package/telegram-plugin/tests/auth-snapshot-format.test.ts +51 -16
  43. package/telegram-plugin/tests/claude-code-event-contract.test.ts +48 -0
  44. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +11 -0
  45. package/telegram-plugin/tests/feed-survival.test.ts +39 -0
  46. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
  47. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +81 -0
  48. package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +82 -0
  49. package/telegram-plugin/tests/liveness-tracker.test.ts +228 -0
  50. package/telegram-plugin/tests/model-command.test.ts +193 -16
  51. package/telegram-plugin/tests/narrative-render.test.ts +125 -0
  52. package/telegram-plugin/tests/operator-events.test.ts +16 -0
  53. package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +123 -163
  54. package/telegram-plugin/tests/pending-work-progress.test.ts +116 -3
  55. package/telegram-plugin/tests/quota-bar-format.test.ts +444 -0
  56. package/telegram-plugin/tests/quota-watch.test.ts +1 -4
  57. package/telegram-plugin/tests/rapid-fire-delivery-ordering.test.ts +149 -0
  58. package/telegram-plugin/tests/render/parse-torture.test.ts +136 -0
  59. package/telegram-plugin/tests/render/parse.test.ts +393 -0
  60. package/telegram-plugin/tests/render/render.test.ts +436 -0
  61. package/telegram-plugin/tests/render/rich-render.test.ts +85 -0
  62. package/telegram-plugin/tests/resolve-person.test.ts +290 -0
  63. package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +53 -0
  64. package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +138 -0
  65. package/telegram-plugin/tests/subagent-watcher.test.ts +61 -0
  66. package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +155 -1
  67. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +19 -0
  68. package/telegram-plugin/tests/worker-activity-feed.test.ts +97 -0
  69. package/telegram-plugin/tests/worktree-watch-cwds.test.ts +98 -3
  70. package/telegram-plugin/turn-liveness-floor.ts +35 -1
  71. package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +99 -7
  72. package/telegram-plugin/worker-activity-feed.ts +220 -15
  73. package/telegram-plugin/worktree-watch-cwds.ts +92 -17
  74. package/vendor/hindsight-memory/scripts/lib/client.py +11 -1
  75. package/vendor/hindsight-memory/scripts/lib/config.py +9 -2
  76. package/vendor/hindsight-memory/scripts/recall.py +64 -6
  77. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +1 -0
  78. package/vendor/hindsight-memory/tests/test_client.py +43 -0
  79. package/vendor/hindsight-memory/tests/test_recall_precision.py +114 -0
  80. package/profiles/default/CLAUDE.md +0 -116
  81. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  82. package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
  83. package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
  84. package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
  85. package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
  86. package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
  87. package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  88. package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
  89. package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
  90. package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
  91. package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
  92. package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
  93. package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
  94. package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
  95. package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
  96. package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
  97. package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
  98. package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
  99. package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  100. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
  101. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
  102. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
  103. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
  104. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
  105. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
  106. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
  107. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
  108. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
  109. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
  110. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
  111. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
  112. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
  113. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
  114. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
  115. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
  116. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
  117. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
  118. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
  119. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
  120. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
  121. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
  122. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
  123. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
  124. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
  125. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  126. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  127. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
  128. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
  129. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
  130. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
  131. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
  132. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
  133. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
  134. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
  135. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
  136. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  137. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
  138. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
  139. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
  140. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
  141. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
  142. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  143. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
  144. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
  145. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
  146. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  147. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
  148. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
  149. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
@@ -48,3 +48,127 @@ export function shouldArmOrphanedReplyTimeout(params: {
48
48
  !params.progressCardActive
49
49
  )
50
50
  }
51
+
52
+ /**
53
+ * Default "recently streaming" window (ms) for the orphaned-reply liveness
54
+ * stamp. The gateway overrides this from
55
+ * SWITCHROOM_ORPHANED_REPLY_STREAM_WINDOW_MS; this constant is the fallback and
56
+ * the value the LivenessTracker tests pin against.
57
+ */
58
+ export const ORPHANED_REPLY_STREAM_WINDOW_MS = 120_000
59
+
60
+ /**
61
+ * Result of a fuse-expiry decision.
62
+ *
63
+ * - `rearm` — re-arm the fuse (keep the turn alive) instead of
64
+ * firing the synthetic turn_end backstop.
65
+ * - `countsAgainstCap`— this rearm (or the fire it turned into at the cap) was
66
+ * a working/recently-streaming rearm, i.e. subject to the
67
+ * ORPHANED_REPLY_MAX_REARMS cap. Human-wait rearms are
68
+ * uncapped and report `false` here.
69
+ */
70
+ export interface OrphanedReplyExpiryDecision {
71
+ rearm: boolean
72
+ countsAgainstCap: boolean
73
+ }
74
+
75
+ /**
76
+ * Per-turn liveness tracker for the orphaned-reply backstop.
77
+ *
78
+ * WHY THIS EXISTS (the thinking-pause fix):
79
+ * The orphaned-reply fuse (ORPHANED_REPLY_TIMEOUT_MS = 30 s) used to be reset
80
+ * ONLY by `tool_label` and `text` stream events. During a long model
81
+ * reasoning pause the gateway sees NO such events (thinking events carry no
82
+ * text), so the fuse ran down and force-ended a genuinely-live turn mid-work
83
+ * ("orphaned-reply timeout (30000ms) — forcing backstop" ~1 ms before
84
+ * turn_end, with minutes of real work still to come).
85
+ *
86
+ * THE FIX:
87
+ * Stamp `lastStreamEventAt` on ANY genuine stream event (via onStreamEvent,
88
+ * called from the gateway dispatcher entry). If a genuine event arrived
89
+ * within `windowMs` (default 120 s) the turn is "recently streaming" and the
90
+ * fuse re-arms instead of firing — a reasoning pause is survivable while a
91
+ * genuine multi-minute hang (no events at all) still fires.
92
+ *
93
+ * This is a small, pure, side-effect-free seam so the decision logic is unit
94
+ * testable without the full gateway. The gateway holds one instance per
95
+ * CurrentTurn (so per-turn identity / supersession semantics are unchanged) and
96
+ * delegates: the dispatcher calls onStreamEvent; the fire callback calls
97
+ * decideOnExpiry; the defensive turn_end guard reads recentlyStreaming.
98
+ */
99
+ export class LivenessTracker {
100
+ /** Wall-clock (ms) of the last genuine stream event for this turn. */
101
+ lastStreamEventAt: number
102
+ /**
103
+ * How many times the fuse re-armed on a working / recently-streaming
104
+ * expiry. Bounded by ORPHANED_REPLY_MAX_REARMS. Zeroed by onStreamEvent
105
+ * whenever a genuine stream event lands, so the cap only bites CONSECUTIVE
106
+ * silent expiries (a genuinely wedged single tool with no stream at all).
107
+ */
108
+ orphanedReplyRearmCount = 0
109
+
110
+ constructor(startedAt: number) {
111
+ this.lastStreamEventAt = startedAt
112
+ }
113
+
114
+ /**
115
+ * Stamp liveness for a genuine stream event and reset the rearm counter.
116
+ *
117
+ * F4 predicate: everything EXCEPT the synthetic `turn_end` re-entry
118
+ * (durationMs === -1, the fire callback's own re-dispatch) counts as a
119
+ * genuine stream event. Stamping a REAL turn_end (durationMs >= 0) is
120
+ * harmless — the turn is ending anyway.
121
+ *
122
+ * The counter reset lives HERE (driven by the dispatcher), NOT in the fuse
123
+ * re-arm path — otherwise every rearm would zero its own counter and nullify
124
+ * the 20-cap.
125
+ */
126
+ onStreamEvent(kind: string, durationMs: number | undefined, now: number): void {
127
+ if (kind === 'turn_end' && durationMs === -1) return
128
+ this.lastStreamEventAt = now
129
+ this.orphanedReplyRearmCount = 0
130
+ }
131
+
132
+ /** True iff a genuine stream event landed within `windowMs` of `now`. */
133
+ recentlyStreaming(now: number, windowMs: number): boolean {
134
+ return now - this.lastStreamEventAt < windowMs
135
+ }
136
+
137
+ /**
138
+ * Decide what to do when the orphaned-reply fuse expires.
139
+ *
140
+ * Rearm when the turn is working OR recently streaming OR a human is being
141
+ * waited on. Working / recently-streaming rearms count against `maxRearms`
142
+ * (so a genuinely wedged single tool that never streams still surfaces after
143
+ * the cap). Human-wait rearms are uncapped (the human simply hasn't tapped
144
+ * yet). Once the cap is hit with nothing else keeping the turn alive, fire
145
+ * (rearm:false) — matching the existing fail-safe.
146
+ *
147
+ * Mutates `orphanedReplyRearmCount` on each rearm.
148
+ */
149
+ decideOnExpiry(opts: {
150
+ working: boolean
151
+ humanWaiting: boolean
152
+ now: number
153
+ windowMs: number
154
+ maxRearms: number
155
+ }): OrphanedReplyExpiryDecision {
156
+ // Human-wait rearms are uncapped (unchanged from prior behaviour).
157
+ if (opts.humanWaiting) {
158
+ this.orphanedReplyRearmCount++
159
+ return { rearm: true, countsAgainstCap: false }
160
+ }
161
+ const recently = this.recentlyStreaming(opts.now, opts.windowMs)
162
+ if (opts.working || recently) {
163
+ if (this.orphanedReplyRearmCount < opts.maxRearms) {
164
+ this.orphanedReplyRearmCount++
165
+ return { rearm: true, countsAgainstCap: true }
166
+ }
167
+ // Cap reached with only working/recently-streaming keeping it alive:
168
+ // fire to surface the wedge.
169
+ return { rearm: false, countsAgainstCap: true }
170
+ }
171
+ // Nothing keeping the turn alive → fire (fail-safe).
172
+ return { rearm: false, countsAgainstCap: false }
173
+ }
174
+ }