pi-crew 0.9.51 → 0.9.53

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 (611) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +216 -20
  3. package/dist/build-meta.json +2090 -1638
  4. package/dist/index.mjs +18057 -17119
  5. package/docs/actions-reference.md +431 -1
  6. package/index.ts +39 -10
  7. package/package.json +19 -9
  8. package/schema.json +41 -0
  9. package/scripts/check-bundle-size.mjs +38 -0
  10. package/scripts/test-runner.mjs +11 -1
  11. package/dist/index.mjs.map +0 -7
  12. package/docs/FEATURE_INTAKE.md +0 -126
  13. package/docs/HARNESS.md +0 -87
  14. package/docs/HARNESS_BACKLOG.md +0 -89
  15. package/docs/PROJECT_REVIEW.md +0 -270
  16. package/docs/PROJECT_REVIEW_FIXES.md +0 -342
  17. package/docs/PROJECT_REVIEW_ROUND4.md +0 -156
  18. package/docs/PROJECT_REVIEW_ROUND5.md +0 -86
  19. package/docs/REVIEW-FINDINGS-2026-06 +0 -357
  20. package/docs/REVIEW-FINDINGS-2026-07-CORE.md +0 -139
  21. package/docs/REVIEW-FINDINGS-2026-07.md +0 -125
  22. package/docs/TEST_MATRIX.md +0 -51
  23. package/docs/archive/README.md +0 -91
  24. package/docs/archive/followup-review-round3-2026-05-12.md +0 -342
  25. package/docs/archive/followup-review-round4-2026-05-13.md +0 -107
  26. package/docs/archive/pi-crew-bugs.md +0 -961
  27. package/docs/archive/pi-crew-test-final.md +0 -120
  28. package/docs/archive/pi-crew-test-results.md +0 -260
  29. package/docs/archive/pi-crew-test-round2.md +0 -136
  30. package/docs/archive/pi-crew-test-round4.md +0 -100
  31. package/docs/archive/pi-crew-test-round5.md +0 -70
  32. package/docs/archive/pi-crew-test-round6.md +0 -110
  33. package/docs/archive/pi-crew-v0.5.10-audit-fix-plan.md +0 -60
  34. package/docs/archive/pi-crew-v0.5.11-audit-fix-plan.md +0 -92
  35. package/docs/archive/pi-crew-v0.5.12-audit-fix-plan.md +0 -76
  36. package/docs/archive/pi-crew-v0.5.13-audit-fix-plan.md +0 -75
  37. package/docs/archive/pi-crew-v0.5.14-audit-fix-plan.md +0 -75
  38. package/docs/archive/pi-crew-v0.5.16-audit-fix-plan.md +0 -35
  39. package/docs/archive/pi-crew-v0.5.17-audit-fix-plan.md +0 -80
  40. package/docs/archive/pi-crew-v0.5.5-audit-fix-plan.md +0 -133
  41. package/docs/archive/pi-crew-v0.5.9-audit-fix-plan.md +0 -88
  42. package/docs/archive/pi-mono-opportunities.md +0 -969
  43. package/docs/archive/pi-mono-review.md +0 -291
  44. package/docs/archive/pi-subagent4-comparison.md +0 -261
  45. package/docs/archive/pi-subagents3-deep-analysis.md +0 -508
  46. package/docs/bugs/bug-021-notification-badge-counter-misleading.md +0 -293
  47. package/docs/bugs/bug-023-chain-windows-path-resolution.md +0 -106
  48. package/docs/bugs/bug-quota-display-truncation.md +0 -223
  49. package/docs/bugs/cross-session-notification-leakage.md +0 -82
  50. package/docs/code-review-2026-05-11.md +0 -592
  51. package/docs/coding-agent-optimization.md +0 -268
  52. package/docs/comparison-pi-subagents-vs-pi-crew.md +0 -303
  53. package/docs/decisions/0001-durable-state.md +0 -41
  54. package/docs/decisions/0002-child-process-for-async.md +0 -42
  55. package/docs/decisions/0003-depth-guard.md +0 -36
  56. package/docs/decisions/0004-execfile-over-exec.md +0 -34
  57. package/docs/decisions/0005-no-parameter-properties.md +0 -49
  58. package/docs/decisions/0006-publish-bundled-esm.md +0 -63
  59. package/docs/decisions/0007-active-run-binary-index.md +0 -54
  60. package/docs/decisions/0008-child-pi-warm-pool.md +0 -61
  61. package/docs/decisions/2026-07-21-broker-phase4-default-on.md +0 -77
  62. package/docs/decisions/2026-07-21-broker-windows-perms.md +0 -91
  63. package/docs/decisions/2026-07-22-broker-phase4-gated-on.md +0 -99
  64. package/docs/decisions/2026-07-24-oidc-trusted-publishing.md +0 -112
  65. package/docs/decisions/2026-07-26-c6-mascot-visibility-not-wired.md +0 -84
  66. package/docs/decisions/README.md +0 -26
  67. package/docs/deep-review-report.md +0 -384
  68. package/docs/distillation/cybersecurity-patterns.md +0 -294
  69. package/docs/feature-analysis-subagent4.md +0 -305
  70. package/docs/fix-plan-disabletools-exit-null.md +0 -219
  71. package/docs/fixes/BATCH_A_H1_H2.md +0 -86
  72. package/docs/fixes/bug-006-foreground-cancel-concurrent.md +0 -78
  73. package/docs/fixes/bug-007-async-notifier-stale-ctx.md +0 -112
  74. package/docs/fixes/bug-008-child-process-silent-timeout.md +0 -100
  75. package/docs/fixes/bug-009-executor-yield-limit-needs-attention.md +0 -75
  76. package/docs/fixes/bug-010-child-process-api-key-filtered.md +0 -109
  77. package/docs/fixes/bug-011-spawn-pi-enoent.md +0 -92
  78. package/docs/fixes/bug-012-essential-env-stripped.md +0 -89
  79. package/docs/fixes/bug-013-background-runner-death.md +0 -84
  80. package/docs/fixes/bug-014-infinite-retry-loop-needs-attention.md +0 -82
  81. package/docs/fixes/bug-015-background-runner-sigterm.md +0 -65
  82. package/docs/fixes/bug-017-background-runner-session-shutdown.md +0 -66
  83. package/docs/fixes/bug-017-background-runner-sigkill-double-fork.md +0 -28
  84. package/docs/fixes/bug-018-child-pi-worker-stdin-hang.md +0 -61
  85. package/docs/fixes/bug-019-phantom-runs-temp-workspace.md +0 -52
  86. package/docs/fixes/bug-020-infinite-retry-loop-needs-attention.md +0 -47
  87. package/docs/fixes/v0.9.10/locks-fix-verify.md +0 -3
  88. package/docs/fixes/v0.9.10/smoke-test.md +0 -12
  89. package/docs/followup-plan-2026-05-12.md +0 -463
  90. package/docs/followup-review-2026-05-12.md +0 -297
  91. package/docs/goals.md +0 -59
  92. package/docs/implementation-plan-top3.md +0 -333
  93. package/docs/issue-29-analysis.md +0 -189
  94. package/docs/live-mailbox-runtime.md +0 -36
  95. package/docs/migration/atomic-write-v2-migration.md +0 -299
  96. package/docs/next-upgrade-roadmap.md +0 -808
  97. package/docs/oh-my-pi-research.md +0 -509
  98. package/docs/optimization-plan-2026-07.md +0 -366
  99. package/docs/optimization-plan.md +0 -644
  100. package/docs/patterns/command-agent-skill.md +0 -71
  101. package/docs/perf/baseline-2026-05.md +0 -113
  102. package/docs/perf/final-report-2026-05.md +0 -206
  103. package/docs/perf/optimization-plan-2026-07-verified.md +0 -396
  104. package/docs/perf/performance-audit-report-2026-07.md +0 -948
  105. package/docs/perf/performance-review-2026-07.md +0 -287
  106. package/docs/perf/sprint-1-report.md +0 -71
  107. package/docs/perf/sprint-2-report.md +0 -81
  108. package/docs/perf/sprint-2.5-report.md +0 -53
  109. package/docs/perf/sprint-3-report.md +0 -36
  110. package/docs/perf/sprint-4-report.md +0 -47
  111. package/docs/perf/sprint-5-report.md +0 -51
  112. package/docs/perf/sprint-6-report.md +0 -94
  113. package/docs/perf/sprint-7-report.md +0 -74
  114. package/docs/perf/upgrade-plan-2026-05.md +0 -147
  115. package/docs/performance-optimization-execution-plan.md +0 -520
  116. package/docs/phase4-triage.md +0 -58
  117. package/docs/pi-crew-investigation-report.md +0 -411
  118. package/docs/product/README.md +0 -31
  119. package/docs/product/platform.md +0 -27
  120. package/docs/product/runtime-safety.md +0 -37
  121. package/docs/product/team-run.md +0 -39
  122. package/docs/product/team-tool.md +0 -37
  123. package/docs/runtime-analysis-child-vs-live.md +0 -171
  124. package/docs/runtime-migration-in-process-analysis.md +0 -250
  125. package/docs/skills/REFERENCE.md +0 -138
  126. package/docs/stories/README.md +0 -32
  127. package/docs/stories/US-DEPS-major-upgrade.md +0 -62
  128. package/docs/stories/backlog.md +0 -36
  129. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +0 -1270
  130. package/docs/templates/decision.md +0 -27
  131. package/docs/templates/story.md +0 -44
  132. package/docs/templates/validation-report.md +0 -32
  133. package/docs/ui-optimization-plan.md +0 -447
  134. package/src/adapters/claude-adapter.ts +0 -23
  135. package/src/adapters/codex-adapter.ts +0 -21
  136. package/src/adapters/cursor-adapter.ts +0 -17
  137. package/src/adapters/export-util.ts +0 -143
  138. package/src/adapters/index.ts +0 -15
  139. package/src/adapters/registry.ts +0 -18
  140. package/src/adapters/types.ts +0 -23
  141. package/src/agents/agent-config.ts +0 -194
  142. package/src/agents/agent-search.ts +0 -98
  143. package/src/agents/agent-serializer.ts +0 -38
  144. package/src/agents/discover-agents.ts +0 -623
  145. package/src/benchmark/benchmark-runner.ts +0 -313
  146. package/src/benchmark/feedback-loop.ts +0 -73
  147. package/src/config/config.ts +0 -1269
  148. package/src/config/defaults.ts +0 -193
  149. package/src/config/drift-detector.ts +0 -290
  150. package/src/config/markers.ts +0 -330
  151. package/src/config/resilient-parser.ts +0 -117
  152. package/src/config/role-tools.ts +0 -118
  153. package/src/config/suggestions.ts +0 -75
  154. package/src/config/types.ts +0 -278
  155. package/src/errors.ts +0 -194
  156. package/src/extension/action-suggestions.ts +0 -117
  157. package/src/extension/async-notifier.ts +0 -179
  158. package/src/extension/autonomous-policy.ts +0 -209
  159. package/src/extension/command-completions.ts +0 -120
  160. package/src/extension/context-status-injection.ts +0 -185
  161. package/src/extension/crew-autocomplete.ts +0 -131
  162. package/src/extension/crew-cleanup.ts +0 -168
  163. package/src/extension/crew-input-router.ts +0 -93
  164. package/src/extension/crew-shortcuts.ts +0 -87
  165. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  166. package/src/extension/crew-vibes/config.ts +0 -195
  167. package/src/extension/crew-vibes/figures.ts +0 -101
  168. package/src/extension/crew-vibes/font-detect.ts +0 -71
  169. package/src/extension/crew-vibes/footer.ts +0 -292
  170. package/src/extension/crew-vibes/index.ts +0 -432
  171. package/src/extension/crew-vibes/provider-usage.ts +0 -396
  172. package/src/extension/crew-vibes/render.ts +0 -206
  173. package/src/extension/crew-vibes/speed.ts +0 -286
  174. package/src/extension/cross-extension-rpc.ts +0 -350
  175. package/src/extension/help.ts +0 -62
  176. package/src/extension/import-index.ts +0 -74
  177. package/src/extension/knowledge-injection.ts +0 -402
  178. package/src/extension/management.ts +0 -571
  179. package/src/extension/message-renderers.ts +0 -108
  180. package/src/extension/notification-router.ts +0 -152
  181. package/src/extension/notification-sink.ts +0 -54
  182. package/src/extension/pi-api.ts +0 -56
  183. package/src/extension/plan-orchestrate.ts +0 -302
  184. package/src/extension/project-init.ts +0 -170
  185. package/src/extension/register.ts +0 -113
  186. package/src/extension/registration/artifact-cleanup.ts +0 -20
  187. package/src/extension/registration/command-registration.ts +0 -58
  188. package/src/extension/registration/command-utils.ts +0 -58
  189. package/src/extension/registration/commands.ts +0 -1228
  190. package/src/extension/registration/compaction-guard.ts +0 -333
  191. package/src/extension/registration/context-builder.ts +0 -135
  192. package/src/extension/registration/crash-recovery-cache.ts +0 -54
  193. package/src/extension/registration/foreground-run-controller.ts +0 -238
  194. package/src/extension/registration/hook-registration.ts +0 -119
  195. package/src/extension/registration/lazy-configurers.ts +0 -124
  196. package/src/extension/registration/lifecycle-handlers.ts +0 -923
  197. package/src/extension/registration/lifecycle.ts +0 -259
  198. package/src/extension/registration/observability.ts +0 -322
  199. package/src/extension/registration/registration-types.ts +0 -158
  200. package/src/extension/registration/runtime-cleanup.ts +0 -230
  201. package/src/extension/registration/subagent-helpers.ts +0 -125
  202. package/src/extension/registration/subagent-manager-setup.ts +0 -330
  203. package/src/extension/registration/subagent-tools.ts +0 -456
  204. package/src/extension/registration/team-tool.ts +0 -220
  205. package/src/extension/registration/tool-registration.ts +0 -50
  206. package/src/extension/registration/ui.ts +0 -172
  207. package/src/extension/registration/viewers.ts +0 -121
  208. package/src/extension/registration/wire-cross-extension.ts +0 -37
  209. package/src/extension/result-watcher.ts +0 -139
  210. package/src/extension/rpc-hmac.ts +0 -256
  211. package/src/extension/run-bundle-schema.ts +0 -104
  212. package/src/extension/run-export.ts +0 -97
  213. package/src/extension/run-import.ts +0 -168
  214. package/src/extension/run-index.ts +0 -131
  215. package/src/extension/run-maintenance.ts +0 -190
  216. package/src/extension/session-summary.ts +0 -18
  217. package/src/extension/team-manager-command.ts +0 -153
  218. package/src/extension/team-onboard.ts +0 -177
  219. package/src/extension/team-recommendation.ts +0 -288
  220. package/src/extension/team-tool/anchor.ts +0 -172
  221. package/src/extension/team-tool/api.ts +0 -1244
  222. package/src/extension/team-tool/auto-summarize.ts +0 -142
  223. package/src/extension/team-tool/cache-control.ts +0 -19
  224. package/src/extension/team-tool/cancel.ts +0 -337
  225. package/src/extension/team-tool/chain-dispatch.ts +0 -95
  226. package/src/extension/team-tool/chain-executor.ts +0 -379
  227. package/src/extension/team-tool/config-patch.ts +0 -50
  228. package/src/extension/team-tool/context.ts +0 -149
  229. package/src/extension/team-tool/destructive-gate.ts +0 -52
  230. package/src/extension/team-tool/doctor.ts +0 -431
  231. package/src/extension/team-tool/explain.ts +0 -282
  232. package/src/extension/team-tool/failure-patterns.ts +0 -118
  233. package/src/extension/team-tool/goal-wrap.ts +0 -330
  234. package/src/extension/team-tool/goal.ts +0 -556
  235. package/src/extension/team-tool/handle-schedule.ts +0 -332
  236. package/src/extension/team-tool/handle-settings.ts +0 -520
  237. package/src/extension/team-tool/health-monitor.ts +0 -481
  238. package/src/extension/team-tool/inspect.ts +0 -88
  239. package/src/extension/team-tool/intent-policy.ts +0 -45
  240. package/src/extension/team-tool/lifecycle-actions.ts +0 -615
  241. package/src/extension/team-tool/orchestrate.ts +0 -98
  242. package/src/extension/team-tool/parallel-dispatch.ts +0 -183
  243. package/src/extension/team-tool/plan.ts +0 -50
  244. package/src/extension/team-tool/respond.ts +0 -150
  245. package/src/extension/team-tool/run-not-found.ts +0 -49
  246. package/src/extension/team-tool/run.ts +0 -1035
  247. package/src/extension/team-tool/status.ts +0 -260
  248. package/src/extension/team-tool/workflow-manage.ts +0 -261
  249. package/src/extension/team-tool-types.ts +0 -26
  250. package/src/extension/team-tool.ts +0 -1116
  251. package/src/extension/tool-result.ts +0 -16
  252. package/src/extension/validate-resources.ts +0 -108
  253. package/src/hooks/registry.ts +0 -200
  254. package/src/hooks/types.ts +0 -69
  255. package/src/i18n.ts +0 -212
  256. package/src/observability/correlation.ts +0 -50
  257. package/src/observability/event-bus.ts +0 -86
  258. package/src/observability/event-to-metric.ts +0 -170
  259. package/src/observability/exporters/adapter.ts +0 -27
  260. package/src/observability/exporters/otlp-exporter.ts +0 -356
  261. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  262. package/src/observability/metric-registry.ts +0 -100
  263. package/src/observability/metric-retention.ts +0 -64
  264. package/src/observability/metric-sink.ts +0 -99
  265. package/src/observability/metrics-primitives.ts +0 -219
  266. package/src/plugins/plugin-define.ts +0 -6
  267. package/src/plugins/plugin-registry.ts +0 -32
  268. package/src/plugins/plugins/index.ts +0 -3
  269. package/src/plugins/plugins/nextjs.ts +0 -19
  270. package/src/plugins/plugins/vite.ts +0 -10
  271. package/src/plugins/plugins/vitest.ts +0 -9
  272. package/src/prompt/prompt-runtime.ts +0 -380
  273. package/src/runtime/adaptive-plan.ts +0 -563
  274. package/src/runtime/agent-control.ts +0 -215
  275. package/src/runtime/agent-memory.ts +0 -81
  276. package/src/runtime/agent-observability.ts +0 -122
  277. package/src/runtime/anchor-manager.ts +0 -475
  278. package/src/runtime/async-marker.ts +0 -32
  279. package/src/runtime/async-runner.ts +0 -381
  280. package/src/runtime/attention-events.ts +0 -28
  281. package/src/runtime/auto-summarize.ts +0 -344
  282. package/src/runtime/background-runner.ts +0 -837
  283. package/src/runtime/batch-barrier.ts +0 -147
  284. package/src/runtime/broker-issuer.ts +0 -37
  285. package/src/runtime/cancellation-token.ts +0 -99
  286. package/src/runtime/cancellation.ts +0 -99
  287. package/src/runtime/capability-inventory.ts +0 -137
  288. package/src/runtime/chain-parser.ts +0 -237
  289. package/src/runtime/chain-runner.ts +0 -606
  290. package/src/runtime/checkpoint.ts +0 -291
  291. package/src/runtime/child-pi-constants.ts +0 -42
  292. package/src/runtime/child-pi-kill.ts +0 -180
  293. package/src/runtime/child-pi-pool.ts +0 -68
  294. package/src/runtime/child-pi-spawn.ts +0 -287
  295. package/src/runtime/child-pi-steering.ts +0 -128
  296. package/src/runtime/child-pi-streams.ts +0 -296
  297. package/src/runtime/child-pi-transcript.ts +0 -169
  298. package/src/runtime/child-pi.ts +0 -1105
  299. package/src/runtime/coalesce-tasks.ts +0 -268
  300. package/src/runtime/code-summary.ts +0 -292
  301. package/src/runtime/command-trace.ts +0 -105
  302. package/src/runtime/compact-pipeline.ts +0 -56
  303. package/src/runtime/compact-stages/ansi-strip-stage.ts +0 -25
  304. package/src/runtime/compact-stages/blank-collapse-stage.ts +0 -31
  305. package/src/runtime/compact-stages/deduplicate-stage.ts +0 -34
  306. package/src/runtime/compact-stages/head-snap-stage.ts +0 -57
  307. package/src/runtime/compact-stages/index.ts +0 -23
  308. package/src/runtime/compact-stages/tail-capture-stage.ts +0 -77
  309. package/src/runtime/compact-stages/truncation-stage.ts +0 -74
  310. package/src/runtime/compaction-summary.ts +0 -278
  311. package/src/runtime/completion-guard.ts +0 -207
  312. package/src/runtime/concurrency.ts +0 -58
  313. package/src/runtime/crash-classification.ts +0 -229
  314. package/src/runtime/crash-recovery.ts +0 -577
  315. package/src/runtime/crew-agent-records.ts +0 -593
  316. package/src/runtime/crew-agent-runtime.ts +0 -62
  317. package/src/runtime/crew-broker-child.ts +0 -88
  318. package/src/runtime/crew-broker-client.ts +0 -673
  319. package/src/runtime/crew-broker-tokens.ts +0 -84
  320. package/src/runtime/crew-broker.ts +0 -1276
  321. package/src/runtime/crew-hooks.ts +0 -227
  322. package/src/runtime/cross-extension-rpc.ts +0 -143
  323. package/src/runtime/custom-tools/irc-tool.ts +0 -282
  324. package/src/runtime/custom-tools/submit-result-tool.ts +0 -102
  325. package/src/runtime/deadletter.ts +0 -47
  326. package/src/runtime/delivery-coordinator.ts +0 -211
  327. package/src/runtime/delta-conflict.ts +0 -348
  328. package/src/runtime/deterministic-ast.ts +0 -161
  329. package/src/runtime/diagnostic-export.ts +0 -171
  330. package/src/runtime/direct-run.ts +0 -45
  331. package/src/runtime/dwf-state-store.ts +0 -102
  332. package/src/runtime/dynamic-workflow-context.ts +0 -1016
  333. package/src/runtime/dynamic-workflow-runner.ts +0 -310
  334. package/src/runtime/effectiveness.ts +0 -90
  335. package/src/runtime/errors/crew-errors.ts +0 -162
  336. package/src/runtime/event-stream-bridge.ts +0 -98
  337. package/src/runtime/foreground-control.ts +0 -193
  338. package/src/runtime/foreground-watchdog.ts +0 -132
  339. package/src/runtime/global-worker-cap.ts +0 -96
  340. package/src/runtime/goal-achievement.ts +0 -148
  341. package/src/runtime/goal-evaluator.ts +0 -361
  342. package/src/runtime/goal-loop-runner.ts +0 -828
  343. package/src/runtime/goal-state-store.ts +0 -219
  344. package/src/runtime/green-contract.ts +0 -55
  345. package/src/runtime/group-join.ts +0 -267
  346. package/src/runtime/handoff-manager.ts +0 -598
  347. package/src/runtime/heartbeat-gradient.ts +0 -36
  348. package/src/runtime/heartbeat-watcher.ts +0 -239
  349. package/src/runtime/hidden-handoff.ts +0 -407
  350. package/src/runtime/important-line-classifier.ts +0 -140
  351. package/src/runtime/intercom-bridge.ts +0 -187
  352. package/src/runtime/iteration-hooks.ts +0 -305
  353. package/src/runtime/live-agent-control.ts +0 -136
  354. package/src/runtime/live-agent-manager.ts +0 -704
  355. package/src/runtime/live-control-realtime.ts +0 -56
  356. package/src/runtime/live-extension-bridge.ts +0 -148
  357. package/src/runtime/live-irc.ts +0 -97
  358. package/src/runtime/live-session-health.ts +0 -108
  359. package/src/runtime/live-session-runtime.ts +0 -1139
  360. package/src/runtime/loop-gates.ts +0 -128
  361. package/src/runtime/manifest-cache.ts +0 -375
  362. package/src/runtime/mcp-proxy.ts +0 -105
  363. package/src/runtime/metric-parser.ts +0 -36
  364. package/src/runtime/model-fallback.ts +0 -409
  365. package/src/runtime/model-resolver.ts +0 -126
  366. package/src/runtime/model-scope.ts +0 -158
  367. package/src/runtime/orphan-worker-registry.ts +0 -465
  368. package/src/runtime/output-validator.ts +0 -202
  369. package/src/runtime/overflow-recovery.ts +0 -206
  370. package/src/runtime/parallel-research.ts +0 -68
  371. package/src/runtime/parallel-utils.ts +0 -160
  372. package/src/runtime/parent-guard.ts +0 -134
  373. package/src/runtime/path-overlap.ts +0 -150
  374. package/src/runtime/peer-dep.ts +0 -292
  375. package/src/runtime/per-write-validator.ts +0 -181
  376. package/src/runtime/phase-progress.ts +0 -217
  377. package/src/runtime/phase-tracker.ts +0 -385
  378. package/src/runtime/pi-args.ts +0 -655
  379. package/src/runtime/pi-json-output.ts +0 -170
  380. package/src/runtime/pi-spawn.ts +0 -273
  381. package/src/runtime/pipeline-runner.ts +0 -523
  382. package/src/runtime/plan-templates.ts +0 -201
  383. package/src/runtime/policy-engine.ts +0 -115
  384. package/src/runtime/post-checks.ts +0 -142
  385. package/src/runtime/post-exit-stdio-guard.ts +0 -104
  386. package/src/runtime/process-lifecycle.ts +0 -491
  387. package/src/runtime/process-status.ts +0 -154
  388. package/src/runtime/progress-event-coalescer.ts +0 -44
  389. package/src/runtime/progress-tracker.ts +0 -124
  390. package/src/runtime/prose-compressor.ts +0 -162
  391. package/src/runtime/recovery-recipes.ts +0 -203
  392. package/src/runtime/replace.ts +0 -570
  393. package/src/runtime/resilient-edit.ts +0 -153
  394. package/src/runtime/result-extractor.ts +0 -213
  395. package/src/runtime/retry-executor.ts +0 -100
  396. package/src/runtime/retry-runner.ts +0 -336
  397. package/src/runtime/role-permission.ts +0 -59
  398. package/src/runtime/run-coalesced-task-group.ts +0 -308
  399. package/src/runtime/run-drift.ts +0 -219
  400. package/src/runtime/run-tracker.ts +0 -116
  401. package/src/runtime/runtime-policy.ts +0 -29
  402. package/src/runtime/runtime-resolver.ts +0 -172
  403. package/src/runtime/runtime-warmup.ts +0 -160
  404. package/src/runtime/scheduler.ts +0 -353
  405. package/src/runtime/semaphore.ts +0 -141
  406. package/src/runtime/sensitive-paths.ts +0 -93
  407. package/src/runtime/session-resources.ts +0 -25
  408. package/src/runtime/session-snapshot.ts +0 -59
  409. package/src/runtime/session-usage.ts +0 -79
  410. package/src/runtime/settings-store.ts +0 -155
  411. package/src/runtime/sidechain-output.ts +0 -35
  412. package/src/runtime/single-agent-compose.ts +0 -84
  413. package/src/runtime/skill-effectiveness.ts +0 -524
  414. package/src/runtime/skill-instructions.ts +0 -391
  415. package/src/runtime/stale-reconciler.ts +0 -680
  416. package/src/runtime/stream-preview.ts +0 -184
  417. package/src/runtime/streaming-output.ts +0 -50
  418. package/src/runtime/subagent-manager.ts +0 -561
  419. package/src/runtime/subprocess-tool-registry.ts +0 -70
  420. package/src/runtime/supervisor-contact.ts +0 -64
  421. package/src/runtime/task-display.ts +0 -52
  422. package/src/runtime/task-graph-scheduler.ts +0 -227
  423. package/src/runtime/task-graph.ts +0 -290
  424. package/src/runtime/task-health.ts +0 -83
  425. package/src/runtime/task-id.ts +0 -161
  426. package/src/runtime/task-output-context.ts +0 -602
  427. package/src/runtime/task-packet.ts +0 -268
  428. package/src/runtime/task-quality.ts +0 -199
  429. package/src/runtime/task-runner/capabilities.ts +0 -78
  430. package/src/runtime/task-runner/context-retrieval.ts +0 -163
  431. package/src/runtime/task-runner/live-executor.ts +0 -227
  432. package/src/runtime/task-runner/output-splitter.ts +0 -152
  433. package/src/runtime/task-runner/progress.ts +0 -157
  434. package/src/runtime/task-runner/prompt-builder.ts +0 -278
  435. package/src/runtime/task-runner/prompt-pipeline.ts +0 -88
  436. package/src/runtime/task-runner/result-utils.ts +0 -16
  437. package/src/runtime/task-runner/retrieval-orchestrator.ts +0 -310
  438. package/src/runtime/task-runner/run-projection.ts +0 -125
  439. package/src/runtime/task-runner/state-helpers.ts +0 -176
  440. package/src/runtime/task-runner/tail-read.ts +0 -34
  441. package/src/runtime/task-runner.ts +0 -1420
  442. package/src/runtime/team-runner-artifacts.ts +0 -13
  443. package/src/runtime/team-runner.ts +0 -1993
  444. package/src/runtime/tool-output-pruner.ts +0 -333
  445. package/src/runtime/tool-progress.ts +0 -278
  446. package/src/runtime/usage-tracker.ts +0 -73
  447. package/src/runtime/verification-gates.ts +0 -571
  448. package/src/runtime/verification-integrity.ts +0 -105
  449. package/src/runtime/verification-worktree.ts +0 -186
  450. package/src/runtime/worker-heartbeat.ts +0 -25
  451. package/src/runtime/worker-startup.ts +0 -83
  452. package/src/runtime/workflow-state.ts +0 -195
  453. package/src/runtime/workspace-lock.ts +0 -443
  454. package/src/runtime/workspace-tree.ts +0 -312
  455. package/src/runtime/yield-handler.ts +0 -224
  456. package/src/runtime/zombie-scanner.ts +0 -298
  457. package/src/schema/config-schema.ts +0 -327
  458. package/src/schema/team-tool-schema.ts +0 -415
  459. package/src/schema/validation-types.ts +0 -159
  460. package/src/skills/discover-skills.ts +0 -197
  461. package/src/skills/skill-templates.ts +0 -456
  462. package/src/skills/validate.ts +0 -285
  463. package/src/state/active-run-registry.ts +0 -439
  464. package/src/state/artifact-store.ts +0 -176
  465. package/src/state/atomic-write.ts +0 -903
  466. package/src/state/blob-store.ts +0 -308
  467. package/src/state/contracts.ts +0 -158
  468. package/src/state/crew-init.ts +0 -269
  469. package/src/state/decision-ledger.ts +0 -345
  470. package/src/state/event-log-rotation.ts +0 -342
  471. package/src/state/event-log.ts +0 -1311
  472. package/src/state/event-reconstructor.ts +0 -229
  473. package/src/state/gitignore-manager.ts +0 -46
  474. package/src/state/health-store.ts +0 -79
  475. package/src/state/hook-instinct-bridge.ts +0 -94
  476. package/src/state/hook-integrations.ts +0 -51
  477. package/src/state/instinct-store.ts +0 -275
  478. package/src/state/jsonl-writer.ts +0 -107
  479. package/src/state/locks.ts +0 -562
  480. package/src/state/mailbox.ts +0 -916
  481. package/src/state/observation-store.ts +0 -176
  482. package/src/state/run-cache.ts +0 -193
  483. package/src/state/run-graph.ts +0 -183
  484. package/src/state/run-metrics.ts +0 -165
  485. package/src/state/schedule.ts +0 -166
  486. package/src/state/session-state-map.ts +0 -51
  487. package/src/state/state-store.ts +0 -958
  488. package/src/state/task-claims.ts +0 -61
  489. package/src/state/tiered-eval.ts +0 -480
  490. package/src/state/types-eval.ts +0 -58
  491. package/src/state/types.ts +0 -440
  492. package/src/state/usage.ts +0 -138
  493. package/src/state/worker-atomic-writer.ts +0 -194
  494. package/src/subagents/async-entry.ts +0 -1
  495. package/src/subagents/index.ts +0 -3
  496. package/src/subagents/live/control.ts +0 -1
  497. package/src/subagents/live/manager.ts +0 -1
  498. package/src/subagents/live/realtime.ts +0 -1
  499. package/src/subagents/live/session-runtime.ts +0 -1
  500. package/src/subagents/manager.ts +0 -1
  501. package/src/subagents/spawn.ts +0 -1
  502. package/src/teams/discover-teams.ts +0 -187
  503. package/src/teams/team-config.ts +0 -27
  504. package/src/teams/team-serializer.ts +0 -38
  505. package/src/tools/safe-bash-extension.ts +0 -54
  506. package/src/tools/safe-bash.ts +0 -505
  507. package/src/types/diff.d.ts +0 -18
  508. package/src/types/new-api-types.ts +0 -35
  509. package/src/ui/agent-management-overlay.ts +0 -160
  510. package/src/ui/card-colors.ts +0 -139
  511. package/src/ui/crew-footer.ts +0 -102
  512. package/src/ui/crew-select-list.ts +0 -111
  513. package/src/ui/dashboard-panes/agents-pane.ts +0 -164
  514. package/src/ui/dashboard-panes/cancellation-pane.ts +0 -66
  515. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  516. package/src/ui/dashboard-panes/health-pane.ts +0 -31
  517. package/src/ui/dashboard-panes/mailbox-pane.ts +0 -35
  518. package/src/ui/dashboard-panes/metrics-pane.ts +0 -37
  519. package/src/ui/dashboard-panes/progress-pane.ts +0 -38
  520. package/src/ui/dashboard-panes/transcript-pane.ts +0 -10
  521. package/src/ui/deploy-bundled-themes.ts +0 -71
  522. package/src/ui/dwf-phase-display.ts +0 -151
  523. package/src/ui/dynamic-border.ts +0 -35
  524. package/src/ui/heartbeat-aggregator.ts +0 -74
  525. package/src/ui/key-utils.ts +0 -42
  526. package/src/ui/keybinding-map.ts +0 -248
  527. package/src/ui/layout-primitives.ts +0 -106
  528. package/src/ui/live-conversation-overlay.ts +0 -183
  529. package/src/ui/live-duration.ts +0 -55
  530. package/src/ui/live-run-sidebar.ts +0 -293
  531. package/src/ui/loaders.ts +0 -6
  532. package/src/ui/mascot.ts +0 -449
  533. package/src/ui/overlays/agent-picker-overlay.ts +0 -65
  534. package/src/ui/overlays/confirm-overlay.ts +0 -60
  535. package/src/ui/overlays/help-overlay.ts +0 -177
  536. package/src/ui/overlays/mailbox-compose-overlay.ts +0 -181
  537. package/src/ui/overlays/mailbox-compose-preview.ts +0 -78
  538. package/src/ui/overlays/mailbox-detail-overlay.ts +0 -154
  539. package/src/ui/pi-ui-compat.ts +0 -68
  540. package/src/ui/powerbar-publisher.ts +0 -459
  541. package/src/ui/render-coalescer.ts +0 -101
  542. package/src/ui/render-diff.ts +0 -160
  543. package/src/ui/render-scheduler.ts +0 -278
  544. package/src/ui/run-action-dispatcher.ts +0 -182
  545. package/src/ui/run-dashboard.ts +0 -904
  546. package/src/ui/run-event-bus.ts +0 -381
  547. package/src/ui/run-snapshot-cache.ts +0 -1057
  548. package/src/ui/settings-overlay.ts +0 -1067
  549. package/src/ui/shared-overlay-scheduler.ts +0 -96
  550. package/src/ui/snapshot-types.ts +0 -90
  551. package/src/ui/spinner.ts +0 -17
  552. package/src/ui/status-colors.ts +0 -143
  553. package/src/ui/syntax-highlight.ts +0 -136
  554. package/src/ui/terminal-status.ts +0 -269
  555. package/src/ui/theme-adapter.ts +0 -271
  556. package/src/ui/theme-discovery.ts +0 -199
  557. package/src/ui/tool-progress-formatter.ts +0 -93
  558. package/src/ui/tool-render.ts +0 -453
  559. package/src/ui/tool-renderers/brief-mode.ts +0 -225
  560. package/src/ui/tool-renderers/index.ts +0 -726
  561. package/src/ui/transcript-cache.ts +0 -128
  562. package/src/ui/transcript-entries.ts +0 -256
  563. package/src/ui/transcript-viewer.ts +0 -424
  564. package/src/ui/widget/index.ts +0 -391
  565. package/src/ui/widget/widget-formatters.ts +0 -182
  566. package/src/ui/widget/widget-model.ts +0 -110
  567. package/src/ui/widget/widget-renderer.ts +0 -182
  568. package/src/ui/widget/widget-types.ts +0 -36
  569. package/src/utils/bm25-search.ts +0 -235
  570. package/src/utils/completion-dedupe.ts +0 -63
  571. package/src/utils/conflict-detect.ts +0 -668
  572. package/src/utils/env-allowlist.ts +0 -30
  573. package/src/utils/env-filter.ts +0 -155
  574. package/src/utils/file-coalescer.ts +0 -98
  575. package/src/utils/fingerprint.ts +0 -180
  576. package/src/utils/frontmatter.ts +0 -70
  577. package/src/utils/fs-watch.ts +0 -37
  578. package/src/utils/gh-protocol.ts +0 -556
  579. package/src/utils/git.ts +0 -260
  580. package/src/utils/guards.ts +0 -107
  581. package/src/utils/ids.ts +0 -24
  582. package/src/utils/incremental-reader.ts +0 -220
  583. package/src/utils/internal-error.ts +0 -10
  584. package/src/utils/names.ts +0 -36
  585. package/src/utils/ndjson.ts +0 -115
  586. package/src/utils/paths.ts +0 -227
  587. package/src/utils/project-detector.ts +0 -160
  588. package/src/utils/redaction.ts +0 -347
  589. package/src/utils/resolve-shell.ts +0 -36
  590. package/src/utils/run-watcher-registry.ts +0 -152
  591. package/src/utils/safe-paths.ts +0 -383
  592. package/src/utils/scan-cache.ts +0 -144
  593. package/src/utils/session-utils.ts +0 -110
  594. package/src/utils/sleep.ts +0 -54
  595. package/src/utils/socket-path.ts +0 -127
  596. package/src/utils/sse-parser.ts +0 -129
  597. package/src/utils/task-name-generator.ts +0 -337
  598. package/src/utils/timings.ts +0 -33
  599. package/src/utils/token-counter.ts +0 -200
  600. package/src/utils/visual.ts +0 -207
  601. package/src/workflows/cost-estimator.ts +0 -34
  602. package/src/workflows/discover-workflows.ts +0 -290
  603. package/src/workflows/intermediate-store.ts +0 -166
  604. package/src/workflows/preflight-validator.ts +0 -168
  605. package/src/workflows/topology-analyzer.ts +0 -203
  606. package/src/workflows/validate-workflow.ts +0 -50
  607. package/src/workflows/workflow-config.ts +0 -70
  608. package/src/workflows/workflow-serializer.ts +0 -32
  609. package/src/worktree/branch-freshness.ts +0 -112
  610. package/src/worktree/cleanup.ts +0 -341
  611. package/src/worktree/worktree-manager.ts +0 -1200
