switchroom 0.7.15 → 0.10.0

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 (301) hide show
  1. package/README.md +51 -59
  2. package/bin/run-hook.sh +27 -11
  3. package/bin/timezone-hook.sh +9 -7
  4. package/dist/agent-scheduler/index.js +410 -133
  5. package/dist/auth-broker/index.js +13932 -0
  6. package/dist/cli/switchroom.js +26937 -5601
  7. package/dist/host-control/main.js +12702 -0
  8. package/dist/vault/approvals/kernel-server.js +467 -184
  9. package/dist/vault/broker/server.js +1430 -724
  10. package/examples/minimal.yaml +63 -0
  11. package/examples/personal-google-workspace-mcp/.env.example +34 -0
  12. package/examples/personal-google-workspace-mcp/README.md +194 -0
  13. package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
  14. package/examples/switchroom.yaml +220 -0
  15. package/package.json +7 -4
  16. package/profiles/_base/settings.json.hbs +20 -5
  17. package/profiles/_base/start.sh.hbs +16 -3
  18. package/profiles/_shared/agent-self-service.md.hbs +126 -0
  19. package/profiles/_shared/telegram-style.md.hbs +20 -90
  20. package/profiles/_shared/vault-protocol.md.hbs +68 -0
  21. package/profiles/default/CLAUDE.md +50 -96
  22. package/profiles/default/CLAUDE.md.hbs +36 -6
  23. package/profiles/default/workspace/SOUL.md.hbs +12 -5
  24. package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
  25. package/skills/buildkite-agent-runtime/SKILL.md +44 -11
  26. package/skills/buildkite-api/SKILL.md +31 -8
  27. package/skills/buildkite-cli/SKILL.md +27 -9
  28. package/skills/buildkite-migration/SKILL.md +22 -9
  29. package/skills/buildkite-pipelines/SKILL.md +26 -9
  30. package/skills/buildkite-secure-delivery/SKILL.md +23 -9
  31. package/skills/buildkite-test-engine/SKILL.md +25 -8
  32. package/skills/docx/SKILL.md +1 -1
  33. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
  34. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
  35. package/skills/file-bug/SKILL.md +34 -6
  36. package/skills/humanizer/SKILL.md +15 -0
  37. package/skills/humanizer-calibrate/SKILL.md +7 -1
  38. package/skills/mcp-builder/SKILL.md +1 -1
  39. package/skills/pdf/SKILL.md +1 -1
  40. package/skills/pptx/SKILL.md +1 -1
  41. package/skills/skill-creator/SKILL.md +21 -1
  42. package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  43. package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
  44. package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
  45. package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
  46. package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
  47. package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  48. package/skills/switchroom-cli/SKILL.md +63 -64
  49. package/skills/switchroom-health/SKILL.md +23 -10
  50. package/skills/switchroom-install/SKILL.md +3 -3
  51. package/skills/switchroom-manage/SKILL.md +26 -19
  52. package/skills/switchroom-runtime/SKILL.md +191 -0
  53. package/skills/switchroom-status/SKILL.md +27 -2
  54. package/skills/telegram-test-harness/SKILL.md +3 -0
  55. package/skills/token-helpers/SKILL.md +24 -1
  56. package/skills/webapp-testing/SKILL.md +31 -1
  57. package/skills/xlsx/SKILL.md +1 -1
  58. package/telegram-plugin/admin-commands/index.ts +7 -5
  59. package/telegram-plugin/analytics-posthog.ts +191 -0
  60. package/telegram-plugin/bridge/bridge.ts +69 -0
  61. package/telegram-plugin/bridge/ipc-client.ts +4 -1
  62. package/telegram-plugin/dist/bridge/bridge.js +194 -119
  63. package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
  64. package/telegram-plugin/dist/server.js +245 -189
  65. package/telegram-plugin/first-paint.ts +3 -24
  66. package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
  67. package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
  68. package/telegram-plugin/gateway/auth-command.ts +794 -0
  69. package/telegram-plugin/gateway/auth-line.ts +123 -0
  70. package/telegram-plugin/gateway/boot-card.ts +169 -40
  71. package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
  72. package/telegram-plugin/gateway/boot-probes.ts +166 -123
  73. package/telegram-plugin/gateway/boot-reason.ts +41 -7
  74. package/telegram-plugin/gateway/boot-version.ts +66 -0
  75. package/telegram-plugin/gateway/gateway.ts +3499 -1885
  76. package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
  77. package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
  78. package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
  79. package/telegram-plugin/gateway/quarantine.ts +69 -0
  80. package/telegram-plugin/gateway/quota-cache.ts +9 -4
  81. package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
  82. package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
  83. package/telegram-plugin/gateway/recent-denials.ts +77 -0
  84. package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
  85. package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
  86. package/telegram-plugin/history.ts +91 -0
  87. package/telegram-plugin/hooks/hooks.json +10 -0
  88. package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
  89. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
  90. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
  91. package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
  92. package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
  93. package/telegram-plugin/inbound-classifier.ts +50 -0
  94. package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
  95. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  96. package/telegram-plugin/package.json +4 -2
  97. package/telegram-plugin/permission-rule.ts +51 -0
  98. package/telegram-plugin/permission-title.ts +56 -0
  99. package/telegram-plugin/quota-check.ts +19 -41
  100. package/telegram-plugin/registry/reaper.ts +223 -0
  101. package/telegram-plugin/retry-api-call.ts +80 -0
  102. package/telegram-plugin/runtime-metrics.ts +177 -0
  103. package/telegram-plugin/scripts/build.mjs +0 -1
  104. package/telegram-plugin/secret-detect/index.ts +24 -0
  105. package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
  106. package/telegram-plugin/secret-detect/vault-error.ts +78 -11
  107. package/telegram-plugin/secret-detect/vault-write.ts +14 -2
  108. package/telegram-plugin/server.js +41795 -0
  109. package/telegram-plugin/session-tail.ts +6 -1
  110. package/telegram-plugin/shared/bot-runtime.ts +5 -4
  111. package/telegram-plugin/silence-poke.ts +420 -0
  112. package/telegram-plugin/silent-end.ts +174 -0
  113. package/telegram-plugin/stream-controller.ts +13 -0
  114. package/telegram-plugin/stream-reply-handler.ts +7 -0
  115. package/telegram-plugin/subagent-watcher.ts +213 -4
  116. package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
  117. package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
  118. package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
  119. package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
  120. package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
  121. package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
  122. package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
  123. package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
  124. package/telegram-plugin/tests/boot-probes.test.ts +216 -10
  125. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  126. package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
  127. package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
  128. package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
  129. package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
  130. package/telegram-plugin/tests/history-reaper.test.ts +378 -0
  131. package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
  132. package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
  133. package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
  134. package/telegram-plugin/tests/issues-card.test.ts +49 -0
  135. package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
  136. package/telegram-plugin/tests/permission-rule.test.ts +80 -1
  137. package/telegram-plugin/tests/permission-title.test.ts +31 -0
  138. package/telegram-plugin/tests/quota-check.test.ts +5 -35
  139. package/telegram-plugin/tests/races.test.ts +179 -0
  140. package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
  141. package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
  142. package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
  143. package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
  144. package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
  145. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
  146. package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
  147. package/telegram-plugin/tests/silence-poke.test.ts +493 -0
  148. package/telegram-plugin/tests/silent-end.test.ts +206 -0
  149. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
  150. package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
  151. package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
  152. package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
  153. package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
  154. package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
  155. package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
  156. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
  157. package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
  158. package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
  159. package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
  160. package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
  161. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
  162. package/telegram-plugin/turn-signal-tracker.ts +100 -24
  163. package/telegram-plugin/uat/SETUP.md +210 -35
  164. package/telegram-plugin/uat/assertions.ts +264 -37
  165. package/telegram-plugin/uat/driver-info.ts +57 -0
  166. package/telegram-plugin/uat/driver.ts +590 -51
  167. package/telegram-plugin/uat/harness.ts +140 -94
  168. package/telegram-plugin/uat/load-env.test.ts +72 -0
  169. package/telegram-plugin/uat/load-env.ts +48 -0
  170. package/telegram-plugin/uat/login.ts +96 -53
  171. package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
  172. package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
  173. package/telegram-plugin/uat/runners/report.ts +150 -0
  174. package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
  175. package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
  176. package/telegram-plugin/uat/runners/scorer.ts +106 -0
  177. package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
  178. package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
  179. package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
  180. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
  181. package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
  182. package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
  183. package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
  184. package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
  185. package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
  186. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
  187. package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
  188. package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
  189. package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
  190. package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
  191. package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
  192. package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
  193. package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
  194. package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
  195. package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
  196. package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
  197. package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
  198. package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
  199. package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
  200. package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
  201. package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
  202. package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
  203. package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
  204. package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
  205. package/telegram-plugin/vault-approval-posture.ts +42 -0
  206. package/telegram-plugin/welcome-text.ts +1 -0
  207. package/telegram-plugin/active-pins-sweep.ts +0 -204
  208. package/telegram-plugin/active-pins.ts +0 -146
  209. package/telegram-plugin/auth-dashboard.ts +0 -1104
  210. package/telegram-plugin/auth-slot-parser.ts +0 -497
  211. package/telegram-plugin/card-event-log.ts +0 -138
  212. package/telegram-plugin/dist/foreman/foreman.js +0 -31106
  213. package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
  214. package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
  215. package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
  216. package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
  217. package/telegram-plugin/foreman/foreman.ts +0 -1165
  218. package/telegram-plugin/foreman/setup-flow.ts +0 -345
  219. package/telegram-plugin/foreman/setup-state.ts +0 -239
  220. package/telegram-plugin/foreman/state.ts +0 -203
  221. package/telegram-plugin/pin-event-log.ts +0 -76
  222. package/telegram-plugin/progress-card-driver.ts +0 -2886
  223. package/telegram-plugin/progress-card-pin-manager.ts +0 -589
  224. package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
  225. package/telegram-plugin/progress-card.ts +0 -1409
  226. package/telegram-plugin/tests/HARNESS.md +0 -340
  227. package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
  228. package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
  229. package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
  230. package/telegram-plugin/tests/active-pins.test.ts +0 -187
  231. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
  232. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
  233. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
  234. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
  235. package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
  236. package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
  237. package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
  238. package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
  239. package/telegram-plugin/tests/card-event-log.test.ts +0 -145
  240. package/telegram-plugin/tests/first-paint.test.ts +0 -257
  241. package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
  242. package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
  243. package/telegram-plugin/tests/foreman-state.test.ts +0 -164
  244. package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
  245. package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
  246. package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
  247. package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
  248. package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
  249. package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
  250. package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
  251. package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
  252. package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
  253. package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
  254. package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
  255. package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
  256. package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
  257. package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
  258. package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
  259. package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
  260. package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
  261. package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
  262. package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
  263. package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
  264. package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
  265. package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
  266. package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
  267. package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
  268. package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
  269. package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
  270. package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
  271. package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
  272. package/telegram-plugin/tests/setup-flow.test.ts +0 -510
  273. package/telegram-plugin/tests/setup-state.test.ts +0 -146
  274. package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
  275. package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
  276. package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
  277. package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
  278. package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
  279. package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
  280. package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
  281. package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
  282. package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
  283. package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
  284. package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
  285. package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
  286. package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
  287. package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
  288. package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
  289. package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
  290. package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
  291. package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
  292. package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
  293. package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
  294. package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
  295. package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
  296. package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
  297. package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
  298. package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
  299. package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
  300. package/telegram-plugin/two-zone-card.ts +0 -269
  301. package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
