tumwater 0.1.3 → 0.2.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 (359) hide show
  1. package/README.md +63 -28
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-eligibility.js +332 -43
  26. package/dist/src/backlog/backlog-md.js +79 -17
  27. package/dist/src/backlog/backlog-render.js +62 -11
  28. package/dist/src/backlog/backlog-structure.js +261 -58
  29. package/dist/src/backlog/backlog-write.js +178 -441
  30. package/dist/src/backlog/backlog.js +113 -16
  31. package/dist/src/backlog/question-answer.js +339 -0
  32. package/dist/src/baseline/main-baseline.js +75 -88
  33. package/dist/src/baseline/main-red.js +32 -30
  34. package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
  35. package/dist/src/{brief.js → brief/brief.js} +33 -8
  36. package/dist/src/budget/budget.js +32 -15
  37. package/dist/src/budget/fallback-breaker.js +8 -7
  38. package/dist/src/build/build-check-detect.js +8 -7
  39. package/dist/src/build/build-check-events.js +68 -22
  40. package/dist/src/build/build-check-report.js +22 -23
  41. package/dist/src/build/build-check-scoped.js +51 -62
  42. package/dist/src/build/build-check.js +15 -14
  43. package/dist/src/build/build-info.js +2 -1
  44. package/dist/src/build/build-stage.js +23 -22
  45. package/dist/src/build/dep-install.js +22 -21
  46. package/dist/src/build/host-sleep.js +1 -1
  47. package/dist/src/change/change-data.js +13 -11
  48. package/dist/src/change/change-render.js +11 -10
  49. package/dist/src/cli/brief-command.js +9 -7
  50. package/dist/src/cli/cli-args.js +25 -5
  51. package/dist/src/cli/cli-command-args.js +43 -10
  52. package/dist/src/cli/cli-editor.js +13 -1
  53. package/dist/src/cli/cli-flag-specs.js +31 -6
  54. package/dist/src/cli/cli-marker-commands.js +13 -10
  55. package/dist/src/cli/cli-query-commands.js +439 -56
  56. package/dist/src/cli/cli-repo-gate.js +1 -1
  57. package/dist/src/cli/cli-run.js +124 -68
  58. package/dist/src/cli/completion.js +70 -12
  59. package/dist/src/cli/config-commands.js +14 -11
  60. package/dist/src/cli/help.js +264 -124
  61. package/dist/src/cli/log-commands.js +127 -21
  62. package/dist/src/cli/notify-command.js +33 -18
  63. package/dist/src/cli/question-commands.js +13 -224
  64. package/dist/src/cli.js +78 -36
  65. package/dist/src/collections.js +66 -16
  66. package/dist/src/concurrency/check-permit.js +16 -17
  67. package/dist/src/concurrency/lock.js +6 -5
  68. package/dist/src/concurrency/provider-lease.js +69 -0
  69. package/dist/src/concurrency/semaphore.js +258 -36
  70. package/dist/src/config/config-aliases.js +106 -0
  71. package/dist/src/config/config-editable-keys.js +3 -3
  72. package/dist/src/config/config-fallbacks.js +379 -0
  73. package/dist/src/config/config-field-checks.js +19 -10
  74. package/dist/src/config/config-keys.js +160 -0
  75. package/dist/src/config/config-live.js +64 -12
  76. package/dist/src/config/config-schema.js +34 -22
  77. package/dist/src/config/config-validation.js +132 -48
  78. package/dist/src/config/config-views.js +107 -325
  79. package/dist/src/config/config-write.js +138 -177
  80. package/dist/src/config/config.js +60 -49
  81. package/dist/src/config/model-selector.js +28 -2
  82. package/dist/src/doctor/doctor-backlog.js +494 -68
  83. package/dist/src/doctor/doctor-checks.js +509 -58
  84. package/dist/src/doctor/doctor-holds.js +139 -78
  85. package/dist/src/doctor/doctor-install.js +30 -0
  86. package/dist/src/doctor/doctor-launch-services.js +2 -2
  87. package/dist/src/doctor/doctor-model-checks.js +549 -157
  88. package/dist/src/doctor/doctor-notify.js +2 -3
  89. package/dist/src/doctor/doctor-orphans.js +10 -9
  90. package/dist/src/doctor/doctor-render.js +36 -9
  91. package/dist/src/doctor/doctor.js +76 -25
  92. package/dist/src/doctor/image-probe.js +244 -0
  93. package/dist/src/doctor/probe.js +64 -0
  94. package/dist/src/events/event-format.js +219 -106
  95. package/dist/src/events/event-labels.js +116 -0
  96. package/dist/src/events/event-legacy.js +140 -0
  97. package/dist/src/events/event-read.js +58 -22
  98. package/dist/src/events/event-window.js +27 -7
  99. package/dist/src/events/events.js +18 -14
  100. package/dist/src/events/notify.js +80 -30
  101. package/dist/src/failure/failure-cluster.js +12 -9
  102. package/dist/src/failure/failure-data.js +96 -48
  103. package/dist/src/failure/failure-render.js +69 -58
  104. package/dist/src/failure/failure-state-change.js +33 -26
  105. package/dist/src/failure/time-spend.js +79 -59
  106. package/dist/src/files/file-queue.js +16 -4
  107. package/dist/src/files/files.js +18 -6
  108. package/dist/src/files/json-object.js +18 -3
  109. package/dist/src/files/tail.js +1 -1
  110. package/dist/src/gates/bootstrap-gates.js +5 -4
  111. package/dist/src/gates/budget-gates.js +250 -34
  112. package/dist/src/gates/disk-gate.js +18 -18
  113. package/dist/src/gates/gate-polls.js +117 -69
  114. package/dist/src/gates/maintenance-quota.js +29 -29
  115. package/dist/src/gates/pause-gates.js +11 -11
  116. package/dist/src/gates/readiness.js +2 -1
  117. package/dist/src/gates/role-cap-gates.js +16 -21
  118. package/dist/src/gates/startup-gate.js +75 -31
  119. package/dist/src/gates/streak-gate.js +18 -16
  120. package/dist/src/git/commit-message.js +6 -5
  121. package/dist/src/git/git-diff.js +16 -16
  122. package/dist/src/git/git-run.js +8 -8
  123. package/dist/src/git/git.js +79 -59
  124. package/dist/src/git/slots-state.js +14 -5
  125. package/dist/src/git/worktree-pool.js +25 -18
  126. package/dist/src/git/worktree-use.js +15 -14
  127. package/dist/src/git/worktree.js +18 -18
  128. package/dist/src/gui/gui-args.js +10 -14
  129. package/dist/src/gui/gui-backlog-commands.js +273 -0
  130. package/dist/src/gui/gui-command.js +1 -1
  131. package/dist/src/gui/gui-config-commands.js +440 -0
  132. package/dist/src/gui/gui-endpoint-commands.js +16 -716
  133. package/dist/src/gui/gui-endpoint-common.js +49 -0
  134. package/dist/src/gui/gui-endpoints.js +105 -54
  135. package/dist/src/gui/gui-operator-commands.js +145 -0
  136. package/dist/src/gui/gui-prompt-commands.js +234 -0
  137. package/dist/src/gui/gui-server.js +25 -8
  138. package/dist/src/gui/gui-team-commands.js +164 -0
  139. package/dist/src/gui/http-body.js +17 -1
  140. package/dist/src/history/history-data.js +179 -104
  141. package/dist/src/history/history-render.js +76 -0
  142. package/dist/src/history/history.js +29 -70
  143. package/dist/src/inbox/inbox-attachments.js +66 -10
  144. package/dist/src/inbox/inbox-cancel.js +7 -5
  145. package/dist/src/inbox/inbox-edit.js +1 -1
  146. package/dist/src/inbox/inbox-submit.js +44 -4
  147. package/dist/src/inbox/inbox-top.js +83 -0
  148. package/dist/src/inbox/inbox.js +8 -2
  149. package/dist/src/inbox/pending-prompt.js +1 -1
  150. package/dist/src/inbox/prompt-commands.js +114 -73
  151. package/dist/src/init/init-templates.js +16 -14
  152. package/dist/src/init/init.js +12 -11
  153. package/dist/src/loop/leftover.js +28 -27
  154. package/dist/src/loop/loop-fallback.js +2 -2
  155. package/dist/src/loop/loop-pi.js +104 -50
  156. package/dist/src/loop/loop-recovery.js +15 -15
  157. package/dist/src/loop/loop-state.js +130 -13
  158. package/dist/src/loop/loop-tick.js +190 -88
  159. package/dist/src/loop/loop.js +68 -36
  160. package/dist/src/loop/model-fallback.js +3 -2
  161. package/dist/src/loop/revision.js +16 -7
  162. package/dist/src/merge/backlog-conflicts.js +155 -0
  163. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  164. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  165. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  166. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  167. package/dist/src/merge/merge-drain.js +212 -0
  168. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
  169. package/dist/src/merge/merge-pipeline.js +99 -0
  170. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  171. package/dist/src/merge/merge-queue-render.js +75 -0
  172. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  173. package/dist/src/merge/merge-slot.js +340 -0
  174. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  175. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  176. package/dist/src/merge/merge-verify.js +212 -0
  177. package/dist/src/notes-cli.js +97 -0
  178. package/dist/src/operator/operator-commands.js +113 -45
  179. package/dist/src/operator/operator-intent.js +74 -53
  180. package/dist/src/operator/operator-requests.js +52 -52
  181. package/dist/src/operator/retire.js +47 -28
  182. package/dist/src/operator/revert.js +71 -0
  183. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  184. package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
  185. package/dist/src/orchestrator/orchestrator.js +149 -96
  186. package/dist/src/orchestrator/retention.js +29 -12
  187. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  188. package/dist/src/orchestrator/startup-migrations.js +40 -0
  189. package/dist/src/paths.js +124 -38
  190. package/dist/src/pi/pi-args.js +26 -3
  191. package/dist/src/pi/pi-backend.js +15 -23
  192. package/dist/src/pi/pi-bin.js +18 -12
  193. package/dist/src/pi/pi-event-line.js +17 -1
  194. package/dist/src/pi/pi-failure.js +87 -0
  195. package/dist/src/pi/pi-models.js +126 -38
  196. package/dist/src/pi/pi-run-result.js +1 -1
  197. package/dist/src/pi/pi-settings.js +86 -0
  198. package/dist/src/pi/pi-stream.js +131 -107
  199. package/dist/src/pi/pi-watchdogs.js +4 -3
  200. package/dist/src/pi-extension/bounded-output.js +20 -5
  201. package/dist/src/pi-extension/context-budget.js +3 -1
  202. package/dist/src/pi-extension/context-shake.js +54 -21
  203. package/dist/src/pi-extension/context-window.js +37 -0
  204. package/dist/src/pi-extension/full-output.js +2 -2
  205. package/dist/src/pi-extension/strip-thinking.js +80 -0
  206. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  207. package/dist/src/process/launch-services.js +11 -10
  208. package/dist/src/process/process-table.js +1 -1
  209. package/dist/src/process/process.js +7 -7
  210. package/dist/src/process/run-marker.js +29 -6
  211. package/dist/src/process/supervisor.js +36 -25
  212. package/dist/src/prompt/dialect.js +30 -10
  213. package/dist/src/prompt/gate-prompts.js +170 -67
  214. package/dist/src/prompt/principles.js +1 -0
  215. package/dist/src/prompt/prompt-followup.js +29 -20
  216. package/dist/src/prompt/prompt.js +249 -91
  217. package/dist/src/redeploy/redeploy-policy.js +10 -10
  218. package/dist/src/redeploy/redeploy.js +6 -4
  219. package/dist/src/redeploy/redeployer.js +63 -55
  220. package/dist/src/redeploy/self-reload.js +1 -1
  221. package/dist/src/report/report-data.js +259 -254
  222. package/dist/src/report/report-fold.js +353 -0
  223. package/dist/src/report/report-render.js +235 -44
  224. package/dist/src/request-timeouts.js +1 -1
  225. package/dist/src/review/known-flakes.js +34 -14
  226. package/dist/src/review/protected-edits.js +4 -3
  227. package/dist/src/review/review-followup.js +16 -4
  228. package/dist/src/review/review-precheck.js +14 -13
  229. package/dist/src/review/review-verdict.js +14 -0
  230. package/dist/src/review/review.js +55 -34
  231. package/dist/src/review/secret-leaks.js +127 -0
  232. package/dist/src/review/suite-rerun.js +4 -3
  233. package/dist/src/roles/legacy-role-ids.js +21 -0
  234. package/dist/src/roles/legacy-role-migration.js +62 -0
  235. package/dist/src/roles/loop-ids.js +19 -2
  236. package/dist/src/roles/role-catalog.js +109 -131
  237. package/dist/src/roles/role-guidance.js +64 -31
  238. package/dist/src/roles/role-labels.js +10 -0
  239. package/dist/src/roles/role-render.js +194 -18
  240. package/dist/src/roles/role-view.js +216 -17
  241. package/dist/src/roles/roles.js +16 -14
  242. package/dist/src/scheduling/backoff.js +29 -19
  243. package/dist/src/scheduling/claims.js +15 -8
  244. package/dist/src/scheduling/once-round.js +11 -11
  245. package/dist/src/scheduling/quiet-hours.js +25 -32
  246. package/dist/src/scheduling/scheduling.js +28 -25
  247. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  248. package/dist/src/status/status-budget.js +115 -0
  249. package/dist/src/status/status-data.js +72 -184
  250. package/dist/src/status/status-inbox.js +47 -0
  251. package/dist/src/status/status-install.js +27 -0
  252. package/dist/src/status/status-polls.js +50 -39
  253. package/dist/src/status/status-roles.js +105 -0
  254. package/dist/src/{fleet → team}/error-storm.js +10 -10
  255. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  256. package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
  257. package/dist/src/{fleet → team}/reclaim.js +5 -4
  258. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  259. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  260. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
  261. package/dist/src/text/datetime.js +42 -2
  262. package/dist/src/text/format.js +10 -1
  263. package/dist/src/text/markdown.js +1 -1
  264. package/dist/src/text/phrases.js +35 -34
  265. package/dist/src/text/suggest.js +3 -2
  266. package/dist/src/text/text.js +2 -2
  267. package/dist/src/tick/qa-coverage.js +2 -1
  268. package/dist/src/tick/stage-check.js +142 -25
  269. package/dist/src/tick/telemetry-digest.js +10 -9
  270. package/dist/src/tick/tick-apply.js +101 -72
  271. package/dist/src/tick/tick-detail-data.js +23 -22
  272. package/dist/src/tick/tick-detail.js +37 -24
  273. package/dist/src/tick/tick-finalize.js +29 -22
  274. package/dist/src/tick/tick-outcome.js +2 -1
  275. package/dist/src/tick/tick-prompt.js +94 -26
  276. package/dist/src/tick/tick-resume.js +29 -13
  277. package/dist/src/tick/tick-stage.js +67 -53
  278. package/dist/src/tick/tick-timing.js +60 -59
  279. package/dist/src/tick/tick-usage.js +53 -23
  280. package/dist/src/tick/tick-verdict.js +75 -10
  281. package/dist/src/ui/badges.js +113 -69
  282. package/dist/src/ui/gui/gui-client-boot.js +202 -29
  283. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  284. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  285. package/dist/src/ui/gui/gui-client-history.js +94 -37
  286. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  287. package/dist/src/ui/gui/gui-client-loops.js +90 -47
  288. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  289. package/dist/src/ui/gui/gui-client-model.js +60 -43
  290. package/dist/src/ui/gui/gui-client-operator.js +73 -43
  291. package/dist/src/ui/gui/gui-client-pending.js +34 -31
  292. package/dist/src/ui/gui/gui-client-report.js +330 -47
  293. package/dist/src/ui/gui/gui-client-settings.js +126 -18
  294. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  295. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
  296. package/dist/src/ui/gui/gui-client.js +26 -20
  297. package/dist/src/ui/gui/gui-page.js +18 -19
  298. package/dist/src/ui/gui/gui-styles.js +12 -4
  299. package/dist/src/ui/progress-data.js +5 -3
  300. package/dist/src/ui/status-model.js +176 -102
  301. package/dist/src/ui/status-payload.js +114 -36
  302. package/dist/src/ui/status-render.js +121 -52
  303. package/dist/src/ui/team-alerts.js +341 -0
  304. package/dist/src/ui/tick-progress-model.js +14 -14
  305. package/dist/src/ui/tone.js +9 -8
  306. package/dist/src/ui/transcript-tail.js +1 -0
  307. package/dist/src/ui/transcript.js +23 -16
  308. package/dist/src/ui/tui/tui-app.js +1 -0
  309. package/dist/src/ui/tui/tui-backlog.js +85 -11
  310. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  311. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  312. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  313. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  314. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  315. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  316. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  317. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  318. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  319. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  320. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  321. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  322. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  323. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  324. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  325. package/dist/src/ui/tui/tui-editors.js +152 -0
  326. package/dist/src/ui/tui/tui-frame.js +61 -57
  327. package/dist/src/ui/tui/tui-history.js +12 -7
  328. package/dist/src/ui/tui/tui-input.js +44 -50
  329. package/dist/src/ui/tui/tui-keymap.js +1 -0
  330. package/dist/src/ui/tui/tui-keys.js +87 -1821
  331. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  332. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  333. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  334. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  335. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  336. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  337. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  338. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  339. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  340. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  341. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  342. package/dist/src/ui/tui/tui-pane.js +52 -28
  343. package/dist/src/ui/tui/tui-panes.js +294 -0
  344. package/dist/src/ui/tui/tui.js +44 -26
  345. package/dist/src/verdict/fix-claim.js +56 -33
  346. package/dist/src/verdict/no-change.js +19 -0
  347. package/dist/src/verdict/refusal.js +43 -11
  348. package/dist/src/verdict/reply-contract.js +17 -5
  349. package/dist/src/version.js +3 -2
  350. package/package.json +4 -2
  351. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  352. package/dist/src/landing/backlog-conflicts.js +0 -116
  353. package/dist/src/landing/landing-drain.js +0 -204
  354. package/dist/src/landing/landing-pipeline.js +0 -96
  355. package/dist/src/landing/landing-queue-render.js +0 -56
  356. package/dist/src/landing/landing-slot.js +0 -256
  357. package/dist/src/landing/landing-vetting.js +0 -196
  358. package/dist/src/pi-extension/role-notes.js +0 -40
  359. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -0,0 +1,116 @@