@@ -1,1311 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { DEFAULT_EVENT_LOG } from "../config/defaults.ts";
5
- import { errors } from "../errors.ts";
6
- import { emitFromTeamEvent } from "../ui/run-event-bus.ts";
7
- import { type IncrementalReadState, readJsonlSince, readJsonlTail } from "../utils/incremental-reader.ts";
8
- import { logInternalError } from "../utils/internal-error.ts";
9
- import { redactSecrets } from "../utils/redaction.ts";
10
- import { sleepSync } from "../utils/sleep.ts";
11
- import { atomicWriteFile } from "./atomic-write.ts";
12
- import {
13
- applyCompactionUnlocked,
14
- compactEventLog,
15
- needsRotation,
16
- prepareCompaction,
17
- rotateEventLog,
18
- rotateEventLogUnlocked,
19
- } from "./event-log-rotation.ts";
20
- import { appendFileViaWorker, isWorkerAtomicWriterEnabled } from "./worker-atomic-writer.ts";
21
-
22
- export type TeamEventProvenance = "live_worker" | "test" | "healthcheck" | "replay" | "api" | "background" | "team_runner";
23
- export type TeamWatcherAction = "act" | "observe" | "ignore";
24
-
25
- export interface TeamEventSessionIdentity {
26
- title: string;
27
- workspace: string;
28
- purpose: string;
29
- placeholderReason?: string;
30
- }
31
-
32
- export interface TeamEventOwnership {
33
- owner: string;
34
- workflowScope: string;
35
- watcherAction: TeamWatcherAction;
36
- }
37
-
38
- export interface TeamEventMetadata {
39
- seq: number;
40
- provenance: TeamEventProvenance;
41
- parentEventId?: string;
42
- attemptId?: string;
43
- branchId?: string;
44
- causationId?: string;
45
- correlationId?: string;
46
- sessionIdentity?: TeamEventSessionIdentity;
47
- ownership?: TeamEventOwnership;
48
- nudgeId?: string;
49
- appended?: boolean;
50
- fingerprint?: string;
51
- confidence?: "low" | "medium" | "high";
52
- }
53
-
54
- export interface TeamEvent {
55
- time: string;
56
- type: string;
57
- runId: string;
58
- taskId?: string;
59
- message?: string;
60
- data?: Record<string, unknown>;
61
- metadata?: TeamEventMetadata;
62
- }
63
-
64
- export type AppendTeamEvent = Omit<TeamEvent, "time" | "metadata"> & {
65
- metadata?: Partial<TeamEventMetadata>;
66
- };
67
-
68
- const TERMINAL_EVENT_TYPES = new Set<string>(DEFAULT_EVENT_LOG.terminalEventTypes);
69
- const MAX_EVENTS_BYTES = 50 * 1024 * 1024;
70
-
71
- const sequenceCache = new Map<string, { size: number; mtimeMs: number; seq: number; lastAccessMs: number }>();
72
- const MAX_SEQUENCE_CACHE_ENTRIES = 256;
73
- let appendCounter = 0;
74
- let overflowCounter = 0;
75
-
76
- /** Simple cross-process lock for an eventsPath to prevent JSONL interleave on concurrent append.
77
- * Detects stale locks by checking the owner PID written inside the lock directory.
78
- *
79
- * @deprecated Prefer `appendEventAsync()` for callers in async contexts. The sync lock
80
- * uses `sleepSync` which blocks the event loop and prevents AbortSignal handlers from firing.
81
- *
82
- * SECURITY WARNING: This function uses `sleepSync` in its lock-acquire retry loop, which
83
- * blocks the Node.js event loop for up to 5s. During that time, AbortSignal handlers
84
- * cannot fire, SIGTERM handlers are delayed, and the process appears unresponsive to
85
- * orchestrator health checks. Known callers include `appendEvent` (sync path),
86
- * `flushOneEventLogBuffer`, and `state/mailbox.ts`. Prefer the async alternative
87
- * (`appendEventAsync`) for all new code.
88
- */
89
- export function withEventLogLockSync<T>(eventsPath: string, fn: () => T, options?: { timeoutMs?: number; staleMs?: number }): T {
90
- // Ensure parent directory exists before attempting lock
91
- fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
92
- const lockDir = `${eventsPath}.lock`;
93
- const pidFile = path.join(lockDir, "pid");
94
- const start = Date.now();
95
- // SECURITY (HIGH #2 fix): Reduced from 120s to 5s to prevent blocking the
96
- // event loop indefinitely. ~100 retries × 50ms ≈ 5s max. After timeout, we
97
- // throw a clear error instead of blocking forever. This ensures AbortSignal
98
- // handlers, SIGTERM, and graceful shutdown can fire within seconds.
99
- const timeout = options?.timeoutMs ?? 5000;
100
- const staleMs = options?.staleMs ?? 10000;
101
- let acquired = false;
102
- while (true) {
103
- try {
104
- // NOTE: mkdir-based lock is acceptable here. On POSIX systems, directory
105
- // creation via mkdir with O_CREAT|O_EXCL semantics is atomic — equivalent
106
- // to O_EXCL file open. The stale detection below uses process.kill(pid, 0)
107
- // which has a TOCTOU race, but O_EXCL is used to atomically verify-and-remove
108
- // the stale lock in one operation, eliminating the race. The 5s timeout
109
- // (reduced from 120s) is appropriate.
110
- fs.mkdirSync(lockDir);
111
- try {
112
- atomicWriteFile(pidFile, String(process.pid));
113
- } catch {
114
- /* best-effort */
115
- }
116
- acquired = true;
117
- break;
118
- } catch {
119
- if (Date.now() - start > timeout) {
120
- // SECURITY (HIGH #2 fix): Throw instead of continuing without lock.
121
- // Previously this logged and broke out of the loop, executing the
122
- // operation without lock protection. Now we throw so callers can retry.
123
- // E1 (Round 15): structured CrewError (E010) with help hint so users know
124
- // to check for orphaned .lock dirs / stale processes.
125
- throw errors.eventLogLockTimeout(eventsPath, timeout);
126
- }
127
- // Round 26 (BUG 3): mtime-based stale check INDEPENDENT of pidFile.
128
- // If the holder crashed between mkdir and writing pidFile, there is no
129
- // pidFile to read — the old code just slept until the 5s timeout, then
130
- // threw, leaving the dir orphaned FOREVER (every retry repeats the
131
- // timeout). Now: if the lock dir's mtime exceeds staleMs, reclaim it.
132
- try {
133
- const dirStat = fs.statSync(lockDir);
134
- if (Date.now() - dirStat.mtimeMs > staleMs) {
135
- fs.rmSync(lockDir, { recursive: true, force: true });
136
- continue;
137
- }
138
- } catch {
139
- /* dir vanished — let loop retry */
140
- }
141
- // Round 26 (BUG 4): the mtime check was previously NESTED inside
142
- // `if (!alive)`, so a recycled PID (crashed holder's PID reused by an
143
- // unrelated live process) kept `alive=true` and the mtime check NEVER
144
- // fired → permanent wedge. mtime is now checked FIRST (above) for ALL
145
- // holders. The PID check below is a secondary fast-path: if the holder
146
- // PID is provably dead AND the lock isn't stale yet, we still wait
147
- // (don't steal a fresh lock just because the pid lookup raced).
148
- try {
149
- const raw = fs.readFileSync(pidFile, "utf-8").trim();
150
- const ownerPid = Number.parseInt(raw, 10);
151
- if (!Number.isNaN(ownerPid) && ownerPid !== process.pid) {
152
- let alive = false;
153
- try {
154
- process.kill(ownerPid, 0);
155
- alive = true;
156
- } catch {
157
- /* dead */
158
- }
159
- // (mtime already handled above; nothing to do here for dead-but-fresh.)
160
- void alive;
161
- }
162
- } catch {
163
- /* no pid file — mtime check above already handles it */
164
- }
165
- sleepSync(50);
166
- }
167
- }
168
- try {
169
- return fn();
170
- } finally {
171
- if (acquired) {
172
- // Round 26 (BUG 5): token/PID-guarded release. Previously the release
173
- // was an UNCONDITIONAL rmSync. If our fn exceeded staleMs, another
174
- // process could steal our lock (rm our dir, make its own); when our fn
175
- // finished our finally block would then DELETE THE STEALER's dir → both
176
- // in the critical section + lost lock. Verify the pidFile still records
177
- // OUR pid before removing; if it doesn't, the lock was stolen and the
178
- // current holder owns the dir.
179
- try {
180
- const currentPid = fs.readFileSync(pidFile, "utf-8").trim();
181
- if (currentPid === String(process.pid)) {
182
- fs.rmSync(lockDir, { recursive: true, force: true });
183
- }
184
- } catch {
185
- /* lock stolen or already gone — do not touch */
186
- }
187
- }
188
- }
189
- }
190
-
191
- function evictOldestSequenceCacheEntries(): void {
192
- // FIX: Evict by lastAccessMs (access time), not insertion order.
193
- // Frequently accessed entries should be retained even if older.
194
- const toEvict = Math.ceil(MAX_SEQUENCE_CACHE_ENTRIES / 2);
195
- // Sort entries by lastAccessMs ascending (oldest first)
196
- const entries = [...sequenceCache.entries()].sort((a, b) => a[1].lastAccessMs - b[1].lastAccessMs);
197
- // Evict the oldest half
198
- for (let i = 0; i < toEvict && i < entries.length; i++) {
199
- sequenceCache.delete(entries[i][0]);
200
- }
201
- }
202
-
203
- /** @internal — exported for sequence-cache LRU testing (Round 19). */
204
- export function __test__sequenceCacheSize(): number {
205
- return sequenceCache.size;
206
- }
207
-
208
- /** @internal — seed an entry into the sequence cache for testing. */
209
- export function __test__seedSequenceCache(eventsPath: string, lastAccessMs: number): void {
210
- sequenceCache.set(eventsPath, {
211
- size: 1,
212
- mtimeMs: 0,
213
- seq: 0,
214
- lastAccessMs,
215
- });
216
- }
217
-
218
- /** @internal — expose eviction for testing. */
219
- export function __test__evictOldestSequenceCacheEntries(): void {
220
- evictOldestSequenceCacheEntries();
221
- }
222
-
223
- /** @internal — clear the sequence cache. */
224
- export function __test__clearSequenceCache(): void {
225
- sequenceCache.clear();
226
- }
227
-
228
- /** @internal — clear the in-process seqCounters Map so nextSequence seeds
229
- * fresh from the sidecar/file (simulates a process restart for testing). */
230
- export function __test__clearSeqCounters(): void {
231
- seqCounters.clear();
232
- }
233
-
234
- /** @internal — the raw nextSequence for testing (forces re-seed from disk
235
- * by requiring the caller to have already cleared both caches). */
236
- export function __test__nextSequence(eventsPath: string): number {
237
- return nextSequence(eventsPath);
238
- }
239
-
240
- /** @internal — the max sequence cache entries bound. */
241
- export const MAX_SEQUENCE_CACHE_ENTRIES_VALUE = MAX_SEQUENCE_CACHE_ENTRIES;
242
-
243
- export function sequencePath(eventsPath: string): string {
244
- return `${eventsPath}.seq`;
245
- }
246
-
247
- function parseSequence(raw: string): number | undefined {
248
- const value = Number.parseInt(raw.trim(), 10);
249
- return Number.isInteger(value) && value >= 0 ? value : undefined;
250
- }
251
-
252
- export function scanSequence(eventsPath: string): number {
253
- if (!fs.existsSync(eventsPath)) return 0;
254
- let max = 0;
255
- let skipped = 0;
256
- for (const line of fs.readFileSync(eventsPath, "utf-8").split("\n")) {
257
- if (!line.trim()) continue;
258
- try {
259
- const event = JSON.parse(line) as TeamEvent;
260
- max = Math.max(max, event.metadata?.seq ?? 0);
261
- } catch {
262
- skipped++;
263
- }
264
- }
265
- if (skipped > 0) {
266
- logInternalError("event-log.scanSequence.corrupt_lines", undefined, `${eventsPath}: skipped ${skipped} corrupt line(s)`);
267
- }
268
- return max;
269
- }
270
-
271
- function readStoredSequence(eventsPath: string): number | undefined {
272
- try {
273
- return parseSequence(fs.readFileSync(sequencePath(eventsPath), "utf-8"));
274
- } catch {
275
- return undefined;
276
- }
277
- }
278
-
279
- function nextSequence(eventsPath: string): number {
280
- if (!fs.existsSync(eventsPath)) return 1;
281
- const stat = fs.statSync(eventsPath);
282
- const cached = sequenceCache.get(eventsPath);
283
- if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
284
- return cached.seq + 1;
285
- }
286
- // FIX: Trust the sidecar seq file if it exists and the file is non-empty.
287
- // Explicitly check for file shrinkage (stat.size < cached.size) to trigger
288
- // re-scan when rotation or compaction has occurred.
289
- const stored = readStoredSequence(eventsPath);
290
- const fileShrunk = cached && stat.size < cached.size;
291
- if (stored !== undefined && !fileShrunk) {
292
- // EL-1: the sidecar can regress via sync/async interleave —
293
- // appendEventAsync reserves a seq, yields during appendFile/fsync, a
294
- // concurrent sync appendEvent reserves+persists a higher seq, then the
295
- // async path persists its lower seq — regressing the sidecar below the
296
- // file's true max. Trusting the regressed sidecar alone would return a
297
- // duplicate seq on the next append. Take max with scanSequence so a
298
- // regressed sidecar cannot cause duplicate sequence numbers.
299
- const fileMax = scanSequence(eventsPath);
300
- const safeSeq = Math.max(stored, fileMax);
301
- sequenceCache.set(eventsPath, {
302
- size: stat.size,
303
- mtimeMs: stat.mtimeMs,
304
- seq: safeSeq,
305
- lastAccessMs: Date.now(),
306
- });
307
- return safeSeq + 1;
308
- }
309
- const current = scanSequence(eventsPath);
310
- sequenceCache.set(eventsPath, {
311
- size: stat.size,
312
- mtimeMs: stat.mtimeMs,
313
- seq: current,
314
- lastAccessMs: Date.now(),
315
- });
316
- persistSequence(eventsPath, current);
317
- return current + 1;
318
- }
319
-
320
- function persistSequence(eventsPath: string, seq: number): void {
321
- try {
322
- atomicWriteFile(sequencePath(eventsPath), String(seq));
323
- } catch (error) {
324
- logInternalError("event-log.persist-sequence-file", error, `eventsPath=${eventsPath}`);
325
- }
326
- }
327
-
328
- // B7: single in-process monotonic sequence counter per eventsPath. The three
329
- // append paths — sync appendEvent (withEventLogLockSync file lock), buffered
330
- // flush (asyncLocks promise chain), and direct appendEventAsync (asyncQueues
331
- // promise chain) — use DIFFERENT locks, so the old read-sidecar / compute /
332
- // persist-sidecar sequence logic in nextSequence() raced ACROSS paths and
333
- // produced duplicate sequence numbers (observed live: distinct events sharing
334
- // a seq; no data loss — only the counter collided). A single in-process counter
335
- // makes assignment atomic (JS is single-threaded); persistSequence() keeps the
336
- // sidecar durable for crash recovery across restarts.
337
- const seqCounters = new Map<string, number>();
338
-
339
- /** Atomically reserve the next sequence number for `eventsPath`. */
340
- function reserveSequence(eventsPath: string): number {
341
- let last = seqCounters.get(eventsPath);
342
- if (last === undefined) {
343
- // Seed once from the authoritative source (sidecar / cache / file scan).
344
- // nextSequence() returns the NEXT seq to assign, so the last assigned is one less.
345
- last = nextSequence(eventsPath) - 1;
346
- }
347
- const next = last + 1;
348
- seqCounters.set(eventsPath, next);
349
- return next;
350
- }
351
-
352
- /** Keep the in-process counter monotonic w.r.t. an explicitly-provided seq
353
- * (e.g. baseMetadata.seq) so a later auto-assigned seq never collides with it. */
354
- function advanceSequenceCounter(eventsPath: string, seq: number): void {
355
- const last = seqCounters.get(eventsPath);
356
- if (last === undefined || seq > last) seqCounters.set(eventsPath, seq);
357
- }
358
-
359
- export function computeEventFingerprint(event: Pick<TeamEvent, "type" | "runId" | "taskId" | "data">): string {
360
- return createHash("sha256")
361
- .update(
362
- JSON.stringify({
363
- type: event.type,
364
- runId: event.runId,
365
- taskId: event.taskId,
366
- data: event.data ?? null,
367
- }),
368
- )
369
- .digest("hex")
370
- .slice(0, 16);
371
- }
372
-
373
- /**
374
- * Check for sequence gaps between the sidecar file and the events file.
375
- * This detects situations where the sidecar records a sequence number that has
376
- * no corresponding event in the file (e.g., due to a crash between
377
- * persistSequence and appendFile in older code, or other corruption).
378
- *
379
- * Returns an array of gap info: for each gap found, { missing: n } indicates
380
- * sequence n is recorded in sidecar but has no corresponding event.
381
- * An empty array means no gaps were found.
382
- */
383
- export function checkSequenceGaps(eventsPath: string): { missing: number }[] {
384
- if (!fs.existsSync(eventsPath)) return [];
385
- const gaps: { missing: number }[] = [];
386
- const storedSeq = readStoredSequence(eventsPath);
387
- if (storedSeq === undefined) return [];
388
- const maxInFile = scanSequence(eventsPath);
389
- // If sidecar is ahead of file, report the missing sequences
390
- // (sidecar stores the NEXT sequence to use, so storedSeq is the last written)
391
- if (storedSeq > maxInFile) {
392
- for (let i = maxInFile + 1; i <= storedSeq; i++) {
393
- gaps.push({ missing: i });
394
- }
395
- }
396
- return gaps;
397
- }
398
-
399
- /**
400
- * @deprecated Prefer `appendEventAsync()` in async contexts. The sync lock uses
401
- * `sleepSync` which blocks the Node.js event loop, preventing AbortSignal handlers
402
- * from firing and degrading live-agent responsiveness.
403
- */
404
- export function appendEvent(eventsPath: string, event: AppendTeamEvent): TeamEvent {
405
- // NOTE: appendEvent is a sync function that uses withEventLogLockSync (sleepSync).
406
- // It cannot route through appendEventBuffered because the buffer timer requires
407
- // the event loop to fire, which sleepSync blocks. Both terminal and non-terminal
408
- // events use the direct sync path here. For non-terminal events, callers should
409
- // prefer appendEventAsync (which routes through the buffer for coalesced writes).
410
- return withEventLogLockSync(eventsPath, () => appendEventInsideLock(eventsPath, event));
411
- }
412
-
413
- // --- Async write queue (non-blocking alternative to withEventLogLockSync) ---
414
- const asyncQueues = new Map<string, Promise<unknown>>();
415
-
416
- // --- Async lock for flush operations (non-blocking alternative to withEventLogLockSync) ---
417
- // Uses promise-chain pattern to ensure sequential lock acquisition without blocking the event loop.
418
- const asyncLocks = new Map<string, Promise<unknown>>();
419
-
420
- /** Drain all pending async writes by awaiting all in-flight queue promises.
421
- * Called on process exit to minimize event loss for crash-sensitive events.
422
- * Note: SIGKILL (kill -9) cannot be intercepted and will still lose events.
423
- */
424
- async function drainAsyncQueues(): Promise<void> {
425
- const promises = [...asyncQueues.values()];
426
- if (promises.length === 0) return;
427
- // Use allSettled to ensure a rejected promise doesn't prevent others from completing.
428
- await Promise.allSettled(promises);
429
- }
430
-
431
- /** Async lock using promise-chain pattern to avoid blocking the Node.js event loop.
432
- * Unlike withEventLogLockSync, this uses async I/O and does not use sleepSync,
433
- * allowing AbortSignal handlers and SIGTERM handlers to proceed while waiting.
434
- */
435
- async function withEventLogLockAsync(eventsPath: string, fn: () => Promise<void>): Promise<void> {
436
- const queueKey = eventsPath;
437
- const prev = asyncLocks.get(queueKey) ?? Promise.resolve();
438
- const next = prev.then(async (): Promise<void> => {
439
- await fn();
440
- });
441
- asyncLocks.set(queueKey, next);
442
- try {
443
- await next;
444
- } finally {
445
- // Compare-and-delete: only remove our entry if it still points at our
446
- // promise. With 3+ overlapping callers, an earlier caller's finally would
447
- // otherwise delete a later caller's promise, letting the next caller start
448
- // immediately (in parallel) -> broken mutual exclusion, duplicate seqs.
449
- if (asyncLocks.get(queueKey) === next) {
450
- asyncLocks.delete(queueKey);
451
- }
452
- }
453
- }
454
-
455
- /** Reset event log mode (for testing only). */
456
- export function resetEventLogMode(): void {
457
- asyncQueues.clear();
458
- // B7: clear in-process sequence counters alongside async state so tests
459
- // don't leak seq state between runs.
460
- seqCounters.clear();
461
- }
462
-
463
- /**
464
- * Append an event to the event log using non-blocking async I/O.
465
- *
466
- * Uses a per-eventsPath promise-chain queue to ensure sequential writes without
467
- * blocking the Node.js event loop. This allows AbortSignal handlers and other
468
- * async operations to proceed while events are being persisted.
469
- *
470
- * For callers that are already in an async context (team-runner, task-runner,
471
- * foreground-control, etc.), prefer this over the sync `appendEvent()`.
472
- */
473
- export async function appendEventAsync(eventsPath: string, event: AppendTeamEvent): Promise<TeamEvent> {
474
- // FIX (v0.9.26): Do NOT route non-terminal events through appendEventBuffered.
475
- // The buffer uses a 20ms timer + withEventLogLockAsync (promise chain), while
476
- // the sync appendEvent path uses withEventLogLockSync (file lock with sleepSync).
477
- // Mixing these two lock mechanisms on the same eventsPath causes a deadlock:
478
- // the buffer timer can't fire while sleepSync blocks the event loop, and
479
- // sleepSync can't acquire the lock while the buffer holds it via the promise
480
- // chain. This deadlocked adaptive-implementation, implementation-fanout,
481
- // parallel-research-dynamic, run-analysis, and team-run tests (>300s timeout).
482
- // Reverted to v0.9.19 behavior: ALL events use the asyncQueues direct path.
483
- // The buffer (appendEventBuffered) is still available for explicit callers
484
- // that want coalesced writes (e.g., appendEventFireAndForget), but
485
- // appendEventAsync itself does NOT buffer.
486
- const queueKey = eventsPath;
487
- const prev = asyncQueues.get(queueKey) ?? Promise.resolve();
488
- const next = prev.then(async (): Promise<TeamEvent> => {
489
- // Ensure directory exists
490
- await fs.promises.mkdir(path.dirname(eventsPath), { recursive: true });
491
-
492
- // Build metadata (same logic as appendEventInsideLock)
493
- // FIX: Sequence is computed INSIDE the promise chain. We NO LONGER persist
494
- // the sequence number before the append — that caused sequence reuse if
495
- // appendFile failed after persistSequence succeeded. Instead, we persist
496
- // ONLY AFTER successful appendFile, so the sidecar is only updated when
497
- // the event is definitively written. If appendFile fails, the sidecar is
498
- // not updated and nextSequence() will re-scan on next call, returning the
499
- // correct value without reuse.
500
- const baseMetadata = event.metadata;
501
- let seq: number;
502
- if (baseMetadata?.seq !== undefined) {
503
- seq = baseMetadata.seq;
504
- advanceSequenceCounter(eventsPath, seq);
505
- } else {
506
- seq = reserveSequence(eventsPath);
507
- // NOTE: We do NOT call persistSequence here. It will be called AFTER
508
- // successful appendFile below to ensure sidecar is only updated when
509
- // the event is actually written.
510
- }
511
- let metadata: TeamEventMetadata = {
512
- seq,
513
- provenance: baseMetadata?.provenance ?? "team_runner",
514
- ...(baseMetadata?.parentEventId ? { parentEventId: baseMetadata.parentEventId } : {}),
515
- ...(baseMetadata?.attemptId ? { attemptId: baseMetadata.attemptId } : {}),
516
- ...(baseMetadata?.branchId ? { branchId: baseMetadata.branchId } : {}),
517
- ...(baseMetadata?.causationId ? { causationId: baseMetadata.causationId } : {}),
518
- ...(baseMetadata?.correlationId ? { correlationId: baseMetadata.correlationId } : {}),
519
- ...(baseMetadata?.sessionIdentity ? { sessionIdentity: baseMetadata.sessionIdentity } : {}),
520
- ...(baseMetadata?.ownership ? { ownership: baseMetadata.ownership } : {}),
521
- ...(baseMetadata?.nudgeId ? { nudgeId: baseMetadata.nudgeId } : {}),
522
- ...(baseMetadata?.confidence ? { confidence: baseMetadata.confidence } : {}),
523
- };
524
- const fullEvent: TeamEvent = {
525
- time: new Date().toISOString(),
526
- ...event,
527
- metadata,
528
- };
529
- if (baseMetadata?.fingerprint || TERMINAL_EVENT_TYPES.has(fullEvent.type)) {
530
- metadata = {
531
- ...metadata,
532
- fingerprint: baseMetadata?.fingerprint ?? computeEventFingerprint(fullEvent),
533
- };
534
- fullEvent.metadata = metadata;
535
- }
536
-
537
- // Overflow handling: same logic as sync path
538
- const isTerminal = TERMINAL_EVENT_TYPES.has(fullEvent.type);
539
- let skippedDueToSize = false;
540
- let fileStat: fs.Stats | undefined;
541
- try {
542
- fileStat = await fs.promises.stat(eventsPath).catch(() => undefined);
543
- } catch {
544
- /* file does not exist */
545
- }
546
- // FIND-10: track whether overflow handling modified the file so we can
547
- // reuse fileStat for the post-overflow size check (avoids redundant stat).
548
- let overflowHandled = false;
549
- if (!isTerminal && fileStat) {
550
- const stat = fileStat;
551
- if (stat.size > MAX_EVENTS_BYTES) {
552
- overflowHandled = true;
553
- try {
554
- compactEventLog(eventsPath);
555
- } catch (error) {
556
- logInternalError("event-log.immediate-compact", error, `eventsPath=${eventsPath}`);
557
- }
558
- let afterCompactStat: fs.Stats | undefined;
559
- try {
560
- afterCompactStat = await fs.promises.stat(eventsPath).catch(() => undefined);
561
- } catch {
562
- /* file does not exist */
563
- }
564
- if (afterCompactStat) {
565
- if (afterCompactStat.size > MAX_EVENTS_BYTES) {
566
- rotateEventLog(eventsPath);
567
- }
568
- }
569
- }
570
- }
571
- // FIND-10: collapse redundant stat. If no overflow handling occurred,
572
- // the file hasn't changed since fileStat — reuse it instead of re-stat'ing.
573
- let sizeCheckStat: fs.Stats | undefined;
574
- if (overflowHandled) {
575
- try {
576
- sizeCheckStat = await fs.promises.stat(eventsPath).catch(() => undefined);
577
- } catch {
578
- /* file does not exist */
579
- }
580
- } else {
581
- sizeCheckStat = fileStat;
582
- }
583
- try {
584
- if (sizeCheckStat && sizeCheckStat.size > MAX_EVENTS_BYTES) {
585
- logInternalError(
586
- "event-log.size-limit",
587
- new Error(`events file ${eventsPath} exceeds ${MAX_EVENTS_BYTES} bytes after compaction`),
588
- `eventsPath=${eventsPath}`,
589
- );
590
- skippedDueToSize = true;
591
- }
592
- } catch (error) {
593
- logInternalError("event-log.size-check", error, `eventsPath=${eventsPath}`);
594
- }
595
-
596
- // FIND-10: post-append stat captured from the same fd (non-worker path)
597
- // for reuse in the cache update below, avoiding a redundant path stat.
598
- let postAppendStat: fs.Stats | undefined;
599
- if (!skippedDueToSize) {
600
- const line = JSON.stringify(redactSecrets(fullEvent)) + "\n";
601
- // Phase 1.5: when worker atomic writer is enabled, append via worker.
602
- if (isWorkerAtomicWriterEnabled()) {
603
- await appendFileViaWorker(eventsPath, line);
604
- // Worker path: fsync via a separate open (worker manages its own fd).
605
- const fd = await fs.promises.open(eventsPath, "r+");
606
- try {
607
- await fd.sync();
608
- } finally {
609
- await fd.close();
610
- }
611
- } else {
612
- // FIND-10: single-fd append+fsync. Opens in append mode, writes,
613
- // fsyncs on the SAME fd, then closes — eliminating the separate
614
- // open("r+") + sync that previously doubled the fd count. The
615
- // fsync (seq-integrity protection) is preserved exactly: it still
616
- // closes the crash window between append and persistSequence.
617
- const fd = await fs.promises.open(eventsPath, "a");
618
- try {
619
- await fd.appendFile(line, "utf-8");
620
- await fd.sync();
621
- // FIND-10 R1 fix: the cache-optimization fd.stat() must NOT sit in the
622
- // seq-durability critical path. If it threw (rare — fd invalidated),
623
- // it would skip persistSequence below and reopen the seq-reuse
624
- // window the fsync just closed. Guard it; fall back to undefined
625
- // (the later cache-update takes a path stat instead).
626
- try {
627
- postAppendStat = await fd.stat();
628
- } catch {
629
- postAppendStat = undefined;
630
- }
631
- } finally {
632
- await fd.close();
633
- }
634
- }
635
- // FIX: Persist sequence AFTER successful appendFile to ensure sidecar
636
- // is only updated when the event is definitively written. If appendFile
637
- // threw, we would not reach here and the sidecar would not be updated,
638
- // preventing sequence reuse on restart.
639
- persistSequence(eventsPath, seq);
640
- }
641
- // FIND-10: track whether compaction happened after the append so the
642
- // cache-update stat can safely reuse postAppendStat (file unchanged).
643
- let compactedAfterAppend = false;
644
- if (appendCounter % 100 === 0 && needsRotation(eventsPath)) {
645
- compactedAfterAppend = true;
646
- try {
647
- compactEventLog(eventsPath);
648
- } catch (error) {
649
- logInternalError("event-log.rotation", error, `eventsPath=${eventsPath}`);
650
- }
651
- }
652
- try {
653
- emitFromTeamEvent(fullEvent);
654
- } catch (error) {
655
- logInternalError("event-log.emit", error);
656
- }
657
-
658
- // FIX: Sequence was persisted AFTER appendFile in the append block above.
659
- // Only update the cache here (the sidecar persist is already done).
660
- const finalSeq = fullEvent.metadata?.seq ?? 0;
661
- try {
662
- // FIND-10: reuse post-append fd stat when available and no compaction
663
- // happened after the append (file unchanged). Falls back to path stat
664
- // for the worker path, skipped events, or post-compaction cases.
665
- let statResult: fs.Stats | undefined;
666
- if (postAppendStat && !compactedAfterAppend) {
667
- statResult = postAppendStat;
668
- } else {
669
- try {
670
- statResult = await fs.promises.stat(eventsPath).catch(() => undefined);
671
- } catch {
672
- /* file may not exist */
673
- }
674
- }
675
- if (statResult) {
676
- if (sequenceCache.size >= MAX_SEQUENCE_CACHE_ENTRIES) {
677
- evictOldestSequenceCacheEntries();
678
- }
679
- sequenceCache.set(eventsPath, {
680
- size: statResult.size,
681
- mtimeMs: statResult.mtimeMs,
682
- seq: finalSeq,
683
- lastAccessMs: Date.now(),
684
- });
685
- }
686
- // Note: persistSequence is NOT called here again - it was already called
687
- // after the append to ensure the sidecar is current after the event is written.
688
- } catch (error) {
689
- logInternalError("event-log.persist-sequence", error, `eventsPath=${eventsPath}`);
690
- }
691
- return fullEvent;
692
- });
693
- const tail = next.then(
694
- () => {
695
- // Compare-and-delete: only remove our entry if it still points at our
696
- // tail promise. An older caller deleting unconditionally would wipe a
697
- // newer caller's promise, letting the next caller bypass serialization
698
- // -> duplicate seqs / interleaved appends.
699
- if (asyncQueues.get(queueKey) === tail) {
700
- asyncQueues.delete(queueKey);
701
- }
702
- },
703
- (error) => {
704
- // FIX: Wrap error handler in try-catch to ensure asyncQueues.delete
705
- // always runs, even if logging itself throws.
706
- try {
707
- logInternalError("event-log.async-queue", error, eventsPath);
708
- } catch {
709
- // logging failed — ensure queue is still cleaned up
710
- }
711
- // FIX: Reset queue to a resolved state instead of deleting it.
712
- // This prevents cascading failures where a single transient error
713
- // (e.g., ENOSPC) causes all subsequent events on the same path to fail.
714
- asyncQueues.set(queueKey, Promise.resolve());
715
- },
716
- );
717
- asyncQueues.set(queueKey, tail);
718
- return next;
719
- }
720
-
721
- /**
722
- * Body of `appendEvent` assuming the caller already holds
723
- * `withEventLogLockSync` for `eventsPath`. Used by `appendEventBuffered` to
724
- * write a whole batch of pending events under a single lock acquire.
725
- */
726
- /**
727
- * Batch variant used by the buffered flush path. Computes metadata for each
728
- * event, writes the whole batch in a single appendFileSync + fsync, persists
729
- * the sequence sidecar once with the last seq, and updates the sequence cache
730
- * once. Resolves each item with its finalized event (carrying the assigned
731
- * seq). This collapses N fsyncs into 1 for the buffered write path, which is
732
- * the entire point of buffering — the previous per-event fsync made buffer
733
- * coalescing useless and added ~30ms/event on tmpfs.
734
- */
735
- async function appendEventBatchInsideLock(eventsPath: string, queue: BufferedAppend[]): Promise<void> {
736
- if (queue.length === 0) return;
737
- fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
738
-
739
- // Pre-flight size check (mirrors appendEventInsideLock). We do it once for
740
- // the batch instead of once per event.
741
- try {
742
- if (fs.existsSync(eventsPath)) {
743
- const stat = fs.statSync(eventsPath);
744
- if (stat.size > MAX_EVENTS_BYTES) {
745
- try {
746
- const prepared = prepareCompaction(eventsPath);
747
- if (prepared) applyCompactionUnlocked(eventsPath, prepared);
748
- } catch (error) {
749
- logInternalError("event-log.batch-immediate-compact", error, `eventsPath=${eventsPath}`);
750
- }
751
- if (fs.existsSync(eventsPath) && fs.statSync(eventsPath).size > MAX_EVENTS_BYTES) {
752
- rotateEventLogUnlocked(eventsPath);
753
- }
754
- }
755
- }
756
- } catch (error) {
757
- logInternalError("event-log.batch-size-check", error, `eventsPath=${eventsPath}`);
758
- }
759
-
760
- // Phase 1: compute metadata + JSON lines for every event in the batch.
761
- // Initialize nextSeq ONCE from nextSequence (or the first event's baseMetadata.seq),
762
- // then increment locally for each subsequent event in the batch. Calling
763
- // nextSequence() per-event would re-read file stat/sidecar with no writes
764
- // in between — every call would see the same file state and return the same
765
- // seq, breaking the "unique monotonic seq" contract. The cache update +
766
- // persistSequence at the end refreshes the sidecar to the last assigned seq.
767
- // B7: use reserveSequence for atomic seq assignment across all paths.
768
- const startingSeq = queue[0]?.event.metadata?.seq ?? reserveSequence(eventsPath);
769
- let nextSeq = startingSeq;
770
- const finalized: { item: BufferedAppend; line: string; fullEvent: TeamEvent }[] = [];
771
- let lastSeq = 0;
772
- for (const item of queue) {
773
- const baseMetadata = item.event.metadata;
774
- const seq = baseMetadata?.seq ?? nextSeq++;
775
- let metadata: TeamEventMetadata = {
776
- seq,
777
- provenance: baseMetadata?.provenance ?? "team_runner",
778
- ...(baseMetadata?.parentEventId ? { parentEventId: baseMetadata.parentEventId } : {}),
779
- ...(baseMetadata?.attemptId ? { attemptId: baseMetadata.attemptId } : {}),
780
- ...(baseMetadata?.branchId ? { branchId: baseMetadata.branchId } : {}),
781
- ...(baseMetadata?.causationId ? { causationId: baseMetadata.causationId } : {}),
782
- ...(baseMetadata?.correlationId ? { correlationId: baseMetadata.correlationId } : {}),
783
- ...(baseMetadata?.sessionIdentity ? { sessionIdentity: baseMetadata.sessionIdentity } : {}),
784
- ...(baseMetadata?.ownership ? { ownership: baseMetadata.ownership } : {}),
785
- ...(baseMetadata?.nudgeId ? { nudgeId: baseMetadata.nudgeId } : {}),
786
- ...(baseMetadata?.confidence ? { confidence: baseMetadata.confidence } : {}),
787
- };
788
- const fullEvent: TeamEvent = {
789
- time: new Date().toISOString(),
790
- ...item.event,
791
- metadata,
792
- };
793
- if (baseMetadata?.fingerprint || TERMINAL_EVENT_TYPES.has(fullEvent.type)) {
794
- metadata = {
795
- ...metadata,
796
- fingerprint: baseMetadata?.fingerprint ?? computeEventFingerprint(fullEvent),
797
- };
798
- fullEvent.metadata = metadata;
799
- }
800
- finalized.push({ item, line: `${JSON.stringify(redactSecrets(fullEvent))}\n`, fullEvent });
801
- lastSeq = seq;
802
- }
803
- // B7: advance counter past the entire batch so next reserveSequence returns the correct value.
804
- advanceSequenceCounter(eventsPath, lastSeq);
805
-
806
- // Phase 2: single appendFileSync + single fsync + single persistSequence.
807
- // Before this fix, each event in the batch triggered its own fsync, which
808
- // was the dominant cost on tmpfs and CI runners.
809
- try {
810
- if (fs.existsSync(eventsPath) && fs.statSync(eventsPath).size > MAX_EVENTS_BYTES) {
811
- logInternalError(
812
- "event-log.size-limit",
813
- new Error(`events file ${eventsPath} exceeds ${MAX_EVENTS_BYTES} bytes after compaction`),
814
- `eventsPath=${eventsPath}`,
815
- );
816
- // Reject the batch — caller will surface the error per item.
817
- for (const { item } of finalized) item.reject(new Error("event log size limit exceeded"));
818
- return;
819
- }
820
- } catch (error) {
821
- logInternalError("event-log.batch-size-check-post", error, `eventsPath=${eventsPath}`);
822
- }
823
-
824
- fs.appendFileSync(eventsPath, finalized.map((f) => f.line).join(""), "utf-8");
825
- const fd = fs.openSync(eventsPath, "r+");
826
- try {
827
- fs.fsyncSync(fd);
828
- } catch {
829
- // EPERM on Windows CI: best-effort flush
830
- } finally {
831
- fs.closeSync(fd);
832
- }
833
- persistSequence(eventsPath, lastSeq);
834
-
835
- // Phase 3: cache update + resolve all promises.
836
- try {
837
- const stat = fs.statSync(eventsPath);
838
- if (sequenceCache.size >= MAX_SEQUENCE_CACHE_ENTRIES) {
839
- evictOldestSequenceCacheEntries();
840
- }
841
- sequenceCache.set(eventsPath, {
842
- size: stat.size,
843
- mtimeMs: stat.mtimeMs,
844
- seq: lastSeq,
845
- lastAccessMs: Date.now(),
846
- });
847
- } catch (error) {
848
- logInternalError("event-log.batch-cache-update", error, `eventsPath=${eventsPath}`);
849
- }
850
-
851
- for (const { item, fullEvent } of finalized) item.resolve(fullEvent);
852
- }
853
-
854
- function appendEventInsideLock(eventsPath: string, event: AppendTeamEvent): TeamEvent {
855
- fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
856
- const baseMetadata = event.metadata;
857
- // B7: use reserveSequence for atomic seq assignment across all paths.
858
- const explicitSeq = baseMetadata?.seq;
859
- const seq = explicitSeq ?? reserveSequence(eventsPath);
860
- if (explicitSeq !== undefined) advanceSequenceCounter(eventsPath, seq);
861
- let metadata: TeamEventMetadata = {
862
- seq,
863
- provenance: baseMetadata?.provenance ?? "team_runner",
864
- ...(baseMetadata?.parentEventId ? { parentEventId: baseMetadata.parentEventId } : {}),
865
- ...(baseMetadata?.attemptId ? { attemptId: baseMetadata.attemptId } : {}),
866
- ...(baseMetadata?.branchId ? { branchId: baseMetadata.branchId } : {}),
867
- ...(baseMetadata?.causationId ? { causationId: baseMetadata.causationId } : {}),
868
- ...(baseMetadata?.correlationId ? { correlationId: baseMetadata.correlationId } : {}),
869
- ...(baseMetadata?.sessionIdentity ? { sessionIdentity: baseMetadata.sessionIdentity } : {}),
870
- ...(baseMetadata?.ownership ? { ownership: baseMetadata.ownership } : {}),
871
- ...(baseMetadata?.nudgeId ? { nudgeId: baseMetadata.nudgeId } : {}),
872
- ...(baseMetadata?.confidence ? { confidence: baseMetadata.confidence } : {}),
873
- };
874
- const fullEvent: TeamEvent = {
875
- time: new Date().toISOString(),
876
- ...event,
877
- metadata,
878
- };
879
- if (baseMetadata?.fingerprint || TERMINAL_EVENT_TYPES.has(fullEvent.type)) {
880
- metadata = {
881
- ...metadata,
882
- fingerprint: baseMetadata?.fingerprint ?? computeEventFingerprint(fullEvent),
883
- };
884
- fullEvent.metadata = metadata;
885
- }
886
- // H1 fix: handle overflow before appending.
887
- // 1. Terminal events must always be persisted regardless of size.
888
- // 2. Non-terminal events exceeding MAX_EVENTS_BYTES trigger immediate compact.
889
- // 3. After compact, if still over limit, rotate.
890
- const isTerminal = TERMINAL_EVENT_TYPES.has(fullEvent.type);
891
- let skippedDueToSize = false;
892
- if (!isTerminal && fs.existsSync(eventsPath)) {
893
- const stat = fs.statSync(eventsPath);
894
- if (stat.size > MAX_EVENTS_BYTES) {
895
- // Try immediate compact (not waiting for counter % 100).
896
- // Round 24 (BUG 1): we are INSIDE withEventLogLockSync. Use the unlocked
897
- // apply/rotate cores — the locked variants would deadlock (mkdir lock
898
- // is not re-entrant → 5s timeout → compaction/rotation never ran →
899
- // unbounded log growth → events silently dropped past 50MB).
900
- try {
901
- const prepared = prepareCompaction(eventsPath);
902
- if (prepared) applyCompactionUnlocked(eventsPath, prepared);
903
- } catch (error) {
904
- logInternalError("event-log.immediate-compact", error, `eventsPath=${eventsPath}`);
905
- }
906
- // Check if still too large after compact — if so, rotate
907
- if (fs.existsSync(eventsPath)) {
908
- const afterCompact = fs.statSync(eventsPath);
909
- if (afterCompact.size > MAX_EVENTS_BYTES) {
910
- rotateEventLogUnlocked(eventsPath);
911
- }
912
- }
913
- }
914
- }
915
- try {
916
- if (fs.existsSync(eventsPath) && fs.statSync(eventsPath).size > MAX_EVENTS_BYTES) {
917
- // Only reach here for non-terminal events that still overflow after compact+rotate.
918
- // Log and mark as not appended.
919
- logInternalError(
920
- "event-log.size-limit",
921
- new Error(`events file ${eventsPath} exceeds ${MAX_EVENTS_BYTES} bytes after compaction`),
922
- `eventsPath=${eventsPath}`,
923
- );
924
- skippedDueToSize = true;
925
- }
926
- } catch (error) {
927
- logInternalError("event-log.size-check", error, `eventsPath=${eventsPath}`);
928
- }
929
- // seq is already computed above via reserveSequence — reuse it for persist/cache.
930
- // const seq declaration removed (B7: seq is now computed before metadata object).
931
- if (!skippedDueToSize) {
932
- fs.appendFileSync(eventsPath, `${JSON.stringify(redactSecrets(fullEvent))}\n`, "utf-8");
933
- // F3a: skip data fsync for non-terminal events. We still call `persistSequence`
934
- // below, which means the .seq sidecar might briefly outpace the actual data
935
- // on disk in a crash, but the event-reconstructor (`event-reconstructor.ts`)
936
- // already handles inconsistent-tail recovery (appends after the sidecar's
937
- // claimed sequence are simply ignored on a lossless recovery scan). We trade
938
- // 1 ms of fsync per event on Windows for informational events; terminal
939
- // events keep the strict window between append and persistSequence.
940
- if (isTerminal) {
941
- const fd = fs.openSync(eventsPath, "r+");
942
- try {
943
- fs.fsyncSync(fd);
944
- } catch {
945
- // EPERM on Windows CI: best-effort flush
946
- } finally {
947
- fs.closeSync(fd);
948
- }
949
- }
950
- // FIX: Persist sequence AFTER the event append to prevent sequence reuse
951
- // on crash. Only update the sidecar when the event is definitively written.
952
- persistSequence(eventsPath, seq);
953
- // FIX: Update cache AFTER append so cache and log are consistent with each other.
954
- // This matches the async path behavior where cache is updated after the append.
955
- // If a crash occurs after append but before cache update, the .seq file is
956
- // already correct and nextSequence() will return the correct value on restart.
957
- try {
958
- const stat = fs.statSync(eventsPath);
959
- if (sequenceCache.size >= MAX_SEQUENCE_CACHE_ENTRIES) {
960
- evictOldestSequenceCacheEntries();
961
- }
962
- sequenceCache.set(eventsPath, {
963
- size: stat.size,
964
- mtimeMs: stat.mtimeMs,
965
- seq,
966
- lastAccessMs: Date.now(),
967
- });
968
- } catch (error) {
969
- logInternalError("event-log.persist-sequence", error, `eventsPath=${eventsPath}`);
970
- }
971
- }
972
- appendCounter++;
973
- if (appendCounter % 100 === 0 && needsRotation(eventsPath)) {
974
- // Round 24 (BUG 1): we are INSIDE withEventLogLockSync here (called via
975
- // appendEventInsideLock). The mkdir lock is NOT re-entrant, so calling the
976
- // locked compactEventLog would deadlock → 5s timeout → compaction never
977
- // ran → unbounded log growth → events silently dropped past 50MB. Use the
978
- // unlocked apply path instead (lock already held).
979
- try {
980
- const prepared = prepareCompaction(eventsPath);
981
- if (prepared) applyCompactionUnlocked(eventsPath, prepared);
982
- } catch (error) {
983
- logInternalError("event-log.rotation", error, `eventsPath=${eventsPath}`);
984
- }
985
- }
986
- try {
987
- emitFromTeamEvent(fullEvent);
988
- } catch (error) {
989
- logInternalError("event-log.emit", error);
990
- }
991
- return fullEvent;
992
- }
993
-
994
- // 2.2 — Buffered append API. Caller queues events and they are flushed under
995
- // a single `withEventLogLockSync` acquire after `bufferingMs` ms. The seq
996
- // invariant is preserved because the flush still goes through
997
- // appendEventInsideLock sequentially.
998
- //
999
- // Caveat: events still in the buffer at process kill -9 are lost. Callers
1000
- // for whom durability is critical (lifecycle terminal events) should keep
1001
- // using `appendEvent`. Used opportunistically for high-frequency events
1002
- // like `task.progress` once integration tests cover crash semantics.
1003
- interface BufferedAppend {
1004
- event: AppendTeamEvent;
1005
- resolve: (event: TeamEvent) => void;
1006
- reject: (error: unknown) => void;
1007
- }
1008
- const bufferedQueues = new Map<string, BufferedAppend[]>();
1009
- const bufferedTimers = new Map<string, ReturnType<typeof setTimeout>>();
1010
- const DEFAULT_BUFFER_MS = 20;
1011
-
1012
- export function appendEventBuffered(eventsPath: string, event: AppendTeamEvent, bufferMs = DEFAULT_BUFFER_MS): Promise<TeamEvent> {
1013
- // FIX: Terminal events must bypass buffer to ensure they're written immediately.
1014
- // Previously, terminal events like task.failed could be lost on process crash.
1015
- if (TERMINAL_EVENT_TYPES.has(event.type)) {
1016
- // FIX: Flush any pending buffered events before writing terminal event
1017
- // to ensure durability of events that precede the terminal event in the
1018
- // same flush cycle. Without this, a kill -9 after terminal event write
1019
- // but before buffer flush would lose the buffered events.
1020
- if (bufferedQueues.has(eventsPath)) {
1021
- flushOneEventLogBuffer(eventsPath);
1022
- }
1023
- // For terminal events, write synchronously to ensure durability
1024
- return Promise.resolve(appendEvent(eventsPath, event));
1025
- }
1026
- return new Promise<TeamEvent>((resolve, reject) => {
1027
- const queue = bufferedQueues.get(eventsPath) ?? [];
1028
- queue.push({ event, resolve, reject });
1029
- bufferedQueues.set(eventsPath, queue);
1030
- if (!bufferedTimers.has(eventsPath)) {
1031
- // Wrap flush in async IIFE so the returned Promise is awaited (avoids
1032
- // "floating promise" warnings under --test-force-exit and prevents
1033
- // the timer from being treated as done before the flush actually
1034
- // completes its async work).
1035
- const timer = setTimeout(() => {
1036
- flushOneEventLogBuffer(eventsPath).catch((error) => {
1037
- logInternalError("event-log.buffered-flush", error, `eventsPath=${eventsPath}`);
1038
- });
1039
- }, bufferMs);
1040
- bufferedTimers.set(eventsPath, timer);
1041
- timer.unref();
1042
- }
1043
- });
1044
- }
1045
-
1046
- async function flushOneEventLogBuffer(eventsPath: string): Promise<void> {
1047
- const queue = bufferedQueues.get(eventsPath);
1048
- bufferedQueues.delete(eventsPath);
1049
- const timer = bufferedTimers.get(eventsPath);
1050
- // Timer is cleared in the finally block to ensure cleanup happens even on error
1051
- try {
1052
- if (!queue || queue.length === 0) return;
1053
-
1054
- // FIX (Round 14, H3): When truncating the queue, explicitly reject the
1055
- // dropped entries' promises. Previously `queue.splice()` silently
1056
- // discarded the oldest items, and their associated Promises were never
1057
- // resolved or rejected — causing callers to await forever and leaking
1058
- // memory. We now reject with a clear error so callers can fall back.
1059
- if (queue.length > 1000) {
1060
- const dropped = queue.splice(0, queue.length - 500);
1061
- overflowCounter++;
1062
- // FIX: Include first/last dropped event type and sequence number in error
1063
- // message to make debugging easier when events are dropped.
1064
- const firstDroppedMeta = dropped[0]?.event.metadata;
1065
- const lastDroppedMeta = dropped[dropped.length - 1]?.event.metadata;
1066
- logInternalError(
1067
- "event-log.buffer-overflow",
1068
- new Error(
1069
- `Buffer overflow #${overflowCounter}: Dropped ${dropped.length} events: first seq=${firstDroppedMeta?.seq} type=${dropped[0]?.event.type}, last seq=${lastDroppedMeta?.seq} type=${dropped[dropped.length - 1]?.event.type}`,
1070
- ),
1071
- `${eventsPath}: ${queue.length + dropped.length} entries > 1000 cap`,
1072
- );
1073
- for (const item of dropped) {
1074
- item.reject(
1075
- new Error(
1076
- `Event log buffer overflow: ${queue.length + dropped.length} entries > 1000 cap; oldest ${dropped.length} dropped to keep memory bounded; first dropped seq=${firstDroppedMeta?.seq} type=${dropped[0]?.event.type}`,
1077
- ),
1078
- );
1079
- }
1080
- }
1081
-
1082
- // FIX (Issue 2): Use async lock instead of withEventLogLockSync to avoid
1083
- // blocking the event loop. The sync lock uses sleepSync which blocks for
1084
- // up to 5s and prevents AbortSignal handlers from firing.
1085
- // FIX (P0 follow-up): Batch the file write + fsync + persistSequence across
1086
- // the whole queue. Previously each event triggered its own fsyncSync,
1087
- // turning 100 buffered events into 100 fsyncs (~3s on tmpfs). Now we do
1088
- // 1 appendFileSync + 1 fsync + 1 persistSequence for the whole batch.
1089
- await withEventLogLockAsync(eventsPath, async () => {
1090
- await appendEventBatchInsideLock(eventsPath, queue);
1091
- });
1092
- } catch (error) {
1093
- // Lock acquire failed — fail every queued item so callers can fall back.
1094
- if (queue) for (const item of queue) item.reject(error);
1095
- } finally {
1096
- bufferedTimers.delete(eventsPath);
1097
- }
1098
- }
1099
-
1100
- /** Asynchronously flush every queued buffered event across all paths. */
1101
- export async function flushEventLogBuffer(): Promise<void> {
1102
- for (const eventsPath of [...bufferedQueues.keys()]) await flushOneEventLogBuffer(eventsPath);
1103
- }
1104
-
1105
- /**
1106
- * EL-2: Synchronously flush every queued buffered event across all paths.
1107
- * Used by the `exit` / `uncaughtException` / `SIGTERM` / `SIGINT` handlers,
1108
- * which CANNOT await async work (process is terminating). The async
1109
- * flushEventLogBuffer() previously called from these handlers created
1110
- * floating promises that never resolved, and the process exited before
1111
- * any buffered events were written — losing all events buffered via
1112
- * appendEventBuffered (task.progress etc.). This sync variant writes the
1113
- * buffered batches using the sync event-log lock + appendFileSync + fsync
1114
- * + persistSequence, recovering buffered events before termination.
1115
- * In-flight asyncQueues (appendEventAsync writes already dispatched to
1116
- * the thread pool) remain best-effort and cannot be awaited on `exit` —
1117
- * we clear them to drop stale state. SIGKILL cannot be intercepted.
1118
- */
1119
- export function flushBufferedQueuesSync(): void {
1120
- for (const eventsPath of [...bufferedQueues.keys()]) {
1121
- const queue = bufferedQueues.get(eventsPath);
1122
- bufferedQueues.delete(eventsPath);
1123
- if (!queue || queue.length === 0) continue;
1124
- try {
1125
- withEventLogLockSync(eventsPath, () => {
1126
- // appendEventBatchInsideLock is declared async but its body is fully
1127
- // synchronous (fs.appendFileSync + fs.fsyncSync + persistSequence,
1128
- // no awaits). Invoking without await runs the body synchronously and
1129
- // returns a resolved Promise that we discard.
1130
- void appendEventBatchInsideLock(eventsPath, queue);
1131
- });
1132
- } catch (error) {
1133
- logInternalError("event-log.sync-flush", error, eventsPath);
1134
- }
1135
- }
1136
- for (const eventsPath of [...bufferedTimers.keys()]) bufferedTimers.delete(eventsPath);
1137
- }
1138
-
1139
- /**
1140
- * Schedule an async event append without waiting for the result.
1141
- * Uses the non-blocking async queue to avoid blocking the event loop.
1142
- * Use only for events whose return value is ignored (high-frequency `task.progress`).
1143
- * Errors are logged via logInternalError.
1144
- */
1145
- export function appendEventFireAndForget(eventsPath: string, event: AppendTeamEvent): void {
1146
- appendEventAsync(eventsPath, event).catch((error) => logInternalError("event-log.fire-and-forget", error, eventsPath));
1147
- }
1148
-
1149
- // Auto-flush on process exit so buffered events do not silently leak.
1150
- // Defense-in-depth: SIGTERM/SIGINT use setImmediate so the handler returns
1151
- // immediately and the main thread is not blocked by sync I/O.
1152
- // FIX (P0 follow-up): Only call flushEventLogBuffer() / drainAsyncQueues() if
1153
- // there is actually pending work. Calling them unconditionally creates a new
1154
- // floating Promise that the test runner detects under --test-force-exit,
1155
- // failing tests with "Promise resolution is still pending but the event loop
1156
- // has already resolved" even when the test body completed cleanly.
1157
- process.on("exit", () => {
1158
- // EL-2: synchronously flush buffered events before the process terminates.
1159
- // `exit` is sync-only and cannot await async work; the previous async
1160
- // flushEventLogBuffer()/drainAsyncQueues() created floating promises that
1161
- // never resolved, and the process exited before any buffered events were
1162
- // written. flushBufferedQueuesSync uses the sync lock + appendFileSync +
1163
- // fsync + persistSequence to recover buffered events.
1164
- flushBufferedQueuesSync();
1165
- asyncQueues.clear();
1166
- });
1167
-
1168
- // FIX (P0 follow-up): Drain buffered events on `beforeExit` (async-aware).
1169
- // The `exit` handler above is sync-only and cannot await the flush, leaving
1170
- // pending promises that the test runner detects under --test-force-exit as
1171
- // "Promise resolution is still pending but the event loop has already
1172
- // resolved". `beforeExit` fires when the event loop drains naturally and
1173
- // supports async handlers, so we can await the flush here and let the loop
1174
- // drain cleanly before process.exit() is called.
1175
- process.on("beforeExit", async () => {
1176
- if (bufferedQueues.size > 0) {
1177
- try {
1178
- await flushEventLogBuffer();
1179
- } catch {
1180
- /* best-effort */
1181
- }
1182
- }
1183
- if (asyncQueues.size > 0) {
1184
- try {
1185
- await drainAsyncQueues();
1186
- } catch {
1187
- /* best-effort */
1188
- }
1189
- }
1190
- });
1191
- process.on("SIGTERM", () => setImmediate(() => flushBufferedQueuesSync()));
1192
- process.on("SIGINT", () => setImmediate(() => flushBufferedQueuesSync()));
1193
- // FIX (Issue 1): Handle uncaught exceptions to flush buffered events before
1194
- // the process terminates. The async queues use promise chains that will be
1195
- // abandoned on crash; clearing the map prevents memory leaks and stale state.
1196
- // Note: SIGKILL (kill -9) cannot be intercepted and is not handled.
1197
- process.on("uncaughtException", (error) => {
1198
- // EL-2: synchronously flush buffered events before re-throwing (which
1199
- // terminates the process). The previous async flushEventLogBuffer() +
1200
- // drainAsyncQueues() couldn't complete before process exit; use the sync
1201
- // variant to recover buffered events.
1202
- flushBufferedQueuesSync();
1203
- asyncQueues.clear();
1204
- // Re-throw to preserve default uncaught exception behavior (process exit)
1205
- throw error;
1206
- });
1207
-
1208
- export function readEvents(eventsPath: string): TeamEvent[] {
1209
- if (!fs.existsSync(eventsPath)) return [];
1210
- return fs
1211
- .readFileSync(eventsPath, "utf-8")
1212
- .split("\n")
1213
- .map((line) => line.trim())
1214
- .filter(Boolean)
1215
- .flatMap((line) => {
1216
- try {
1217
- return [JSON.parse(line) as TeamEvent];
1218
- } catch {
1219
- return [];
1220
- }
1221
- });
1222
- }
1223
-
1224
- export interface EventCursorOptions {
1225
- sinceSeq?: number;
1226
- limit?: number;
1227
- fromByteOffset?: number;
1228
- }
1229
-
1230
- export interface EventCursorResult {
1231
- events: TeamEvent[];
1232
- nextSeq: number;
1233
- total: number;
1234
- nextByteOffset?: number;
1235
- }
1236
-
1237
- function positiveInteger(value: number | undefined): number | undefined {
1238
- return value !== undefined && Number.isInteger(value) && value >= 0 ? value : undefined;
1239
- }
1240
-
1241
- export function readEventsCursor(eventsPath: string, options: EventCursorOptions = {}): EventCursorResult {
1242
- // Incremental byte-offset path: read only new bytes since last known offset
1243
- if (options.fromByteOffset !== undefined) {
1244
- const byteOffset = positiveInteger(options.fromByteOffset) ?? 0;
1245
- const initialState: IncrementalReadState = { byteOffset, lineCount: 0 };
1246
- const { items, state: newState, eof } = readJsonlSince<TeamEvent>(eventsPath, initialState);
1247
- const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
1248
- const filtered = items.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
1249
- const limit = positiveInteger(options.limit);
1250
- const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
1251
- const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
1252
- return {
1253
- events,
1254
- nextSeq: returnedMaxSeq,
1255
- total: filtered.length,
1256
- nextByteOffset: newState.byteOffset,
1257
- };
1258
- }
1259
-
1260
- // FIND-05 default path: byte-level tail read (last 4MB) instead of
1261
- // full-file read. Bounds CPU to O(tail bytes) instead of O(total
1262
- // events). The legacy readEvents() full parse path is preserved for
1263
- // callers that explicitly need the full history (e.g. tests that
1264
- // assert exact contents) and as a small-file fallback.
1265
- //
1266
- // The 5000-event tail cap and the "event-log.cursor-full-read"
1267
- // warning are preserved. A separate cursor-tail-truncated warning is
1268
- // emitted whenever the file exceeds the 4MB tail budget, signalling
1269
- // that a prefix was dropped and callers should pass fromByteOffset for
1270
- // streaming reads.
1271
- const TAIL_BYTES = 4 * 1024 * 1024; // 4 MB
1272
- const TAIL_EVENT_CAP = 5000;
1273
- const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
1274
- const limit = positiveInteger(options.limit);
1275
-
1276
- const tail = readJsonlTail<TeamEvent>(eventsPath, TAIL_BYTES);
1277
- let all = tail.items;
1278
- if (tail.truncated) {
1279
- logInternalError("event-log.cursor-tail-truncated", {
1280
- eventsPath,
1281
- returned: all.length,
1282
- tailBytes: TAIL_BYTES,
1283
- });
1284
- }
1285
- if (all.length > TAIL_EVENT_CAP) {
1286
- logInternalError(
1287
- "event-log.cursor-full-read",
1288
- new Error(`readEventsCursor tail read dropped events from a larger log; pass fromByteOffset for incremental reads`),
1289
- `eventsPath=${eventsPath}`,
1290
- );
1291
- all = all.slice(-TAIL_EVENT_CAP);
1292
- }
1293
- const filtered = all.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
1294
- const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
1295
- const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
1296
- return { events, nextSeq: returnedMaxSeq, total: filtered.length };
1297
- }
1298
-
1299
- export function dedupeTerminalEvents(events: TeamEvent[]): TeamEvent[] {
1300
- const seen = new Set<string>();
1301
- const output: TeamEvent[] = [];
1302
- for (const event of events) {
1303
- const fingerprint = event.metadata?.fingerprint;
1304
- if (fingerprint && TERMINAL_EVENT_TYPES.has(event.type)) {
1305
- if (seen.has(fingerprint)) continue;
1306
- seen.add(fingerprint);
1307
- }
1308
- output.push(event);
1309
- }
1310
- return output;
1311
- }