@@ -1,187 +0,0 @@
1
- /**
2
- * PR-C2 — additional golden snapshots for renderTwoZoneCard not
3
- * covered by two-zone-card-snapshot.test.ts:
4
- *
5
- * 1. silent-end + bg fleet running (silentEnd lifted above
6
- * Background; the bg member still appears in the FLEET zone).
7
- * 2. stalled-close header (`stalledClose` precedence dominates).
8
- * 3. Parent zone "(+N earlier)" overflow when items.length >
9
- * PARENT_BULLET_CAP (=8).
10
- *
11
- * fails when: phaseFor's precedence regresses (silentEnd no longer
12
- * lifted above background), the stalledClose label changes, or
13
- * PARENT_BULLET_CAP overflow rendering drops the "(+N earlier)" prefix.
14
- */
15
- import { describe, it, expect } from 'vitest'
16
- import { renderTwoZoneCard } from '../two-zone-card.js'
17
- import type { FleetMember } from '../fleet-state.js'
18
- import type { ProgressCardState } from '../progress-card.js'
19
-
20
- function fm(over: Partial<FleetMember>): FleetMember {
21
- return {
22
- agentId: 'aaaaaa00',
23
- role: 'agent',
24
- startedAt: 0,
25
- toolCount: 0,
26
- lastActivityAt: 0,
27
- lastTool: null,
28
- status: 'running',
29
- terminalAt: null,
30
- errorSeen: false,
31
- originatingTurnKey: 'k',
32
- ...over,
33
- }
34
- }
35
-
36
- function st(over: Partial<ProgressCardState> & { stage: ProgressCardState['stage'] }): ProgressCardState {
37
- return {
38
- turnStartedAt: 0,
39
- items: [],
40
- narratives: [],
41
- stage: over.stage,
42
- thinking: false,
43
- subAgents: new Map(),
44
- pendingAgentSpawns: new Map(),
45
- tasks: [],
46
- ...over,
47
- }
48
- }
49
-
50
- const NOW = 100_000
51
-
52
- describe('PR-C2: two-zone card snapshot extras', () => {
53
- it('silent-end + bg fleet still running → header is "Ended without reply", FLEET shows bg member', () => {
54
- const fleet = new Map([
55
- ['a', fm({
56
- agentId: 'aaaaaa01', role: 'background', status: 'background',
57
- toolCount: 7, lastActivityAt: NOW - 2000,
58
- lastTool: { name: 'Bash', sanitisedArg: 'long.sh' },
59
- })],
60
- ])
61
- const out = renderTwoZoneCard({
62
- state: st({ stage: 'done', turnStartedAt: NOW - 30_000 }),
63
- fleet,
64
- now: NOW,
65
- opts: { silentEnd: true },
66
- })
67
- expect(out).toBe(
68
- '🙊 <b>Ended without reply</b> · ⏱ 00:30 · 🔧 7 · 🤖 1\n' +
69
- '\n' +
70
- '<b>FLEET (1)</b>\n' +
71
- '⏸ background <code>aaaaaa</code> · 7t · Bash <code>long.sh</code> (2s ago)',
72
- )
73
- })
74
-
75
- it('stalled-close header dominates regardless of fleet state', () => {
76
- const fleet = new Map([
77
- ['a', fm({ agentId: 'aaaaaa01', role: 'worker', status: 'running', toolCount: 3, lastActivityAt: NOW - 1000 })],
78
- ])
79
- const out = renderTwoZoneCard({
80
- state: st({ stage: 'run', turnStartedAt: NOW - 60_000 }),
81
- fleet,
82
- now: NOW,
83
- opts: { stalledClose: true },
84
- })
85
- // Header begins with the "Forced close" phase. We don't snapshot the
86
- // full body — just lock down the header and the icon.
87
- expect(out.startsWith('⚠ <b>Forced close</b> · ⏱ 01:00')).toBe(true)
88
- })
89
-
90
- it('parent zone overflow: "(+N earlier)" prefix when items > PARENT_BULLET_CAP=8', () => {
91
- const items = Array.from({ length: 12 }, (_, i) => ({
92
- tool: 'Read',
93
- label: `f${i}.ts`,
94
- }))
95
- const out = renderTwoZoneCard({
96
- state: st({ stage: 'run', turnStartedAt: NOW - 5000, items }),
97
- fleet: new Map(),
98
- now: NOW,
99
- })
100
- // 12 items, cap 8 → 4 hidden.
101
- expect(out).toContain('(+4 earlier)')
102
- // The visible bullets are the LAST 8 (slice(-8) → f4..f11).
103
- // f11 is the in-flight bullet (stage=run, last index) → ◉.
104
- expect(out).toContain('◉ f11.ts')
105
- expect(out).toContain('● f4.ts')
106
- // f3 (the latest hidden) must not appear as a bullet.
107
- expect(out).not.toContain('f3.ts')
108
- // No <code> wrapping around row labels anymore.
109
- expect(out).not.toContain('<code>f11.ts</code>')
110
- })
111
-
112
- it('parent zone: in-flight last bullet uses ◉ <plain>; earlier use ● <plain>', () => {
113
- const items = [
114
- { tool: 'Read', label: 'a.ts' },
115
- { tool: 'Read', label: 'b.ts' },
116
- { tool: 'Bash', label: 'ls' },
117
- ]
118
- const out = renderTwoZoneCard({
119
- state: st({ stage: 'run', turnStartedAt: NOW - 5000, items }),
120
- fleet: new Map(),
121
- now: NOW,
122
- })
123
- // last item active — plain text, no <b>, no <code>, no tool prefix
124
- expect(out).toContain('◉ ls')
125
- expect(out).not.toContain('◉ <b>')
126
- // earlier items — plain text only, no tool prefix
127
- expect(out).toContain('● a.ts')
128
- expect(out).toContain('● b.ts')
129
- expect(out).not.toContain('Read <code>')
130
- // No <code> wrapping anywhere on parent rows.
131
- expect(out).not.toContain('<code>ls</code>')
132
- expect(out).not.toContain('<code>a.ts</code>')
133
- })
134
-
135
- it('parent zone: when stage=done all bullets render as ● (no active marker)', () => {
136
- const items = [
137
- { tool: 'Read', label: 'a.ts' },
138
- { tool: 'Bash', label: 'ls' },
139
- ]
140
- const out = renderTwoZoneCard({
141
- state: st({ stage: 'done', turnStartedAt: NOW - 5000, items }),
142
- fleet: new Map(),
143
- now: NOW,
144
- })
145
- expect(out).toContain('● a.ts')
146
- expect(out).toContain('● ls')
147
- expect(out).not.toContain('◉')
148
- })
149
-
150
- it('parent zone: row with no label falls back to humanised tool name', () => {
151
- const items = [
152
- { tool: 'TodoWrite', label: '' },
153
- { tool: 'Edit', label: '' },
154
- ]
155
- const out = renderTwoZoneCard({
156
- state: st({ stage: 'run', turnStartedAt: NOW - 5000, items }),
157
- fleet: new Map(),
158
- now: NOW,
159
- })
160
- expect(out).toContain('● updating tasks')
161
- expect(out).toContain('◉ editing file')
162
- })
163
-
164
- it('parent zone: row with no label on mcp tool uses mcpDisplayName', () => {
165
- const items = [
166
- { tool: 'mcp__switchroom-telegram__reply', label: '' },
167
- ]
168
- const out = renderTwoZoneCard({
169
- state: st({ stage: 'run', turnStartedAt: NOW - 5000, items }),
170
- fleet: new Map(),
171
- now: NOW,
172
- })
173
- expect(out).toContain('◉ Telegram: reply')
174
- })
175
-
176
- it('parent zone: HTML in label is escaped (no raw <code> styling)', () => {
177
- const items = [
178
- { tool: 'Bash', label: 'echo <hi>' },
179
- ]
180
- const out = renderTwoZoneCard({
181
- state: st({ stage: 'done', turnStartedAt: NOW - 5000, items }),
182
- fleet: new Map(),
183
- now: NOW,
184
- })
185
- expect(out).toContain('● echo &lt;hi&gt;')
186
- })
187
- })
@@ -1,149 +0,0 @@
1
- /**
2
- * P3 of #662 — stuck escalation respects the edit throttle.
3
- *
4
- * Tests that stuck-detection adds at most one extra emit beyond the
5
- * heartbeat baseline. Compares two 100s runs: one where keep-alive
6
- * events prevent the member from going stuck, one where it does.
7
- * The difference should be ≤1 (the stuck transition itself).
8
- */
9
-
10
- import { describe, it, expect } from 'vitest'
11
- import { createProgressDriver } from '../progress-card-driver.js'
12
- import type { SessionEvent } from '../session-tail.js'
13
-
14
- interface Timer {
15
- fireAt: number
16
- fn: () => void
17
- ref: number
18
- repeat?: number
19
- }
20
-
21
- function createHarness() {
22
- let now = 1000
23
- const timers: Timer[] = []
24
- let nextRef = 0
25
- const emits: Array<{ html: string; isFirstEmit: boolean }> = []
26
- const driver = createProgressDriver({
27
- emit: (e) => {
28
- emits.push({ html: e.html, isFirstEmit: e.isFirstEmit })
29
- },
30
- minIntervalMs: 500,
31
- coalesceMs: 400,
32
- initialDelayMs: 0,
33
- promoteAfterMs: 999_999,
34
- heartbeatMs: 5000,
35
- now: () => now,
36
- setTimeout: (fn, ms) => {
37
- const ref = nextRef++
38
- timers.push({ fireAt: now + ms, fn, ref })
39
- return { ref }
40
- },
41
- clearTimeout: (h) => {
42
- const ref = (h as { ref: number }).ref
43
- const idx = timers.findIndex((t) => t.ref === ref)
44
- if (idx !== -1) timers.splice(idx, 1)
45
- },
46
- setInterval: (fn, ms) => {
47
- const ref = nextRef++
48
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
49
- return { ref }
50
- },
51
- clearInterval: (h) => {
52
- const ref = (h as { ref: number }).ref
53
- const idx = timers.findIndex((t) => t.ref === ref)
54
- if (idx !== -1) timers.splice(idx, 1)
55
- },
56
- })
57
- function advance(ms: number) {
58
- const target = now + ms
59
- while (true) {
60
- const due = timers
61
- .filter((t) => t.fireAt <= target)
62
- .sort((a, b) => a.fireAt - b.fireAt)
63
- if (due.length === 0) break
64
- const t = due[0]
65
- now = t.fireAt
66
- t.fn()
67
- if (t.repeat) {
68
- t.fireAt = now + t.repeat
69
- } else {
70
- const idx = timers.indexOf(t)
71
- if (idx !== -1) timers.splice(idx, 1)
72
- }
73
- }
74
- now = target
75
- }
76
- return { driver, advance, emits, getNow: () => now }
77
- }
78
-
79
- const enqueue = (chatId: string): SessionEvent => ({
80
- kind: 'enqueue',
81
- chatId,
82
- messageId: '1',
83
- threadId: null,
84
- rawContent: `<channel chat_id="${chatId}">go</channel>`,
85
- })
86
-
87
- const toolUse = (toolUseId: string): SessionEvent => ({
88
- kind: 'sub_agent_tool_use',
89
- agentId: 'sa1',
90
- toolUseId,
91
- toolName: 'Read',
92
- input: { file_path: '/tmp/x' },
93
- })
94
-
95
- function runHarness(keepAlive: boolean): { emitCount: number } {
96
- const { driver, advance, emits, getNow } = createHarness()
97
- const CHAT = 'c1'
98
- driver.ingest(enqueue(CHAT), null)
99
- driver.ingest(
100
- { kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work', subagentType: 'worker' },
101
- CHAT,
102
- )
103
- const startEmits = emits.length
104
-
105
- // Run 100s total. If keepAlive, dispatch tool_use events at 30s intervals
106
- // to keep lastActivityAt fresh and prevent the member from going stuck.
107
- // Otherwise, let it go stuck at ~60s.
108
- if (keepAlive) {
109
- for (let elapsed = 0; elapsed < 100_000; elapsed += 30_000) {
110
- driver.ingest(toolUse(`tu-${elapsed}`), CHAT)
111
- advance(30_000)
112
- }
113
- } else {
114
- advance(100_000)
115
- }
116
-
117
- return { emitCount: emits.length - startEmits }
118
- }
119
-
120
- describe('P3 stuck escalation — edit throttle', () => {
121
- it('stuck-detection adds at most one extra emit beyond heartbeat baseline', () => {
122
- // Baseline: 100s with keep-alive events every 30s. Member never goes
123
- // stuck. Heartbeat fires ~20 times due to elapsed-time changes (by
124
- // design, matches #314's elapsed-ticker JTBD).
125
- const baseline = runHarness(true)
126
-
127
- // With stuck transition: 100s with no keep-alive. Member crosses stuck
128
- // threshold at ~60s; markStuck flips status once.
129
- const stuck = runHarness(false)
130
-
131
- // The delta should be 0 or 1 — the stuck transition is content-changing
132
- // so it could add one emit, but it likely lands on a heartbeat tick that
133
- // was emitting anyway.
134
- // The stuck path must not emit a storm — at most one extra emit
135
- // beyond the keep-alive baseline. (The keep-alive baseline can be
136
- // HIGHER than the stuck path because each tool_use event is itself
137
- // content-changing; that's fine — we only care that stuck-detection
138
- // doesn't ADD emits beyond the heartbeat-driven elapsed updates.)
139
- expect(stuck.emitCount).toBeLessThanOrEqual(baseline.emitCount + 1)
140
- })
141
-
142
- it('stuck transition does not produce a runaway edit storm', () => {
143
- // Sanity: 100s of pure silence (no keep-alive) emits well under the
144
- // ~20-tick heartbeat ceiling plus a single stuck transition. This
145
- // catches a regression where markStuck would re-fire every tick.
146
- const stuck = runHarness(false)
147
- expect(stuck.emitCount).toBeLessThanOrEqual(25)
148
- })
149
- })
@@ -1,101 +0,0 @@
1
- /**
2
- * P3 of #662 — header escalation data side.
3
- *
4
- * P1's renderer reads fleet member statuses to compute the ⚠ Stalled
5
- * header phase. P3's job is just to make sure the data correctly
6
- * reflects "every running member is stuck" once the threshold passes.
7
- */
8
-
9
- import { describe, it, expect } from 'vitest'
10
- import { createProgressDriver } from '../progress-card-driver.js'
11
- import type { SessionEvent } from '../session-tail.js'
12
-
13
- interface Timer {
14
- fireAt: number
15
- fn: () => void
16
- ref: number
17
- repeat?: number
18
- }
19
-
20
- function harness() {
21
- let now = 1000
22
- const timers: Timer[] = []
23
- let nextRef = 0
24
- const driver = createProgressDriver({
25
- emit: () => {},
26
- minIntervalMs: 500,
27
- coalesceMs: 400,
28
- initialDelayMs: 0,
29
- promoteAfterMs: 999_999,
30
- heartbeatMs: 5000,
31
- now: () => now,
32
- setTimeout: (fn, ms) => {
33
- const ref = nextRef++
34
- timers.push({ fireAt: now + ms, fn, ref })
35
- return { ref }
36
- },
37
- clearTimeout: (h) => {
38
- const ref = (h as { ref: number }).ref
39
- const idx = timers.findIndex((t) => t.ref === ref)
40
- if (idx !== -1) timers.splice(idx, 1)
41
- },
42
- setInterval: (fn, ms) => {
43
- const ref = nextRef++
44
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
45
- return { ref }
46
- },
47
- clearInterval: (h) => {
48
- const ref = (h as { ref: number }).ref
49
- const idx = timers.findIndex((t) => t.ref === ref)
50
- if (idx !== -1) timers.splice(idx, 1)
51
- },
52
- })
53
- function advance(ms: number) {
54
- const target = now + ms
55
- while (true) {
56
- const due = timers
57
- .filter((t) => t.fireAt <= target)
58
- .sort((a, b) => a.fireAt - b.fireAt)
59
- if (due.length === 0) break
60
- const t = due[0]
61
- now = t.fireAt
62
- t.fn()
63
- if (t.repeat) {
64
- t.fireAt = now + t.repeat
65
- } else {
66
- const idx = timers.indexOf(t)
67
- if (idx !== -1) timers.splice(idx, 1)
68
- }
69
- }
70
- now = target
71
- }
72
- return { driver, advance }
73
- }
74
-
75
- const enqueue = (chatId: string): SessionEvent => ({
76
- kind: 'enqueue',
77
- chatId,
78
- messageId: '1',
79
- threadId: null,
80
- rawContent: `<channel chat_id="${chatId}">go</channel>`,
81
- })
82
-
83
- describe('P3 stuck escalation — header escalation (data side)', () => {
84
- it('two simultaneously idle members both flip to stuck after 60s', () => {
85
- const { driver, advance } = harness()
86
- const CHAT = 'c1'
87
- driver.ingest(enqueue(CHAT), null)
88
- driver.ingest(
89
- { kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'a', subagentType: 'worker' },
90
- CHAT,
91
- )
92
- driver.ingest(
93
- { kind: 'sub_agent_started', agentId: 'sa2', firstPromptText: 'b', subagentType: 'worker' },
94
- CHAT,
95
- )
96
- advance(61_000)
97
- const fleet = driver.peekFleet(CHAT)!
98
- expect(fleet.get('sa1')!.status).toBe('stuck')
99
- expect(fleet.get('sa2')!.status).toBe('stuck')
100
- })
101
- })
@@ -1,114 +0,0 @@
1
- /**
2
- * P3 of #662 — per-member stuck escalation.
3
- *
4
- * Drives the real createProgressDriver heartbeat tick across the 60s
5
- * threshold and asserts the fleet member's `status` flips
6
- * `running` → `stuck` exactly when `now - lastActivityAt > 60_000`.
7
- */
8
-
9
- import { describe, it, expect } from 'vitest'
10
- import { createProgressDriver } from '../progress-card-driver.js'
11
- import type { SessionEvent } from '../session-tail.js'
12
-
13
- interface Timer {
14
- fireAt: number
15
- fn: () => void
16
- ref: number
17
- repeat?: number
18
- }
19
-
20
- function harness(opts: { heartbeatMs?: number } = {}) {
21
- let now = 1000
22
- const timers: Timer[] = []
23
- let nextRef = 0
24
- const driver = createProgressDriver({
25
- emit: () => {},
26
- minIntervalMs: 500,
27
- coalesceMs: 400,
28
- initialDelayMs: 0,
29
- promoteAfterMs: 999_999,
30
- heartbeatMs: opts.heartbeatMs ?? 5000,
31
- now: () => now,
32
- setTimeout: (fn, ms) => {
33
- const ref = nextRef++
34
- timers.push({ fireAt: now + ms, fn, ref })
35
- return { ref }
36
- },
37
- clearTimeout: (h) => {
38
- const ref = (h as { ref: number }).ref
39
- const idx = timers.findIndex((t) => t.ref === ref)
40
- if (idx !== -1) timers.splice(idx, 1)
41
- },
42
- setInterval: (fn, ms) => {
43
- const ref = nextRef++
44
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
45
- return { ref }
46
- },
47
- clearInterval: (h) => {
48
- const ref = (h as { ref: number }).ref
49
- const idx = timers.findIndex((t) => t.ref === ref)
50
- if (idx !== -1) timers.splice(idx, 1)
51
- },
52
- })
53
- function advance(ms: number) {
54
- const target = now + ms
55
- // Fire all due timers (including repeating heartbeat) up to target.
56
- // Loop until no due timers remain to handle re-scheduled timers
57
- // synthesised inside fired callbacks.
58
- while (true) {
59
- const due = timers
60
- .filter((t) => t.fireAt <= target)
61
- .sort((a, b) => a.fireAt - b.fireAt)
62
- if (due.length === 0) break
63
- const t = due[0]
64
- now = t.fireAt
65
- t.fn()
66
- if (t.repeat) {
67
- t.fireAt = now + t.repeat
68
- } else {
69
- const idx = timers.indexOf(t)
70
- if (idx !== -1) timers.splice(idx, 1)
71
- }
72
- }
73
- now = target
74
- }
75
- return { driver, advance, getNow: () => now, timers }
76
- }
77
-
78
- const enqueue = (chatId: string): SessionEvent => ({
79
- kind: 'enqueue',
80
- chatId,
81
- messageId: '1',
82
- threadId: null,
83
- rawContent: `<channel chat_id="${chatId}">go</channel>`,
84
- })
85
-
86
- describe('P3 stuck escalation — per-member', () => {
87
- it('fleet member stays running at 59s of idle', () => {
88
- const { driver, advance } = harness({ heartbeatMs: 5000 })
89
- const CHAT = 'c1'
90
- driver.ingest(enqueue(CHAT), null)
91
- driver.ingest(
92
- { kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work', subagentType: 'worker' },
93
- CHAT,
94
- )
95
- // Advance 59s — heartbeat fires multiple times but we are still
96
- // within the 60s idle window, so no stuck transition should happen.
97
- advance(59_000)
98
- const m = driver.peekFleet(CHAT)!.get('sa1')!
99
- expect(m.status).toBe('running')
100
- })
101
-
102
- it('fleet member flips to stuck at >60s of idle', () => {
103
- const { driver, advance } = harness({ heartbeatMs: 5000 })
104
- const CHAT = 'c1'
105
- driver.ingest(enqueue(CHAT), null)
106
- driver.ingest(
107
- { kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work', subagentType: 'worker' },
108
- CHAT,
109
- )
110
- advance(61_000)
111
- const m = driver.peekFleet(CHAT)!.get('sa1')!
112
- expect(m.status).toBe('stuck')
113
- })
114
- })
@@ -1,105 +0,0 @@
1
- /**
2
- * P3 of #662 — stuck → running recovery.
3
- *
4
- * After a member is marked stuck via the heartbeat tick, a new
5
- * sub_agent_tool_use event must reset status to running and refresh
6
- * lastActivityAt to the event's now.
7
- */
8
-
9
- import { describe, it, expect } from 'vitest'
10
- import { createProgressDriver } from '../progress-card-driver.js'
11
- import type { SessionEvent } from '../session-tail.js'
12
-
13
- interface Timer {
14
- fireAt: number
15
- fn: () => void
16
- ref: number
17
- repeat?: number
18
- }
19
-
20
- function harness() {
21
- let now = 1000
22
- const timers: Timer[] = []
23
- let nextRef = 0
24
- const driver = createProgressDriver({
25
- emit: () => {},
26
- minIntervalMs: 500,
27
- coalesceMs: 400,
28
- initialDelayMs: 0,
29
- promoteAfterMs: 999_999,
30
- heartbeatMs: 5000,
31
- now: () => now,
32
- setTimeout: (fn, ms) => {
33
- const ref = nextRef++
34
- timers.push({ fireAt: now + ms, fn, ref })
35
- return { ref }
36
- },
37
- clearTimeout: (h) => {
38
- const ref = (h as { ref: number }).ref
39
- const idx = timers.findIndex((t) => t.ref === ref)
40
- if (idx !== -1) timers.splice(idx, 1)
41
- },
42
- setInterval: (fn, ms) => {
43
- const ref = nextRef++
44
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
45
- return { ref }
46
- },
47
- clearInterval: (h) => {
48
- const ref = (h as { ref: number }).ref
49
- const idx = timers.findIndex((t) => t.ref === ref)
50
- if (idx !== -1) timers.splice(idx, 1)
51
- },
52
- })
53
- function advance(ms: number) {
54
- const target = now + ms
55
- while (true) {
56
- const due = timers
57
- .filter((t) => t.fireAt <= target)
58
- .sort((a, b) => a.fireAt - b.fireAt)
59
- if (due.length === 0) break
60
- const t = due[0]
61
- now = t.fireAt
62
- t.fn()
63
- if (t.repeat) {
64
- t.fireAt = now + t.repeat
65
- } else {
66
- const idx = timers.indexOf(t)
67
- if (idx !== -1) timers.splice(idx, 1)
68
- }
69
- }
70
- now = target
71
- }
72
- return { driver, advance, getNow: () => now }
73
- }
74
-
75
- const enqueue = (chatId: string): SessionEvent => ({
76
- kind: 'enqueue',
77
- chatId,
78
- messageId: '1',
79
- threadId: null,
80
- rawContent: `<channel chat_id="${chatId}">go</channel>`,
81
- })
82
-
83
- describe('P3 stuck escalation — recovery', () => {
84
- it('next sub_agent_tool_use after stuck flips status back to running and bumps lastActivityAt', () => {
85
- const { driver, advance, getNow } = harness()
86
- const CHAT = 'c1'
87
- driver.ingest(enqueue(CHAT), null)
88
- driver.ingest(
89
- { kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work', subagentType: 'worker' },
90
- CHAT,
91
- )
92
- advance(61_000)
93
- const stuck = driver.peekFleet(CHAT)!.get('sa1')!
94
- expect(stuck.status).toBe('stuck')
95
-
96
- // Now a real tool event arrives — recovery.
97
- driver.ingest(
98
- { kind: 'sub_agent_tool_use', agentId: 'sa1', toolUseId: 't1', toolName: 'Read', input: { file_path: '/tmp/x.ts' } },
99
- CHAT,
100
- )
101
- const recovered = driver.peekFleet(CHAT)!.get('sa1')!
102
- expect(recovered.status).toBe('running')
103
- expect(recovered.lastActivityAt).toBe(getNow())
104
- })
105
- })