1
+ // vocabulary: plain
2
+ /** Words for the two code families the dashboards show: event types (the activity chips) and run
3
+ * result codes (the result cells). Pure, no I/O, so core modules and the UI may both import it. */
4
+ /** The words for each event type. The `Record` type makes the compiler demand a label for every
5
+ * type in the union, so a new event type cannot ship without one; renaming a type renames its key
6
+ * here and leaves the label unchanged. A label is a lowercase phrase of a few words, never the
7
+ * code itself. */
8
+ export const EVENT_TYPE_LABELS = {
9
+ run_start: "run started",
10
+ run_end: "run finished",
11
+ merge_queued: "waiting to merge",
12
+ merge_done: "merge finished",
13
+ merge_failed: "merge failed",
14
+ merged: "merged",
15
+ question_posted: "question posted",
16
+ entry_refused: "plan refused",
17
+ wake: "woke up",
18
+ run_deferred: "run deferred",
19
+ orchestrator_start: "tumwater started",
20
+ orchestrator_stop: "tumwater stopped",
21
+ prompt_enqueued: "message queued",
22
+ prompt_cancelled: "message cancelled",
23
+ prompt_edited: "message edited",
24
+ prompt_topped: "message moved to front",
25
+ prompt_taken: "message picked up",
26
+ counters_reset: "counters reset",
27
+ run_aborted: "run stopped by you",
28
+ resume: "run resumed",
29
+ review_start: "review started",
30
+ review_verdict: "review approved",
31
+ review_rejected: "review rejected",
32
+ review_failed: "review failed",
33
+ revision: "revision round",
34
+ conflict_handback: "sent back to resolve",
35
+ build_check: "check ran",
36
+ dep_install: "dependencies installed",
37
+ budget_warning: "budget warning",
38
+ budget_paused: "budget paused",
39
+ budget_fallback: "over-cap model",
40
+ budget_resumed: "budget resumed",
41
+ budget_handback: "returned to main model",
42
+ fallback_switched: "fallback switched",
43
+ team_paused: "team paused",
44
+ team_resumed: "team resumed",
45
+ state_migrated: "state migrated",
46
+ role_paused: "role paused",
47
+ role_resumed: "role resumed",
48
+ role_streak_paused: "repeated-failure pause",
49
+ role_cap_warning: "role budget warning",
50
+ role_cap_paused: "role budget paused",
51
+ role_cap_resumed: "role budget resumed",
52
+ main_red_hold: "main's check is failing",
53
+ maintenance_quota_hold: "waiting: maintenance allowance used",
54
+ maintenance_quota_resumed: "maintenance allowance reopened",
55
+ bootstrap_complete: "bootstrap complete",
56
+ quiet_hours_started: "quiet hours started",
57
+ quiet_hours_ended: "quiet hours ended",
58
+ disk_low: "waiting: low disk",
59
+ disk_ok: "disk space restored",
60
+ disk_reclaim: "disk space reclaimed",
61
+ claim: "entry claimed",
62
+ permit_reserve: "permits reserved",
63
+ slot_wait: "waited to start",
64
+ provider_wait: "waited for a provider slot",
65
+ rate_limit_hold: "waiting: provider limit",
66
+ rate_limit_resumed: "provider limit lifted",
67
+ claude_window_reserved: "claude usage window reserved",
68
+ claude_window_warning: "claude usage window warning",
69
+ claude_window_limited: "waiting: claude usage window",
70
+ claude_window_reset: "claude usage window reset",
71
+ max_concurrent_changed: "concurrency changed",
72
+ provider_concurrency_changed: "provider concurrency changed",
73
+ retention_changed: "retention changed",
74
+ config_changed: "settings changed",
75
+ model_changed: "model changed",
76
+ backend_binary: "backend checked",
77
+ model_fallback_started: "fallback model started",
78
+ model_fallback_ended: "fallback model ended",
79
+ opencode_free_model_billed: "free model billed",
80
+ image_blind: "image not seen",
81
+ image_reroute: "image sent to another model",
82
+ build_stale: "update available",
83
+ restart_pending: "update waits for idle",
84
+ restart: "restarted onto update",
85
+ restart_refused: "update refused",
86
+ restart_blocked: "update blocked",
87
+ restart_forced: "update forced",
88
+ supervisor_exit: "tumwater exited",
89
+ warning: "warning",
90
+ };
91
+ /** The words for each run result code, shown in the result cells. */
92
+ export const RUN_RESULT_LABELS = {
93
+ changed: "merged",
94
+ queued: "waiting to merge",
95
+ no_change: "no change",
96
+ skipped: "skipped",
97
+ refused: "refused",
98
+ rejected: "rejected in review",
99
+ review_error: "review failed",
100
+ merge_conflict: "merge conflict",
101
+ merge_blocked: "merge blocked",
102
+ error: "error",
103
+ aborted: "interrupted",
104
+ quiet_killed: "stalled, resumed",
105
+ user_aborted: "stopped by you",
106
+ main_red: "main's check is failing",
107
+ };
108
+ /** The words for an event type or run result code: its own-property label, else the code with
109
+ * underscores as spaces. The two code families are disjoint, so one lookup serves both. */
110
+ export function eventTypeLabel(code) {
111
+ if (Object.hasOwn(EVENT_TYPE_LABELS, code))
112
+ return EVENT_TYPE_LABELS[code];
113
+ if (Object.hasOwn(RUN_RESULT_LABELS, code))
114
+ return RUN_RESULT_LABELS[code];
115
+ return code.replace(/_/g, " ");
116
+ }
@@ -0,0 +1,140 @@
1
+ import { TUMWATER_SOURCE } from "./events.js";
2
+ import { canonicalRoleId } from "../roles/legacy-role-ids.js";
3
+ /** Old event lines read under today's names. `.tumwater/log/events.jsonl` (and its rotated
4
+ * `events.jsonl.1`) outlive the build that wrote them, and a long-lived supervisor on an older
5
+ * build keeps writing old names, so every reader normalizes on read: `normalizeLegacyEvent` is
6
+ * applied by `parseEventLine` (which every file reader goes through) and by `logEvent` (so
7
+ * in-process subscribers see exactly what readers see). Its rewrites are permanent.
8
+ *
9
+ * Role ids (`dry`, `telemetry`) are deliberately NOT mapped here. The role-id rename aliases an
10
+ * event's `source` id through its own legacy-id table when it switches each id, so mapping them
11
+ * here would show `dedupe` while the role is still `dry`. */
12
+ /** An old event type name → today's name. */
13
+ const LEGACY_EVENT_TYPES = {
14
+ tick_start: "run_start", // vocabulary-ok: legacy event name read from old logs
15
+ tick_end: "run_end", // vocabulary-ok: legacy event name read from old logs
16
+ tick_deferred: "run_deferred", // vocabulary-ok: legacy event name read from old logs
17
+ tick_aborted: "run_aborted", // vocabulary-ok: legacy event name read from old logs
18
+ // retired-name-ok:start old event names
19
+ fleet_paused: "team_paused", // vocabulary-ok: legacy event name read from old logs
20
+ fleet_resumed: "team_resumed", // vocabulary-ok: legacy event name read from old logs
21
+ // retired-name-ok:end
22
+ // retired-name-ok:start old event names
23
+ land_queued: "merge_queued", // vocabulary-ok: legacy event name read from old logs
24
+ landed: "merge_done", // vocabulary-ok: legacy event name read from old logs
25
+ land_failed: "merge_failed", // vocabulary-ok: legacy event name read from old logs
26
+ // retired-name-ok:end
27
+ };
28
+ /** The ordered field rewrites; later parts of the series append here, and they run in order. */
29
+ const LEGACY_EVENT_REWRITES = [
30
+ {
31
+ // The run number rode `tick` on run_start, run_end, prompt_taken, image_blind, image_reroute.
32
+ name: "tick-to-run",
33
+ apply(ev) {
34
+ const e = ev;
35
+ if ("tick" in e && !("run" in e)) { // vocabulary-ok: legacy field name read from old logs
36
+ e.run = e.tick; // vocabulary-ok: legacy field name read from old logs
37
+ delete e.tick; // vocabulary-ok: legacy field name read from old logs
38
+ }
39
+ },
40
+ },
41
+ {
42
+ // build_check.scope and conflict_handback.reason said "landing" for what is now "merge".
43
+ name: "landing-to-merge",
44
+ apply(ev) {
45
+ const e = ev;
46
+ if (e.type === "build_check" && e.scope === "landing")
47
+ e.scope = "merge"; // vocabulary-ok: legacy event value read from old logs
48
+ if (e.type === "conflict_handback" && e.reason === "landing")
49
+ e.reason = "merge"; // vocabulary-ok: legacy event value read from old logs
50
+ },
51
+ },
52
+ {
53
+ // slot_wait.purpose said "tick"/"vet" for what is now "run"/"verify".
54
+ name: "slot-purpose",
55
+ apply(ev) {
56
+ const e = ev;
57
+ if (e.type !== "slot_wait")
58
+ return;
59
+ if (e.purpose === "tick")
60
+ e.purpose = "run"; // vocabulary-ok: legacy event value read from old logs
61
+ if (e.purpose === "vet")
62
+ e.purpose = "verify"; // vocabulary-ok: legacy event value read from old logs
63
+ },
64
+ },
65
+ {
66
+ // The restart event's drain fields became finish fields.
67
+ name: "restart-drain-to-finish",
68
+ apply(ev) {
69
+ const e = ev;
70
+ if (e.type !== "restart")
71
+ return;
72
+ const renames = [
73
+ ["drainedMs", "finishedMs"], // vocabulary-ok: legacy field name read from old logs
74
+ ["abortedTicks", "abortedRuns"], // vocabulary-ok: legacy field name read from old logs
75
+ ["drainWindowMs", "finishWindowMs"], // vocabulary-ok: legacy field name read from old logs
76
+ ];
77
+ for (const [from, to] of renames) {
78
+ if (from in e && !(to in e)) {
79
+ e[to] = e[from];
80
+ delete e[from];
81
+ }
82
+ }
83
+ },
84
+ },
85
+ {
86
+ // The writer's id rode `loop`, with "harness" for tumwater's own events. A staged-claim
87
+ // `claim` line also carried a payload `source: "staged"`, which moves to `origin` first. A
88
+ // call site that still passes `source: "harness"` (the warn helpers' callers) maps too.
89
+ // Kept permanently though no TypeScript type accepts `loop` any more: old logs and a
90
+ // still-running old supervisor keep writing it, so the read path and logEvent need this.
91
+ name: "loop-to-source",
92
+ apply(ev) {
93
+ const e = ev;
94
+ if (typeof e.loop === "string") { // vocabulary-ok: legacy field name read from old logs
95
+ if (e.type === "claim" && e.source === "staged" && !("origin" in e)) {
96
+ e.origin = e.source;
97
+ delete e.source;
98
+ }
99
+ e.source = e.loop; // vocabulary-ok: legacy field name read from old logs
100
+ delete e.loop; // vocabulary-ok: legacy field name read from old logs
101
+ }
102
+ if (e.source === "harness")
103
+ e.source = TUMWATER_SOURCE; // vocabulary-ok: legacy event source read from old logs
104
+ },
105
+ },
106
+ {
107
+ // A retired role id on an old line reads as the role's current id. The table is empty
108
+ // until a switch part adds a row, so this changes nothing yet.
109
+ name: "legacy-role-ids",
110
+ apply(ev) {
111
+ const e = ev;
112
+ if (typeof e.source === "string")
113
+ e.source = canonicalRoleId(e.source);
114
+ if (typeof e.role === "string")
115
+ e.role = canonicalRoleId(e.role);
116
+ },
117
+ },
118
+ ];
119
+ /** Today's name for an old event type name; any other string comes back unchanged. */
120
+ export function canonicalEventType(name) {
121
+ return Object.prototype.hasOwnProperty.call(LEGACY_EVENT_TYPES, name) ? LEGACY_EVENT_TYPES[name] : name;
122
+ }
123
+ /** Rewrite a freshly parsed event line in place to today's names and return it. Never throws; a
124
+ * current line comes back untouched. */
125
+ export function normalizeLegacyEvent(ev) {
126
+ if (typeof ev.type === "string") {
127
+ const next = canonicalEventType(ev.type);
128
+ if (next !== ev.type)
129
+ ev.type = next;
130
+ }
131
+ for (const rewrite of LEGACY_EVENT_REWRITES) {
132
+ try {
133
+ rewrite.apply(ev);
134
+ }
135
+ catch {
136
+ // A malformed field is left as written: normalizing must never lose the line.
137
+ }
138
+ }
139
+ return ev;
140
+ }
@@ -3,6 +3,7 @@ import { finiteNumber, parseJsonObject } from "../files/json-object.js";
3
3
  import { eventsLogPath } from "../paths.js";
