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,272 +0,0 @@
1
- /**
2
- * Refactor regression: every per-chat close site must end in the SAME
3
- * post-conditions on the driver's internal state. Pre-refactor, three
4
- * code paths (turn_end → completeTurnFully, heartbeat zombie ceiling →
5
- * closeZombie, Gap-8 deferred-completion timeout → inline) reproduced
6
- * the cleanup tail by hand and diverged on edge cases. The refactor
7
- * funnels them all through `closePerChat(reason)` so the only remaining
8
- * deltas are:
9
- *
10
- * - 'turn-end': no sub-agent force-close (none are running).
11
- * - 'zombie' : force-close running sub-agents; preserve
12
- * pendingSyncEchoes (echo may still arrive).
13
- * - 'stalled' : force-close running sub-agents; flush(stalledClose=true).
14
- *
15
- * This test drives all three reasons against a fresh driver instance
16
- * and asserts the convergent post-conditions. It is the load-bearing
17
- * test for the unified close path — if it fails, the refactor regressed.
18
- */
19
- import { describe, it, expect } from 'vitest'
20
- import { createProgressDriver } from '../progress-card-driver.js'
21
- import type { SessionEvent } from '../session-tail.js'
22
-
23
- let nextMsgId = 7000
24
-
25
- function harness(opts?: { maxIdleMs?: number; deferredCompletionTimeoutMs?: number }) {
26
- let now = 1000
27
- const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
28
- let nextRef = 0
29
- const emits: Array<{ chatId: string; threadId?: string; turnKey: string; html: string; done: boolean }> = []
30
-
31
- const driver = createProgressDriver({
32
- emit: (a) => emits.push(a),
33
- minIntervalMs: 0,
34
- coalesceMs: 0,
35
- initialDelayMs: 0,
36
- heartbeatMs: 1_000,
37
- maxIdleMs: opts?.maxIdleMs ?? 30_000,
38
- deferredCompletionTimeoutMs: opts?.deferredCompletionTimeoutMs ?? 10_000,
39
- now: () => now,
40
- setTimeout: (fn, ms) => {
41
- const ref = nextRef++
42
- timers.push({ fireAt: now + ms, fn, ref })
43
- return { ref }
44
- },
45
- clearTimeout: (handle) => {
46
- const target = (handle as { ref: number }).ref
47
- const idx = timers.findIndex((t) => t.ref === target)
48
- if (idx !== -1) timers.splice(idx, 1)
49
- },
50
- setInterval: (fn, ms) => {
51
- const ref = nextRef++
52
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
53
- return { ref }
54
- },
55
- clearInterval: (handle) => {
56
- const target = (handle as { ref: number }).ref
57
- const idx = timers.findIndex((t) => t.ref === target)
58
- if (idx !== -1) timers.splice(idx, 1)
59
- },
60
- })
61
-
62
- const advance = (ms: number): void => {
63
- now += ms
64
- for (;;) {
65
- timers.sort((a, b) => a.fireAt - b.fireAt)
66
- const next = timers[0]
67
- if (!next || next.fireAt > now) break
68
- if (next.repeat != null) {
69
- next.fireAt += next.repeat
70
- next.fn()
71
- } else {
72
- timers.shift()
73
- next.fn()
74
- }
75
- }
76
- }
77
-
78
- return { driver, emits, advance, getNow: () => now }
79
- }
80
-
81
- function enqueue(chatId: string): SessionEvent {
82
- return {
83
- kind: 'enqueue',
84
- chatId,
85
- messageId: String(nextMsgId++),
86
- threadId: null,
87
- rawContent: `<channel chat_id="${chatId}">go</channel>`,
88
- }
89
- }
90
-
91
- describe('progress-card-driver: all close paths converge on identical final state', () => {
92
- it("'turn-end' path: chats empty, baseTurnSeqs cleaned, heartbeat stopped", () => {
93
- const { driver } = harness()
94
- const maps = driver._debugGetMaps!()
95
-
96
- driver.ingest(enqueue('cA'), null)
97
- expect(maps.chats.size).toBe(1)
98
- expect(maps.baseTurnSeqs.has('cA')).toBe(true)
99
-
100
- driver.ingest({ kind: 'turn_end', durationMs: 50 }, 'cA')
101
-
102
- expect(maps.chats.size).toBe(0)
103
- expect(maps.baseTurnSeqs.has('cA')).toBe(false)
104
- expect(maps.chatRunningSubagents.has('cA')).toBe(false)
105
- })
106
-
107
- it("'zombie' path (heartbeat maxIdle ceiling): same convergence + pendingSyncEchoes preserved", () => {
108
- const { driver, advance } = harness({ maxIdleMs: 5_000 })
109
- const maps = driver._debugGetMaps!()
110
-
111
- driver.ingest(enqueue('cA'), null)
112
- expect(maps.chats.size).toBe(1)
113
-
114
- // Seed a pending sync-echo so we can assert the zombie path leaves it
115
- // in place (the echo may still arrive after close).
116
- maps.pendingSyncEchoes.set('cA:fake', 1000)
117
-
118
- // Idle past maxIdleMs so the heartbeat reclassifies the card as zombie.
119
- advance(20_000)
120
-
121
- expect(maps.chats.size).toBe(0)
122
- expect(maps.baseTurnSeqs.has('cA')).toBe(false)
123
- expect(maps.chatRunningSubagents.has('cA')).toBe(false)
124
- // CRITICAL invariant: zombie close must NOT clear pendingSyncEchoes.
125
- // The dedup map's TTL eviction (maybeEvict) reaps it later.
126
- expect(maps.pendingSyncEchoes.has('cA:fake')).toBe(true)
127
- })
128
-
129
- it("'zombie' path also force-closes running sub-agents (sync registry drained)", () => {
130
- const { driver, advance } = harness({ maxIdleMs: 5_000 })
131
- const maps = driver._debugGetMaps!()
132
-
133
- driver.ingest(enqueue('cA'), null)
134
- driver.ingest({ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work' }, 'cA')
135
- expect(maps.chats.size).toBe(1)
136
-
137
- // Idle past maxIdleMs without ever reporting sub_agent_turn_end.
138
- advance(20_000)
139
-
140
- expect(maps.chats.size).toBe(0)
141
- // Issue #399: sync registry must be drained even when sub-agents
142
- // never reported their own turn_end.
143
- expect(maps.chatRunningSubagents.has('cA')).toBe(false)
144
- })
145
-
146
- it("'stalled' path (Gap-8 deferred-completion timeout): same convergence", () => {
147
- const { driver, advance } = harness({
148
- maxIdleMs: 999_999, // disable zombie ceiling so we hit the stalled branch
149
- deferredCompletionTimeoutMs: 5_000,
150
- })
151
- const maps = driver._debugGetMaps!()
152
-
153
- driver.ingest(enqueue('cA'), null)
154
- // Spawn a background sub-agent so parent turn_end defers instead of
155
- // closing immediately.
156
- driver.ingest(
157
- {
158
- kind: 'tool_use',
159
- toolName: 'Agent',
160
- toolUseId: 'tu1',
161
- input: { prompt: 'bg', run_in_background: true },
162
- },
163
- 'cA',
164
- )
165
- driver.ingest({ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'bg' }, 'cA')
166
- driver.ingest({ kind: 'turn_end', durationMs: 100 }, 'cA')
167
- // After parent turn_end: card alive in pendingCompletion.
168
- expect(maps.chats.size).toBe(1)
169
-
170
- // Sub-agent never reports done; advance past the deferred timeout so
171
- // the heartbeat's stalled-cards branch fires.
172
- advance(15_000)
173
-
174
- expect(maps.chats.size).toBe(0)
175
- expect(maps.baseTurnSeqs.has('cA')).toBe(false)
176
- expect(maps.chatRunningSubagents.has('cA')).toBe(false)
177
- })
178
-
179
- it('all three paths fire onTurnComplete callback exactly once', () => {
180
- // The completion callback is the externally-visible side-effect that
181
- // gates everything downstream (Stop hook, summary writer). Every
182
- // close path must fire it; the unified path makes that automatic
183
- // because the cleanup tail in completeTurnFully gates on
184
- // completionFired.
185
- const calls: string[] = []
186
- const opts = {
187
- onTurnComplete: (a: { turnKey: string }) => {
188
- calls.push(a.turnKey)
189
- },
190
- }
191
- let now = 1000
192
- const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
193
- let nextRef = 0
194
- const driver = createProgressDriver({
195
- emit: () => {},
196
- minIntervalMs: 0,
197
- coalesceMs: 0,
198
- initialDelayMs: 0,
199
- heartbeatMs: 1_000,
200
- maxIdleMs: 5_000,
201
- deferredCompletionTimeoutMs: 5_000,
202
- now: () => now,
203
- setTimeout: (fn, ms) => {
204
- const ref = nextRef++
205
- timers.push({ fireAt: now + ms, fn, ref })
206
- return { ref }
207
- },
208
- clearTimeout: (h) => {
209
- const ref = (h as { ref: number }).ref
210
- const idx = timers.findIndex((t) => t.ref === ref)
211
- if (idx !== -1) timers.splice(idx, 1)
212
- },
213
- setInterval: (fn, ms) => {
214
- const ref = nextRef++
215
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
216
- return { ref }
217
- },
218
- clearInterval: (h) => {
219
- const ref = (h as { ref: number }).ref
220
- const idx = timers.findIndex((t) => t.ref === ref)
221
- if (idx !== -1) timers.splice(idx, 1)
222
- },
223
- ...opts,
224
- })
225
- const advance = (ms: number): void => {
226
- now += ms
227
- for (;;) {
228
- timers.sort((a, b) => a.fireAt - b.fireAt)
229
- const next = timers[0]
230
- if (!next || next.fireAt > now) break
231
- if (next.repeat != null) {
232
- next.fireAt += next.repeat
233
- next.fn()
234
- } else {
235
- timers.shift()
236
- next.fn()
237
- }
238
- }
239
- }
240
-
241
- // turn-end
242
- driver.ingest(enqueue('cA'), null)
243
- driver.ingest({ kind: 'turn_end', durationMs: 10 }, 'cA')
244
- // zombie
245
- driver.ingest(enqueue('cB'), null)
246
- advance(20_000)
247
- // stalled (after time has advanced to satisfy the deferred timeout)
248
- driver.ingest(enqueue('cC'), null)
249
- driver.ingest(
250
- {
251
- kind: 'tool_use',
252
- toolName: 'Agent',
253
- toolUseId: 'tu',
254
- input: { prompt: 'bg', run_in_background: true },
255
- },
256
- 'cC',
257
- )
258
- driver.ingest({ kind: 'sub_agent_started', agentId: 'sa', firstPromptText: 'bg' }, 'cC')
259
- driver.ingest({ kind: 'turn_end', durationMs: 10 }, 'cC')
260
- advance(20_000)
261
-
262
- // Each chat got exactly one completion callback.
263
- const byChat = new Map<string, number>()
264
- for (const tk of calls) {
265
- const chat = tk.split(':')[0]
266
- byChat.set(chat, (byChat.get(chat) ?? 0) + 1)
267
- }
268
- expect(byChat.get('cA')).toBe(1)
269
- expect(byChat.get('cB')).toBe(1)
270
- expect(byChat.get('cC')).toBe(1)
271
- })
272
- })
@@ -1,258 +0,0 @@
1
- /**
2
- * Tests for issue #334 — cross-turn sub-agent visibility.
3
- *
4
- * A background sub-agent dispatched in turn N (via Agent({run_in_background:true}))
5
- * must remain visible on the new progress card that appears when turn N+1 starts.
6
- */
7
- import { describe, it, expect } from 'vitest'
8
- import { createProgressDriver } from '../progress-card-driver.js'
9
- import type { SessionEvent } from '../session-tail.js'
10
-
11
- let nextMsgId = 100
12
-
13
- function harness(
14
- initialDelayMs = 0,
15
- opts: { coldSubAgentThresholdMs?: number; heartbeatMs?: number } = {},
16
- ) {
17
- let now = 1000
18
- const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
19
- let nextRef = 0
20
- const emits: Array<{ chatId: string; threadId?: string; turnKey: string; html: string; done: boolean }> = []
21
-
22
- const driver = createProgressDriver({
23
- emit: (a) => emits.push(a),
24
- minIntervalMs: 0,
25
- coalesceMs: 0,
26
- initialDelayMs,
27
- coldSubAgentThresholdMs: opts.coldSubAgentThresholdMs,
28
- heartbeatMs: opts.heartbeatMs,
29
- now: () => now,
30
- setTimeout: (fn, ms) => {
31
- const ref = nextRef++
32
- timers.push({ fireAt: now + ms, fn, ref })
33
- return { ref }
34
- },
35
- clearTimeout: (handle) => {
36
- const target = (handle as { ref: number }).ref
37
- const idx = timers.findIndex((t) => t.ref === target)
38
- if (idx !== -1) timers.splice(idx, 1)
39
- },
40
- setInterval: (fn, ms) => {
41
- const ref = nextRef++
42
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
43
- return { ref }
44
- },
45
- clearInterval: (handle) => {
46
- const target = (handle as { ref: number }).ref
47
- const idx = timers.findIndex((t) => t.ref === target)
48
- if (idx !== -1) timers.splice(idx, 1)
49
- },
50
- })
51
-
52
- const advance = (ms: number): void => {
53
- now += ms
54
- for (;;) {
55
- timers.sort((a, b) => a.fireAt - b.fireAt)
56
- const next = timers[0]
57
- if (!next || next.fireAt > now) break
58
- if (next.repeat != null) {
59
- next.fireAt += next.repeat
60
- next.fn()
61
- } else {
62
- timers.shift()
63
- next.fn()
64
- }
65
- }
66
- }
67
-
68
- return { driver, emits, advance }
69
- }
70
-
71
- function enqueue(chatId: string, text = 'hi'): SessionEvent {
72
- return {
73
- kind: 'enqueue',
74
- chatId,
75
- messageId: String(nextMsgId++),
76
- threadId: null,
77
- rawContent: `<channel chat_id="${chatId}">${text}</channel>`,
78
- }
79
- }
80
-
81
- describe('cross-turn sub-agent visibility (#334)', () => {
82
- it('Test 1: closeZombie on turn-1 force-close removes sub-agent from registry (fix #399)', () => {
83
- // When turn 2 starts while turn 1 has a pending background sub-agent,
84
- // the ingest enqueue path calls closeZombie on turn 1's card. closeZombie
85
- // explicitly abandons all running sub-agents (marks them done for display),
86
- // and — after fix #399 — also removes them from chatRunningSubagents.
87
- // Therefore turn 2 starts clean (no carry-over of abandoned agents).
88
- const { driver } = harness()
89
-
90
- // Turn 1: dispatch a background sub-agent, then turn ends.
91
- driver.ingest(enqueue('c1'), null)
92
- driver.ingest({ kind: 'sub_agent_started', agentId: 'bg-agent', firstPromptText: 'do work' }, 'c1')
93
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
94
-
95
- // Turn 2 starts — triggers closeZombie on turn 1 → removes bg-agent from registry.
96
- driver.startTurn({ chatId: 'c1', userText: 'new prompt' })
97
-
98
- const turn2State = driver.peek('c1', undefined)
99
- expect(turn2State).toBeDefined()
100
- // bg-agent was abandoned by closeZombie; it must NOT carry over into turn 2.
101
- expect(turn2State!.subAgents.has('bg-agent')).toBe(false)
102
- })
103
-
104
- it('Test 2: sub-agent finishing naturally before new turn does not appear on turn 2', () => {
105
- // When a sub-agent finishes via sub_agent_turn_end (natural completion),
106
- // it is removed from chatRunningSubagents by the ingest sync (fix #399
107
- // also keeps this path correct). Turn 2 starts clean.
108
- const { driver } = harness()
109
-
110
- // Turn 1: dispatch background sub-agent.
111
- driver.ingest(enqueue('c1'), null)
112
- driver.ingest({ kind: 'sub_agent_started', agentId: 'bg-agent', firstPromptText: 'do work' }, 'c1')
113
- // Sub-agent finishes naturally before turn 2 starts.
114
- driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'bg-agent', durationMs: 5000 }, 'c1')
115
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
116
-
117
- // Turn 2 starts.
118
- driver.startTurn({ chatId: 'c1', userText: 'next prompt' })
119
-
120
- const turn2State = driver.peek('c1', undefined)
121
- expect(turn2State).toBeDefined()
122
- // bg-agent finished before turn 2 — must NOT appear.
123
- expect(turn2State!.subAgents.has('bg-agent')).toBe(false)
124
- })
125
-
126
- it('Test 3: foreground sub-agent (completes mid-turn 1) does NOT appear on turn 2', () => {
127
- const { driver } = harness()
128
-
129
- // Turn 1: foreground sub-agent — starts and finishes before turn ends.
130
- driver.ingest(enqueue('c1'), null)
131
- driver.ingest({ kind: 'sub_agent_started', agentId: 'fg-agent', firstPromptText: 'quick task' }, 'c1')
132
- driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'fg-agent', durationMs: 200 }, 'c1')
133
- driver.ingest({ kind: 'turn_end', durationMs: 800 }, 'c1')
134
-
135
- // Turn 2 starts.
136
- driver.startTurn({ chatId: 'c1', userText: 'next prompt' })
137
-
138
- const turn2State = driver.peek('c1', undefined)
139
- expect(turn2State).toBeDefined()
140
- // Foreground sub-agent completed in turn 1 — must NOT bleed into turn 2.
141
- expect(turn2State!.subAgents.has('fg-agent')).toBe(false)
142
- })
143
-
144
- it('multiple background sub-agents: closeZombie removes all from registry (fix #399)', () => {
145
- // When closeZombie abandons all running sub-agents, they are all removed
146
- // from chatRunningSubagents. Turn 2 starts with an empty sub-agent map.
147
- const { driver } = harness()
148
-
149
- driver.ingest(enqueue('c1'), null)
150
- driver.ingest({ kind: 'sub_agent_started', agentId: 'bg1', firstPromptText: 'task 1' }, 'c1')
151
- driver.ingest({ kind: 'sub_agent_started', agentId: 'bg2', firstPromptText: 'task 2' }, 'c1')
152
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
153
-
154
- // New turn triggers closeZombie → all running agents marked done → removed from registry.
155
- driver.startTurn({ chatId: 'c1', userText: 'turn 2' })
156
-
157
- const state = driver.peek('c1', undefined)
158
- // Both abandoned agents must NOT carry over.
159
- expect(state!.subAgents.has('bg1')).toBe(false)
160
- expect(state!.subAgents.has('bg2')).toBe(false)
161
- })
162
-
163
- it('different chats do not cross-contaminate', () => {
164
- const { driver } = harness()
165
-
166
- // Chat A has a background sub-agent.
167
- driver.ingest(enqueue('chatA'), null)
168
- driver.ingest({ kind: 'sub_agent_started', agentId: 'agentA', firstPromptText: 'A' }, 'chatA')
169
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'chatA')
170
-
171
- // Chat B starts a new turn (no sub-agents in chat B).
172
- driver.startTurn({ chatId: 'chatB', userText: 'hello' })
173
-
174
- const stateB = driver.peek('chatB', undefined)
175
- expect(stateB!.subAgents.has('agentA')).toBe(false)
176
- expect(stateB!.subAgents.size).toBe(0)
177
- })
178
-
179
- it('cold-jsonl-synth path syncs registry: turn 2 does NOT inherit cold-synth-terminated agent (fix #399)', () => {
180
- // Forensic case from the live klanker bug: sub-agent ada7c3d07c28158f5
181
- // hit its turn limit mid-tool-call and never wrote system.turn_duration.
182
- // The cold-jsonl-synth heartbeat path (Gap 4 #313) marks it done
183
- // synthetically. BEFORE fix #399 the registry was never synced from
184
- // this path, so the agent appeared as a phantom on every subsequent
185
- // turn's card. AFTER fix #399 the registry is synced and turn 2 is clean.
186
- const { driver, advance } = harness(0, { coldSubAgentThresholdMs: 30_000, heartbeatMs: 5_000 })
187
-
188
- // Turn 1: dispatch background sub-agent, parent turn ends → pendingCompletion=true
189
- driver.ingest(enqueue('c1'), null)
190
- driver.ingest({ kind: 'sub_agent_started', agentId: 'cold-agent', firstPromptText: 'long task' }, 'c1')
191
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
192
-
193
- // Sub-agent goes cold (no events for > coldSubAgentThresholdMs).
194
- // Heartbeat ticks fire repeatedly; once lastEventAt is older than the
195
- // threshold, the cold-jsonl-synth path runs and synthesises sub_agent_turn_end.
196
- advance(35_000)
197
-
198
- // Turn 2 starts in the same chat. WITHOUT #399's fix, cold-agent would
199
- // re-seed into turn 2's PerChatState.subAgents (the bug). WITH the fix,
200
- // syncChatRunningSubagents fired from the cold-synth path and removed it.
201
- driver.startTurn({ chatId: 'c1', userText: 'turn 2' })
202
-
203
- const turn2State = driver.peek('c1', undefined)
204
- expect(turn2State).toBeDefined()
205
- expect(turn2State!.subAgents.has('cold-agent')).toBe(false)
206
- })
207
-
208
- it('counter-test: still-running background sub-agent DOES carry over (preserves #334)', () => {
209
- // The carry-over feature from #334 must continue to work for legitimate
210
- // still-running sub-agents. If syncChatRunningSubagents over-removes,
211
- // this test catches the regression. Asserts:
212
- // 1. A bg sub-agent that started in turn 1 and never went terminal
213
- // 2. After turn 2 starts (closeZombie fires on turn 1's card), the
214
- // sub-agent is correctly REMOVED (closeZombie marks it done)
215
- // Since closeZombie is the post-turn-1 cleanup path, "still running
216
- // across turns" actually means "running while turn 1 is in pendingCompletion
217
- // BEFORE turn 2 enqueues". The carry-over visibility happens during the
218
- // pendingCompletion window — verified here by peeking BEFORE turn 2.
219
- const { driver } = harness()
220
-
221
- driver.ingest(enqueue('c1'), null)
222
- driver.ingest({ kind: 'sub_agent_started', agentId: 'still-running', firstPromptText: 'long task' }, 'c1')
223
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
224
-
225
- // During pendingCompletion the sub-agent is visible on the card.
226
- const duringPending = driver.peek('c1', undefined)
227
- expect(duringPending).toBeDefined()
228
- expect(duringPending!.subAgents.has('still-running')).toBe(true)
229
- expect(duringPending!.subAgents.get('still-running')?.state).toBe('running')
230
- })
231
-
232
- it('sub-agent finishes naturally between turns: turn 3 starts clean', () => {
233
- // Verifies that a sub-agent finishing via sub_agent_turn_end (natural
234
- // completion via the ingest path) is removed from chatRunningSubagents
235
- // so subsequent turns do not see it.
236
- const { driver } = harness()
237
-
238
- // Turn 1: background sub-agent dispatched.
239
- driver.ingest(enqueue('c1'), null)
240
- driver.ingest({ kind: 'sub_agent_started', agentId: 'bg1', firstPromptText: 'shared?' }, 'c1')
241
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
242
-
243
- // Sub-agent finishes naturally BEFORE turn 2 starts.
244
- driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'bg1', durationMs: 3000 }, 'c1')
245
-
246
- // Turn 2 starts — bg1 already finished, so registry is empty.
247
- driver.startTurn({ chatId: 'c1', userText: 'turn 2' })
248
- expect(driver.peek('c1', undefined)!.subAgents.has('bg1')).toBe(false)
249
-
250
- driver.ingest({ kind: 'turn_end', durationMs: 1000 }, 'c1')
251
-
252
- // Turn 3: the finished sub-agent must NOT appear.
253
- driver.startTurn({ chatId: 'c1', userText: 'turn 3' })
254
- const stateT3 = driver.peek('c1', undefined)
255
- expect(stateT3).toBeDefined()
256
- expect(stateT3!.subAgents.has('bg1')).toBe(false)
257
- })
258
- })
@@ -1,160 +0,0 @@
1
- /**
2
- * #842 — first-render delay (45s default) with explicit-background bypass.
3
- *
4
- * Behavioural contract:
5
- * 1. Turn that ends BEFORE the threshold trips → no card emit at all.
6
- * 2. Turn that runs PAST the threshold → exactly one card emit at the
7
- * threshold, rendering the full buffered event stream (verified by
8
- * checking the rendered HTML reflects accumulated state).
9
- * 3. Explicit `Agent({ run_in_background: true })` dispatch with
10
- * `delay_ms_background=0` → card emits immediately on the
11
- * tool_use, regardless of the long `delay_ms` budget.
12
- * 4. Threshold timer is cleared on early turn_end (no late phantom
13
- * emit when wall-clock advances past the threshold afterwards).
14
- * 5. Pre-threshold buffer matches post-threshold render — i.e. the
15
- * first emit's HTML reflects every tool_use that landed during
16
- * the suppression window (no events lost).
17
- */
18
-
19
- import { describe, it, expect } from 'vitest'
20
- import { makeHarness, enqueue } from './_progress-card-harness.js'
21
- import type { SessionEvent } from '../session-tail.js'
22
-
23
- const tu = (
24
- toolName: string,
25
- toolUseId: string,
26
- input: Record<string, unknown> = {},
27
- ): SessionEvent => ({
28
- kind: 'tool_use',
29
- toolName,
30
- toolUseId,
31
- input,
32
- })
33
-
34
- const tr = (toolUseId: string): SessionEvent => ({
35
- kind: 'tool_result',
36
- toolUseId,
37
- isError: false,
38
- errorText: null,
39
- })
40
-
41
- describe('#842 progress-card first-render delay', () => {
42
- it('AC2 + AC6: turn ends BEFORE the 45s threshold → no card is ever posted', () => {
43
- const { driver, emits, advance } = makeHarness({ initialDelayMs: 45_000 })
44
- driver.ingest(enqueue('chat-fast'), null)
45
- advance(5_000)
46
- driver.ingest(tu('Read', 'tu1', { file_path: '/tmp/a.ts' }), 'chat-fast')
47
- advance(10_000)
48
- driver.ingest(tr('tu1'), 'chat-fast')
49
- advance(10_000)
50
- driver.ingest({ kind: 'turn_end' }, 'chat-fast')
51
- // Turn finished at t=25s — well before 45s. No card should have
52
- // been emitted, and no late phantom emit when we keep the clock
53
- // running.
54
- advance(60_000)
55
- expect(emits.length).toBe(0)
56
- })
57
-
58
- it('AC3 + AC6: turn that crosses 45s → one card emit at threshold, full backfill', () => {
59
- const { driver, emits, advance } = makeHarness({ initialDelayMs: 45_000 })
60
- driver.ingest(enqueue('chat-long'), null)
61
- // Buffer some events through the suppression window.
62
- advance(10_000)
63
- driver.ingest(tu('Read', 'tu1', { file_path: '/tmp/a.ts' }), 'chat-long')
64
- advance(10_000)
65
- driver.ingest(tr('tu1'), 'chat-long')
66
- advance(10_000)
67
- driver.ingest(tu('Bash', 'tu2', { description: 'check commits' }), 'chat-long')
68
- // No emits yet — still within the 45s window.
69
- expect(emits.length).toBe(0)
70
- // Cross the threshold.
71
- advance(20_000) // total elapsed ~50s
72
- // Exactly one initial emit at threshold, rendering the buffered
73
- // state. The first emit must reflect the tool_use accumulation
74
- // that happened during the suppression window — i.e. the renderer
75
- // saw the buffer.
76
- expect(emits.length).toBeGreaterThanOrEqual(1)
77
- const first = emits[0]
78
- expect(first.html.length).toBeGreaterThan(0)
79
- // Buffer included a Bash with a human description — render must
80
- // include the description text (non-trivial: proves the reducer
81
- // ate the events before the first flush). This is the
82
- // "pre-threshold buffer matches post-threshold render" assertion.
83
- expect(first.html).toContain('check commits')
84
- })
85
-
86
- it('AC4: explicit Agent({run_in_background:true}) bypasses the long delay', () => {
87
- const { driver, emits, advance } = makeHarness({
88
- initialDelayMs: 45_000,
89
- initialDelayMsBackground: 0,
90
- })
91
- driver.ingest(enqueue('chat-bg'), null)
92
- advance(2_000)
93
- driver.ingest(
94
- tu('Agent', 'tu-bg', {
95
- prompt: 'do bg work',
96
- description: 'bg-job',
97
- run_in_background: true,
98
- }),
99
- 'chat-bg',
100
- )
101
- // Card should emit immediately — no need to advance the clock.
102
- expect(emits.length).toBeGreaterThanOrEqual(1)
103
- expect(emits[0].html.length).toBeGreaterThan(0)
104
- })
105
-
106
- it('AC4 (foreground variant): non-background Agent does NOT bypass the delay', () => {
107
- const { driver, emits, advance } = makeHarness({
108
- initialDelayMs: 45_000,
109
- initialDelayMsBackground: 0,
110
- })
111
- driver.ingest(enqueue('chat-fg'), null)
112
- advance(2_000)
113
- driver.ingest(
114
- tu('Agent', 'tu-fg', { prompt: 'p', description: 'fg-job' }),
115
- 'chat-fg',
116
- )
117
- // No emit yet — foreground Agent should follow the 45s rule.
118
- // (`promoteOnSubAgent` only fires once `sub_agent_started` lands;
119
- // this test stops at the parent tool_use to isolate the
120
- // background-bypass branch.)
121
- expect(emits.length).toBe(0)
122
- })
123
-
124
- it('AC4 (with positive background budget): timer rescheduled to short budget', () => {
125
- const { driver, emits, advance } = makeHarness({
126
- initialDelayMs: 45_000,
127
- initialDelayMsBackground: 5_000,
128
- })
129
- driver.ingest(enqueue('chat-bg-short'), null)
130
- advance(1_000)
131
- driver.ingest(
132
- tu('Agent', 'tu-bg2', {
133
- prompt: 'p',
134
- description: 'bg',
135
- run_in_background: true,
136
- }),
137
- 'chat-bg-short',
138
- )
139
- // No immediate emit — budget is 5s.
140
- expect(emits.length).toBe(0)
141
- // Advance past 45s budget would emit, but we expect the
142
- // background bypass to fire by 5s elapsed.
143
- advance(5_000)
144
- expect(emits.length).toBeGreaterThanOrEqual(1)
145
- })
146
-
147
- it('AC5: timer cleared on early turn_end — no phantom emit when clock keeps running', () => {
148
- const { driver, emits, advance } = makeHarness({ initialDelayMs: 45_000 })
149
- driver.ingest(enqueue('chat-fast2'), null)
150
- advance(5_000)
151
- driver.ingest(tu('Read', 'tu1', { file_path: '/x' }), 'chat-fast2')
152
- advance(5_000)
153
- driver.ingest({ kind: 'turn_end' }, 'chat-fast2')
154
- expect(emits.length).toBe(0)
155
- // Push the clock far past the original threshold. If the timer
156
- // wasn't cleared, a phantom flush would land here.
157
- advance(120_000)
158
- expect(emits.length).toBe(0)
159
- })
160
- })