4
4
  import { cachedByStat } from "../files/stat-cache.js";
5
5
  import { readTailTextWithEnd } from "../files/tail.js";
6
+ import { normalizeLegacyEvent } from "./event-legacy.js";
6
7
  /** The READ side of the event feed: everything that consumes events.jsonl after the fact —
7
8
  * parseEventLine's skip-without-failing rule, the per-event query conventions (eventDayKey,
8
9
  * eventRole, eventUsage, tickStartMap/tickSpanMs) the reports and history share, and the
@@ -20,10 +21,11 @@ import { readTailTextWithEnd } from "../files/tail.js";
20
21
  * object either: the log is one JSON object per line, so anything else is corrupt (or foreign)
21
22
  * and reads as no data — the same object check readJsonFile applies to the harness's state
22
23
  * files. Without it, `readEvents` pushes the truthy non-object into the feed and `formatEvent`
23
- * renders it as an `Invalid Date undefined` line. */
24
+ * renders it as an `Invalid Date undefined` line. An old line comes back under today's names
25
+ * (normalizeLegacyEvent, event-legacy.ts). */
24
26
  export function parseEventLine(line) {
25
27
  const parsed = parseJsonObject(line);
26
- return parsed ? parsed : null;
28
+ return parsed ? normalizeLegacyEvent(parsed) : null;
27
29
  }
28
30
  /** The local calendar-day key ("YYYY-MM-DD") of an event's `ts`, or null when it carries no
29
31
  * numeric timestamp. The one day-bucketing rule the windowed reader (event-window.ts) and the
@@ -40,19 +42,19 @@ export function eventDayKey(ev) {
40
42
  /** The loop id an event is filed under, or "?" when absent or empty — the guard the usage
41
43
  * report and the failure digest both apply when grouping by role. */
42
44
  export function eventRole(ev) {
43
- return typeof ev.loop === "string" && ev.loop !== "" ? ev.loop : "?";
45
+ return typeof ev.source === "string" && ev.source !== "" ? ev.source : "?";
44
46
  }
45
47
  /** The tick number an event names, or 0 when it is absent or non-finite. The one guard every
46
48
  * display surface applies to the loosened `tick` field: an event log is read without a shape
47
49
  * filter, so a torn or hand-edited line can carry no tick (String(undefined) printed "#undefined"
48
50
  * in the feed) or a non-numeric one (String(Number(…)) printed "#NaN" in history). Ticks are
49
- * 1-based — the loop increments state.ticks before logging its first tick — so 0 is visibly not
51
+ * 1-based — the loop increments state.runs before logging its first tick — so 0 is visibly not
50
52
  * a real tick, the same corrupt-value-reads-as-0 rule gigabytes and the failure digest's
51
53
  * aheadCommits apply, kept finite so a JSON consumer never sees NaN's null stand-in. */
52
54
  export function eventTick(ev) {
53
- return finiteNumber(ev.tick, 0);
55
+ return finiteNumber(ev.run, 0);
54
56
  }
55
- /** The usage numbers an event records (`tick_end`'s own run, `landed`'s landing slot):
57
+ /** The usage numbers an event records (`run_end`'s own run, `merge_done`'s merge slot):
56
58
  * `tokens` and `costUsd` arrive `unknown` through the event's index signature, so they are
57
59
  * coerced to numbers here, with an absent, non-numeric, or non-finite value reading as 0 —
58
60
  * the one convention every usage consumer applies (the event feed's usage fragment,
@@ -66,27 +68,61 @@ export function eventUsage(ev) {
66
68
  const notionalCostUsd = finiteNumber(ev.notionalCostUsd, 0);
67
69
  return { tokens, costUsd, notionalCostUsd };
68
70
  }
69
- /** Pair the tick_start events of `events` by tick, keyed `${loop}#${tick}` → start epoch ms —
70
- * the one home of that pairing and its key format, shared by `tumwater history`'s rows
71
+ /** Pair the run_start events of `events` by tick, keyed `${loop}#${tick}` → the start event
72
+ * itself — the one home of that pairing and its key format, shared by the timestamps
73
+ * (tickStartMap), the models (tickStartModelMap), and the tick drill-down's duration
74
+ * (tick-detail-data.ts uses tickSpanMs' key). A run_start whose `ts` is not a finite number is
75
+ * as unusable as an absent one and adds no entry either (the same "a torn or hand-edited line
76
+ * carries no usable timestamp" policy eventTick/eventUsage apply to their fields), so every
77
+ * entry is a real start the projections below can trust. Private: external consumers read one
78
+ * of the projections, not this raw pairing. */
79
+ function runStartMap(events) {
80
+ const starts = new Map();
81
+ for (const e of events) {
82
+ const ts = finiteNumber(e.ts, null);
83
+ if (e.type === "run_start" && ts !== null)
84
+ starts.set(`${e.source}#${e.run}`, e);
85
+ }
86
+ return starts;
87
+ }
88
+ /** The run_start pairing's start epoch ms: `${loop}#${tick}` → start epoch ms, the projection
89
+ * of runStartMap onto each event's `ts` — shared by `tumwater history`'s row spans
71
90
  * (history-data.ts), the failure digest's time-and-spend fold (time-spend.ts), and the tick
72
91
  * drill-down's duration (tick-detail-data.ts), so those consumers cannot drift into different
73
- * notions of a tick's span. A tick_end whose start is not in `events` (log rotation cut it, or
92
+ * notions of a tick's span. A run_end whose start is not in `events` (log rotation cut it, or
74
93
  * the tick was skipped before any start logged) has no entry; callers decide what an unpaired
75
- * end costs. A tick_start whose `ts` is not a finite number is as unusable as an absent one and
76
- * adds no entry either (the same "a torn or hand-edited line carries no usable timestamp"
77
- * policy eventTick/eventUsage apply to their fields), so every value in the map is real epoch
78
- * ms and the span below cannot subtract from `undefined`. Lives in the read module, not in a
79
- * ui module, so core collectors can share it without a core→ui import. */
94
+ * end costs. Lives in the read module, not in a ui module, so core collectors can share it
95
+ * without a core→ui import. */
80
96
  export function tickStartMap(events) {
81
97
  const starts = new Map();
82
- for (const e of events) {
83
- const ts = finiteNumber(e.ts, null);
84
- if (e.type === "tick_start" && ts !== null)
85
- starts.set(`${e.loop}#${e.tick}`, ts);
86
- }
98
+ for (const [key, e] of runStartMap(events))
99
+ starts.set(key, finiteNumber(e.ts, 0));
87
100
  return starts;
88
101
  }
89
- /** The start→end span of the `tick_end` event `ev` against `starts` (tickStartMap's pairing):
102
+ /** The run_start pairing's model selectors: `${loop}#${tick}` → the run's model when its
103
+ * run_start carries a non-empty string `model` (the agent-qualified selector string — a
104
+ * per-role override, a fallback, or the primary — that the status rows carry; see
105
+ * config-views.ts's agentQualifiedSelector). A run_start with no `model`, or an empty or
106
+ * non-string one, adds no entry, so `tumwater history`'s model cell renders empty for a start
107
+ * that named none — exactly the empty-usage convention applied to cells. The finite-`ts`
108
+ * guard rides runStartMap, the one home of the pairing. */
109
+ export function tickStartModelMap(events) {
110
+ const models = new Map();
111
+ for (const [key, e] of runStartMap(events)) {
112
+ if (typeof e.model === "string" && e.model !== "")
113
+ models.set(key, e.model);
114
+ }
115
+ return models;
116
+ }
117
+ /** The model of the run_end event `ev` against `models` (tickStartModelMap's pairing): the
118
+ * `${loop}#${tick}` lookup, in one place beside the map it indexes, so the key format and the
119
+ * "no model is null" rule are spelled once — the model twin of tickSpanMs. null when the
120
+ * start is missing (rotation cut it) or carried no model; callers decide what an empty model
121
+ * costs — history renders it as an empty cell. */
122
+ export function tickStartModel(ev, models) {
123
+ return models.get(`${ev.source}#${ev.run}`) ?? null;
124
+ }
125
+ /** The start→end span of the `run_end` event `ev` against `starts` (tickStartMap's pairing):
90
126
  * the `${loop}#${tick}` lookup and the clamp to non-negative, in one place beside the map
91
127
  * they index, so the key format and the span rule are spelled once. null when the start is
92
128
  * missing (rotation cut it) or when `ev.ts` is not a finite number (a torn or hand-edited end
@@ -97,12 +133,12 @@ export function tickStartMap(events) {
97
133
  * (`tickSpanMs(ev, starts) ?? 0`) folded that NaN straight into its agent-hour totals —
98
134
  * `NaN ?? 0` is still NaN. */
99
135
  export function tickSpanMs(ev, starts) {
100
- const startTs = starts.get(`${ev.loop}#${ev.tick}`);
136
+ const startTs = starts.get(`${ev.source}#${ev.run}`);
101
137
  const endTs = finiteNumber(ev.ts, null);
102
138
  return startTs === undefined || endTs === null ? null : Math.max(0, endTs - startTs);
103
139
  }
104
140
  /** The event-tail size readEvents falls back to when a caller omits `limit` — a working
105
- * handful of a busy fleet's recent activity, small enough that the per-poll tail read stays
141
+ * handful of a busy team's recent activity, small enough that the per-poll tail read stays
106
142
  * cheap. Exported so a caller that starts a scan at the default (status-polls.ts's
107
143
  * mainCheckForPoll ladder) grows from the same value instead of re-spelling the literal. */
108
144
  export const DEFAULT_EVENT_TAIL = 200;
@@ -1,4 +1,4 @@
1
- import { dayKey, humanSeconds } from "../text/datetime.js";
1
+ import { dayAt, dayKey, humanSeconds } from "../text/datetime.js";
2
2
  import { eventDayKey, parseEventLine } from "./event-read.js";
3
3
  import { eventsArchivePath, eventsLogPath } from "../paths.js";
4
4
  import { statOrNull } from "../files/files.js";
@@ -23,16 +23,24 @@ export const LOGS_SINCE_MAX_MS = 7 * 24 * 60 * 60 * 1000;
23
23
  * signal, and the log rotates at 16 MB (EVENTS_MAX_BYTES in events.ts) anyway. Lives here so
24
24
  * the CLI's flag validation and the collector's window read share one bound. */
25
25
  export const REPORT_SINCE_MAX_MS = 7 * 24 * 60 * 60 * 1000;
26
+ /** Why a trailing-window length is refused, as the rejection text naming the out-of-range
27
+ * value (`sinceMs must be between 1 and 7d (got 604800000)`). The single home of the report
28
+ * window's bound wording, shared by the data-layer guard (assertReportSinceMs) and the
29
+ * dashboard's ?sinceMs= 400 (gui-endpoints.ts) so the two cannot drift apart; the `--since`
30
+ * CLI flag uses parseSinceFlag's own distinct wording. */
31
+ export function reportSinceMsg(sinceMs) {
32
+ return `sinceMs must be between 1 and ${humanSeconds(REPORT_SINCE_MAX_MS / 1000)} (got ${sinceMs})`;
33
+ }
26
34
  /** Reject a trailing-window length outside the report bound before a collector reads the log.
27
35
  * The CLI's parseSinceFlag already rejects one, but both trailing-window collectors are also
28
36
  * called directly (tests, future callers), so the data layer enforces the bound too. A
29
37
  * non-finite length is refused here as well: NaN slips past both comparisons (`NaN <= 0` and
30
38
  * `NaN > MAX` are each false) and would reach the window arithmetic, so the guard tests
31
- * finiteness first — the same rule the persisted-state readers apply. One home for the message
32
- * keeps the usage report and the failure digest from drifting apart. */
39
+ * finiteness first — the same rule the persisted-state readers apply. The message comes from
40
+ * reportSinceMsg so the data layer and the dashboard share the bound's wording. */
33
41
  export function assertReportSinceMs(sinceMs) {
34
42
  if (!Number.isFinite(sinceMs) || sinceMs <= 0 || sinceMs > REPORT_SINCE_MAX_MS)
35
- throw new Error(`sinceMs must be between 1 and ${humanSeconds(REPORT_SINCE_MAX_MS / 1000)} (got ${sinceMs})`);
43
+ throw new Error(reportSinceMsg(sinceMs));
36
44
  }
37
45
  /** The one-line note every windowed render prints when its read cannot prove the window was
38
46
  * fully covered (coversFullWindow false or eventWindowCovers false): the log's oldest retained
@@ -139,7 +147,7 @@ export function eventWindowCovers(w, cutoff) {
139
147
  * as the oldest complete line in hand predates the window — cost scales with the window's size,
140
148
  * not the file's. `coversFullWindow` records whether this file reaches back before the window
141
149
  * (see EventWindow's field doc), so the caller can tell "retention cut the window" from "the
142
- * fleet was idle": both leave the oldest returned event later than the window start, but only
150
+ * team was idle": both leave the oldest returned event later than the window start, but only
143
151
  * the former means data was lost. */
144
152
  function scanEventsFile(file, fromKey) {
145
153
  const st = statOrNull(file);
@@ -232,7 +240,7 @@ function scanFileReadState(file) {
232
240
  }
233
241
  /** The duration-shaped windowed read whose consumer needs a SECOND view of the same window
234
242
  * with a wider cutoff (history's tick-row join: a queued tick spanning the rows' cutoff logged
235
- * its land_queued pin before that cutoff, so its landing verdict must be joined from evidence
243
+ * its merge_queued pin before that cutoff, so its merge verdict must be joined from evidence
236
244
  * up to `joinBackslackMs` older). One backwards scan serves both: the wide read's day key never
237
245
  * sits after the narrow one's, so every `ts >= cutoff` event it holds is exactly the narrow
238
246
  * read's event list — the narrow scan reads a prefix of the wide scan's bytes and stops no
@@ -260,7 +268,7 @@ export function readEventsSinceJoined(root, sinceMs, joinBackslackMs) {
260
268
  // exactly ts < that midnight — so the checks below are numeric and the event list's
261
269
  // oldest-first order makes them one bounded pass instead of a day-key formatting per event.
262
270
  const cut = new Date(cutoff);
263
- const narrowDayStartMs = new Date(cut.getFullYear(), cut.getMonth(), cut.getDate()).getTime();
271
+ const narrowDayStartMs = dayAt(0, cut).getTime();
264
272
  // One pass up the oldest-first list: the oldest numerically-timestamped event (any line
265
273
  // older than the narrow day proves the narrow scan would have early-stopped on it) and
266
274
  // the first event inside the narrow day (the narrow window's oldest retained event, for
@@ -298,3 +306,15 @@ export function readEventsSince(root, sinceMs) {
298
306
  const events = window.events.filter((e) => typeof e.ts === "number" && e.ts >= cutoff);
299
307
  return { cutoff, events, covered: eventWindowCovers(window, cutoff) };
300
308
  }
309
+ /** The "recent events of a kind" read every doctor wait/failure check shares: events since the
310
+ * trailing `sinceMs` window, kept only when they clear `now − sinceMs` (the caller's seam for the
311
+ * window's far edge) and pass `keep`. One home of the read-and-recheck shape the slot/provider
312
+ * wait checks (doctor-checks.ts), the notify-failure check, and the queued-merge wait check each
313
+ * spelled beside readEventsSince's cutoff — the kind + recency rule cannot drift per caller. `keep`
314
+ * defaults to every event, so the ts-only reader passes nothing; `now` is explicit so every call in
315
+ * one window derives from the same instant (readEventsSince's own cutoff still uses the real
316
+ * clock, so this recheck is what a test seam can pin). `ts` is already number-typed by
317
+ * readEventsSince's filter, so the recheck never compares against a NaN. */
318
+ export function eventsSince(root, sinceMs, now, keep = () => true) {
319
+ return readEventsSince(root, sinceMs).events.filter((ev) => keep(ev) && ev.ts >= now - sinceMs);
320
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import { eventsLogPath } from "../paths.js";
3
3
  import { ensureParentDir, rotateIfLarge, sameStatIdentity, statOrNull, terminateTornTail } from "../files/files.js";
4
+ import { normalizeLegacyEvent } from "./event-legacy.js";
4
5
  import { errorMessage, stripTerminalControls } from "../text/text.js";
5
6
  const listeners = new Set();
6
7
  /** Get notified of every event logged in this process (e.g. to narrate `tumwater run`).
@@ -19,9 +20,12 @@ export const EVENTS_MAX_BYTES = 16 * 1024 * 1024;
19
20
  export function eventsRotationLabel() {
20
21
  return `rotated at ${EVENTS_MAX_BYTES / (1024 * 1024)} MB`;
21
22
  }
23
+ ;
24
+ /** The `source` of events tumwater writes about itself rather than on a role's behalf. */
25
+ export const TUMWATER_SOURCE = "tumwater";
22
26
  /** The usage fields an event carries, as a fragment with both omitted when zero — the
23
- * omit-when-zero convention the two usage-emitters apply (tick-finalize.ts's tick_end,
24
- * landing-slot.ts's landed/land_failed): a zero-token, zero-cost run (a free model, an abort
27
+ * omit-when-zero convention the two usage-emitters apply (tick-finalize.ts's run_end,
28
+ * merge-slot.ts's merge_done/merge_failed): a zero-token, zero-cost run (a free model, an abort
25
29
  * before any tokens) renders without usage, so event-format's usage fragment and
26
30
  * `tumwater history` show nothing rather than "$0.00". Spreading the fragment keeps the
27
31
  * fields absent rather than 0; readers (event-read.ts's eventUsage and the index-signature
@@ -32,7 +36,7 @@ export function usageFragment(tokens, costUsd, notionalCostUsd = 0) {
32
36
  ...(tokens > 0 ? { tokens } : {}),
33
37
  ...(costUsd > 0 ? { costUsd } : {}),
34
38
  // Subscription spend rides on the same fragment (plans/agent-backends.md part 6a): it is
35
- // omitted when zero like the other fields, so an API-only fleet's lines are unchanged.
39
+ // omitted when zero like the other fields, so an API-only team's lines are unchanged.
36
40
  ...(notionalCostUsd > 0 ? { notionalCostUsd } : {}),
37
41
  };
38
42
  }
@@ -44,7 +48,7 @@ export function usageFragment(tokens, costUsd, notionalCostUsd = 0) {
44
48
  * so it runs only when the file's shape differs from what this process's last append left —
45
49
  * a steady stream of events costs one stat, not an fd cycle, per event. */
46
50
  export function logEvent(root, event) {
47
- const full = { ts: Date.now(), ...event };
51
+ const full = normalizeLegacyEvent({ ts: Date.now(), ...event });
48
52
  const file = eventsLogPath(root);
49
53
  ensureParentDir(file);
50
54
  rotateIfLarge(file, EVENTS_MAX_BYTES);
@@ -54,10 +58,10 @@ export function logEvent(root, event) {
54
58
  return full;
55
59
  }
56
60
  /** Log a warning event: the harness's "something is off but the loop continues" signal. The
57
- * single home of the `{ loop, type: "warning", message }` shape every warn site constructs —
61
+ * single home of the `{ source, type: "warning", message }` shape every warn site constructs —
58
62
  * without it, each caller restates the event object and a field can drift between them. */
59
- export function warnEvent(root, loop, message) {
60
- return logEvent(root, { loop, type: "warning", message });
63
+ export function warnEvent(root, source, message) {
64
+ return logEvent(root, { source, type: "warning", message });
61
65
  }
62
66
  /** logEvent for a caller on a path that cannot handle a throw — the orchestrator's
63
67
  * catch-less poll loop and the gate family it runs every couple of seconds. An unwritable
@@ -71,7 +75,7 @@ export function logEventBestEffort(root, event) {
71
75
  // A terminal display boundary like cli-output's say()/fail(): the loop id, event type, and
72
76
  // error text can carry model- or subprocess-supplied bytes, and stderr is a terminal. Strip
73
77
  // the control characters the same way so a fallback line cannot drive the operator's shell.
74
- const detail = `${event.loop}: could not log ${event.type} event: ${errorMessage(err)}`;
78
+ const detail = `${event.source}: could not log ${event.type} event: ${errorMessage(err)}`;
75
79
  process.stderr.write(`tumwater: ${stripTerminalControls(detail)}\n`);
76
80
  }
77
81
  }
@@ -81,26 +85,26 @@ export function logEventBestEffort(root, event) {
81
85
  * caller's message (a spawn failure, a rejected tick) survives to be read. The single home of
82
86
  * the try-warn → message-bearing-stderr-fallback shape, so the notify hook and the launch pass
83
87
  * do not each carry a copy of it. */
84
- export function warnEventBestEffort(root, loop, message) {
88
+ export function warnEventBestEffort(root, source, message) {
85
89
  try {
86
- warnEvent(root, loop, message);
90
+ warnEvent(root, source, message);
87
91
  }
88
92
  catch (err) {
89
93
  // The warning's own text is very often model-supplied (a stalled tool call's label, a tick's
90
94
  // lastError) and the loop id can be a custom one, so this fallback is a terminal boundary:
91
95
  // sanitize exactly as say()/fail() do before the line reaches the operator's terminal.
92
- const detail = `${loop}: ${message} (warning log unwritable: ${errorMessage(err)})`;
96
+ const detail = `${source}: ${message} (warning log unwritable: ${errorMessage(err)})`;
93
97
  process.stderr.write(`tumwater: ${stripTerminalControls(detail)}\n`);
94
98
  }
95
99
  }
96
100
  /** The `{ emit, warn }` pair for a caller on a catch-less path (the orchestrator's poll loop
97
101
  * and the live config reload it drives): both route through logEventBestEffort and
98
- * warnEventBestEffort, so the "an unwritable feed must not end the fleet" policy has one home
102
+ * warnEventBestEffort, so the "an unwritable feed must not end the team" policy has one home
99
103
  * and the harness-loop warning shorthand cannot drift from warnEvent's event shape. */
100
104
  export function bestEffortEventLogger(root) {
101
105
  return {
102
106
  emit: (event) => logEventBestEffort(root, event),
103
- warn: (message) => warnEventBestEffort(root, "harness", message),
107
+ warn: (message) => warnEventBestEffort(root, TUMWATER_SOURCE, message),
104
108
  };
105
109
  }
106
110
  /** The shape this process's last event append left a log in, per log path: while the file
@@ -127,7 +131,7 @@ function appendEventLine(file, line) {
127
131
  // The append writes UTF-8 BYTES, so the growth is Buffer.byteLength, not the string's
128
132
  // code-unit length: a multi-byte event line (an em dash in an agent's message is enough)
129
133
  // would otherwise leave the memo short of the file's real size, every later stat would
130
- // miss it, and the memo would silently never hit on the fleet's actual traffic.
134
+ // miss it, and the memo would silently never hit on the team's actual traffic.
131
135
  size: st.size + (terminated ? 1 : 0) + Buffer.byteLength(line, "utf8"),
132
136
  });
133
137
  }