pi-crew 0.9.68 → 0.10.2

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 (253) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/NOTICE.md +21 -0
  3. package/README.md +44 -2
  4. package/agents/analyst.md +1 -1
  5. package/agents/cold-verifier.md +3 -1
  6. package/agents/critic.md +1 -1
  7. package/agents/executor.md +1 -1
  8. package/agents/explorer.md +1 -1
  9. package/agents/planner.md +1 -1
  10. package/agents/reviewer.md +1 -1
  11. package/agents/security-reviewer.md +1 -1
  12. package/agents/test-engineer.md +1 -1
  13. package/agents/verifier.md +1 -1
  14. package/agents/writer.md +1 -1
  15. package/dist/index.mjs +68113 -60774
  16. package/docs/README.md +2 -0
  17. package/docs/actions-reference.md +31 -0
  18. package/docs/commands-reference.md +17 -6
  19. package/docs/resource-formats.md +13 -0
  20. package/package.json +4 -2
  21. package/schema.json +503 -91
  22. package/scripts/resource-sampler.mjs +36 -2
  23. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  24. package/skills/widget-rendering/SKILL.md +7 -7
  25. package/src/agents/agent-config.ts +2 -1
  26. package/src/agents/discover-agents.ts +23 -14
  27. package/src/config/config-merge.ts +183 -0
  28. package/src/config/config-validation.ts +687 -0
  29. package/src/config/config.ts +22 -864
  30. package/src/config/defaults.ts +43 -2
  31. package/src/config/drift-detector.ts +1 -1
  32. package/src/config/env-vars.ts +691 -0
  33. package/src/config/role-tools.ts +11 -9
  34. package/src/config/sanitize-project-config.ts +172 -0
  35. package/src/config/types.ts +49 -1
  36. package/src/extension/async-notifier.ts +25 -2
  37. package/src/extension/crew-cleanup.ts +13 -0
  38. package/src/extension/crew-vibes/config.ts +2 -1
  39. package/src/extension/crew-vibes/footer.ts +19 -0
  40. package/src/extension/crew-vibes/index.ts +11 -1
  41. package/src/extension/plan-orchestrate.ts +132 -0
  42. package/src/extension/register.ts +8 -0
  43. package/src/extension/registration/command-registration.ts +1 -0
  44. package/src/extension/registration/commands/dashboard.ts +158 -0
  45. package/src/extension/registration/commands/index.ts +35 -0
  46. package/src/extension/registration/commands/manage.ts +303 -0
  47. package/src/extension/registration/commands/run.ts +228 -0
  48. package/src/extension/registration/commands/shared.ts +639 -0
  49. package/src/extension/registration/commands/status.ts +60 -0
  50. package/src/extension/registration/commands.ts +13 -1224
  51. package/src/extension/registration/foreground-run-controller.ts +10 -2
  52. package/src/extension/registration/lifecycle-handlers.ts +178 -17
  53. package/src/extension/registration/runtime-cleanup.ts +23 -5
  54. package/src/extension/registration/subagent-tools.ts +218 -9
  55. package/src/extension/registration/team-tool.ts +5 -1
  56. package/src/extension/registration/ui.ts +5 -4
  57. package/src/extension/rpc-hmac.ts +5 -3
  58. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  59. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  60. package/src/extension/team-tool/api/task-claims.ts +109 -40
  61. package/src/extension/team-tool/cancel.ts +84 -50
  62. package/src/extension/team-tool/dispatch/index.ts +1 -0
  63. package/src/extension/team-tool/dispatch/run.ts +4 -1
  64. package/src/extension/team-tool/doctor.ts +103 -1
  65. package/src/extension/team-tool/orchestrate.ts +66 -1
  66. package/src/extension/team-tool/plans.ts +192 -0
  67. package/src/extension/team-tool/respond.ts +197 -65
  68. package/src/extension/team-tool/run-deadline.ts +35 -3
  69. package/src/extension/team-tool/run-intent.ts +63 -0
  70. package/src/extension/team-tool/run.ts +74 -20
  71. package/src/extension/team-tool/status.ts +84 -26
  72. package/src/extension/team-tool.ts +11 -2
  73. package/src/hooks/registry.ts +1 -6
  74. package/src/i18n.ts +9 -0
  75. package/src/prompt/prompt-runtime.ts +521 -2
  76. package/src/prompt/worker-events-channel.ts +173 -0
  77. package/src/runtime/README.md +8 -8
  78. package/src/runtime/async-runner.ts +7 -3
  79. package/src/runtime/background-runner.ts +42 -14
  80. package/src/runtime/broker/broker-issuer.ts +9 -2
  81. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  82. package/src/runtime/broker/crew-broker.ts +838 -10
  83. package/src/runtime/broker/wait-status-cache.ts +157 -0
  84. package/src/runtime/budget-enforcement.ts +281 -0
  85. package/src/runtime/child-pi/child-pi-constants.ts +8 -0
  86. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  87. package/src/runtime/child-pi/child-pi-streams.ts +21 -1
  88. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  89. package/src/runtime/child-pi/child-pi.ts +97 -201
  90. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  91. package/src/runtime/crew-agent-records.ts +259 -14
  92. package/src/runtime/delegate-spawn.ts +148 -0
  93. package/src/runtime/detached-run-results.ts +90 -0
  94. package/src/runtime/deterministic-ast.ts +2 -1
  95. package/src/runtime/dispatch-batch.ts +945 -0
  96. package/src/runtime/finalize-run.ts +557 -0
  97. package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
  98. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
  99. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  100. package/src/runtime/group-join.ts +11 -125
  101. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  102. package/src/runtime/merge-gate.ts +32 -10
  103. package/src/runtime/merge-loop.ts +130 -0
  104. package/src/runtime/model/model-budget-summary.ts +53 -0
  105. package/src/runtime/model/model-fallback.ts +36 -2
  106. package/src/runtime/model/pi-args.ts +10 -0
  107. package/src/runtime/model/provider-extensions.ts +10 -0
  108. package/src/runtime/orphan-worker-registry.ts +1 -1
  109. package/src/runtime/output/output-validator.ts +45 -0
  110. package/src/runtime/parent-guard.ts +3 -1
  111. package/src/runtime/peer-dep.ts +2 -1
  112. package/src/runtime/per-write-validator.ts +0 -5
  113. package/src/runtime/pi-spawn.ts +61 -15
  114. package/src/runtime/plan-approval.ts +125 -0
  115. package/src/runtime/plan-replan.ts +151 -0
  116. package/src/runtime/process-status.ts +16 -1
  117. package/src/runtime/recovery/checkpoint.ts +0 -18
  118. package/src/runtime/recovery/crash-recovery.ts +111 -46
  119. package/src/runtime/run-tracker.ts +77 -10
  120. package/src/runtime/scheduler-context.ts +98 -0
  121. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  122. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  123. package/src/runtime/scheduling/nested-slots.ts +70 -0
  124. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  125. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  126. package/src/runtime/settings-store.ts +219 -0
  127. package/src/runtime/spawn-policy.ts +217 -0
  128. package/src/runtime/stale-reconciler.ts +87 -6
  129. package/src/runtime/subagent-manager.ts +25 -1
  130. package/src/runtime/task-output-context.ts +230 -9
  131. package/src/runtime/task-packet.ts +23 -1
  132. package/src/runtime/task-runner/child-executor.ts +106 -7
  133. package/src/runtime/task-runner/post-execution.ts +125 -1
  134. package/src/runtime/task-runner/pre-execution.ts +39 -1
  135. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  136. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  137. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  138. package/src/runtime/task-runner/state-helpers.ts +26 -24
  139. package/src/runtime/task-runner.ts +11 -0
  140. package/src/runtime/team-runner.ts +132 -1673
  141. package/src/runtime/verification/spec-sandbox.ts +255 -0
  142. package/src/runtime/verification/verification-gates.ts +3 -2
  143. package/src/runtime/verification/verification-worktree.ts +2 -1
  144. package/src/runtime/workflow-phase-advance.ts +100 -0
  145. package/src/runtime/workspace-tree.ts +9 -0
  146. package/src/schema/config-schema.ts +66 -26
  147. package/src/schema/sensitive-config-paths.ts +64 -0
  148. package/src/schema/team-tool-schema.ts +13 -3
  149. package/src/state/README.md +4 -10
  150. package/src/state/atomic-write.ts +20 -3
  151. package/src/state/contracts.ts +38 -0
  152. package/src/state/coordination/mailbox.ts +12 -2
  153. package/src/state/event-log/cursor.ts +223 -0
  154. package/src/state/event-log/event-log-rotation.ts +12 -4
  155. package/src/state/event-log/event-log.ts +152 -369
  156. package/src/state/event-log/sequence-cache.ts +373 -0
  157. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  158. package/src/state/stores/active-run-registry.ts +3 -2
  159. package/src/state/stores/manifest-io.ts +237 -0
  160. package/src/state/stores/ownership-map.ts +162 -0
  161. package/src/state/stores/plan-store.ts +241 -0
  162. package/src/state/stores/run-cache.ts +0 -90
  163. package/src/state/stores/spec-store.ts +189 -0
  164. package/src/state/stores/state-store.ts +139 -232
  165. package/src/state/types.ts +199 -0
  166. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  167. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  168. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  169. package/src/ui/dock-footer.ts +49 -0
  170. package/src/ui/heartbeat-aggregator.ts +9 -1
  171. package/src/ui/inline-panel/agent-pane.ts +375 -0
  172. package/src/ui/inline-panel/agent-transcript.ts +338 -0
  173. package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
  174. package/src/ui/inline-panel/crew-editor.ts +192 -0
  175. package/src/ui/inline-panel/index.ts +290 -0
  176. package/src/ui/inline-panel/panel-rows.ts +37 -0
  177. package/src/ui/inline-panel/panel-selection.ts +157 -0
  178. package/src/ui/inline-panel/panel-store.ts +111 -0
  179. package/src/ui/inline-panel/view-session-store.ts +36 -0
  180. package/src/ui/keybinding-map.ts +54 -13
  181. package/src/ui/pi-ui-compat.ts +9 -0
  182. package/src/ui/powerbar-publisher.ts +52 -1
  183. package/src/ui/run-dashboard.ts +31 -5
  184. package/src/ui/run-snapshot-cache.ts +57 -30
  185. package/src/ui/snapshot-types.ts +6 -1
  186. package/src/ui/widget/index.ts +176 -22
  187. package/src/ui/widget/task-list.ts +198 -0
  188. package/src/ui/widget/widget-formatters.ts +240 -4
  189. package/src/ui/widget/widget-renderer.ts +243 -38
  190. package/src/ui/widget/widget-types.ts +11 -0
  191. package/src/utils/child-process-shield.ts +106 -0
  192. package/src/utils/file-coalescer.ts +0 -4
  193. package/src/utils/fs-errno.ts +66 -0
  194. package/src/utils/fs-watch.ts +1 -1
  195. package/src/utils/internal-error.ts +3 -1
  196. package/src/utils/paths.ts +11 -3
  197. package/src/utils/redaction.ts +7 -0
  198. package/src/utils/safe-abort.ts +45 -0
  199. package/src/utils/task-name-generator.ts +1 -8
  200. package/src/workflows/discover-workflows.ts +20 -2
  201. package/src/workflows/validate-workflow.ts +7 -1
  202. package/src/workflows/workflow-config.ts +17 -0
  203. package/src/workflows/workflow-serializer.ts +3 -0
  204. package/src/worktree/worktree-manager.ts +22 -0
  205. package/workflows/default.workflow.md +36 -26
  206. package/workflows/strict-fast-fix.workflow.md +26 -0
  207. package/src/agents/agent-search.ts +0 -98
  208. package/src/benchmark/benchmark-runner.ts +0 -313
  209. package/src/benchmark/feedback-loop.ts +0 -73
  210. package/src/config/resilient-parser.ts +0 -117
  211. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  212. package/src/extension/result-watcher.ts +0 -139
  213. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  214. package/src/observability/metric-retention.ts +0 -64
  215. package/src/runtime/compaction/compaction-summary.ts +0 -278
  216. package/src/runtime/errors/crew-errors.ts +0 -162
  217. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  218. package/src/runtime/loop-gates.ts +0 -128
  219. package/src/runtime/metric-parser.ts +0 -36
  220. package/src/runtime/output/stream-preview.ts +0 -184
  221. package/src/runtime/output/tool-progress.ts +0 -278
  222. package/src/runtime/phase-tracker.ts +0 -385
  223. package/src/runtime/pipeline-runner.ts +0 -523
  224. package/src/runtime/process/process-lifecycle.ts +0 -491
  225. package/src/runtime/recovery/retry-runner.ts +0 -330
  226. package/src/runtime/run-drift.ts +0 -219
  227. package/src/runtime/task-quality.ts +0 -199
  228. package/src/runtime/task-runner/run-projection.ts +0 -128
  229. package/src/runtime/verification/post-checks.ts +0 -142
  230. package/src/state/coordination/schedule.ts +0 -166
  231. package/src/state/event-log/jsonl-writer.ts +0 -115
  232. package/src/state/hook-instinct-bridge.ts +0 -94
  233. package/src/state/hook-integrations.ts +0 -51
  234. package/src/state/session-state-map.ts +0 -51
  235. package/src/state/stores/blob-store.ts +0 -308
  236. package/src/state/stores/instinct-store.ts +0 -275
  237. package/src/state/stores/observation-store.ts +0 -176
  238. package/src/state/tiered-eval.ts +0 -480
  239. package/src/state/types-eval.ts +0 -58
  240. package/src/tools/safe-bash-extension.ts +0 -54
  241. package/src/tools/safe-bash.ts +0 -505
  242. package/src/ui/agent-management-overlay.ts +0 -160
  243. package/src/ui/crew-footer.ts +0 -102
  244. package/src/ui/crew-select-list.ts +0 -114
  245. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  246. package/src/ui/transcript-entries.ts +0 -256
  247. package/src/utils/conflict-detect.ts +0 -721
  248. package/src/utils/fingerprint.ts +0 -180
  249. package/src/utils/gh-protocol.ts +0 -556
  250. package/src/utils/project-detector.ts +0 -160
  251. package/src/utils/sse-parser.ts +0 -131
  252. package/src/workflows/cost-estimator.ts +0 -34
  253. package/src/workflows/intermediate-store.ts +0 -166
@@ -4,18 +4,20 @@ import * as path from "node:path";
4
4
  import { DEFAULT_EVENT_LOG } from "../../config/defaults.ts";
5
5
  import { errors } from "../../errors.ts";
6
6
  import { emitFromTeamEvent } from "../../ui/run-event-bus.ts";
7
- import { type IncrementalReadState, readJsonlSince, readJsonlTail } from "../../utils/incremental-reader.ts";
8
7
  import { logInternalError } from "../../utils/internal-error.ts";
9
8
  import { redactSecrets } from "../../utils/redaction.ts";
10
9
  import { sleep, sleepSync } from "../../utils/sleep.ts";
11
10
  import { atomicWriteFile } from "../atomic-write.ts";
11
+ import { applyCompactionUnlocked, needsRotation, prepareCompaction, rotateEventLogUnlocked } from "./event-log-rotation.ts";
12
12
  import {
13
- applyCompactionUnlocked,
14
- currentGeneration,
15
- needsRotation,
16
- prepareCompaction,
17
- rotateEventLogUnlocked,
18
- } from "./event-log-rotation.ts";
13
+ advanceSequenceCounter,
14
+ evictOldestSequenceCacheEntries,
15
+ MAX_SEQUENCE_CACHE_ENTRIES,
16
+ persistSequenceMonotonic,
17
+ reserveSequence,
18
+ reserveSequenceUnderLockAsync,
19
+ sequenceCache,
20
+ } from "./sequence-cache.ts";
19
21
  import { appendFileViaWorker, isWorkerAtomicWriterEnabled } from "./worker-atomic-writer.ts";
20
22
 
21
23
  export type TeamEventProvenance = "live_worker" | "test" | "healthcheck" | "replay" | "api" | "background" | "team_runner";
@@ -48,6 +50,14 @@ export interface TeamEventMetadata {
48
50
  appended?: boolean;
49
51
  fingerprint?: string;
50
52
  confidence?: "low" | "medium" | "high";
53
+ /** R17-S1 (Phase 3.8): set to `true` when the append was SKIPPED because the
54
+ * events file exceeded MAX_EVENTS_BYTES after compaction+rotation and the
55
+ * event is non-terminal. The returned TeamEvent then represents an event
56
+ * that was NOT persisted (and NOT emitted to the run-event-bus) — callers
57
+ * can detect the silent drop instead of treating it as success. Never
58
+ * written to disk (skipped events are not appended), so this is not an
59
+ * on-disk format change. */
60
+ skippedDueToSize?: boolean;
51
61
  }
52
62
 
53
63
  export interface TeamEvent {
@@ -67,8 +77,6 @@ export type AppendTeamEvent = Omit<TeamEvent, "time" | "metadata"> & {
67
77
  const TERMINAL_EVENT_TYPES = new Set<string>(DEFAULT_EVENT_LOG.terminalEventTypes);
68
78
  const MAX_EVENTS_BYTES = 50 * 1024 * 1024;
69
79
 
70
- const sequenceCache = new Map<string, { size: number; mtimeMs: number; seq: number; lastAccessMs: number }>();
71
- const MAX_SEQUENCE_CACHE_ENTRIES = 256;
72
80
  // P0-2: per-eventsPath append counter. Previously a single module-global shared
73
81
  // across all runs AND never incremented on the async path, so the async rotation
74
82
  // gate (`0 % 100 === 0`) was always true → needsRotation ran on EVERY async append
@@ -205,223 +213,6 @@ export function withEventLogLockSync<T>(eventsPath: string, fn: () => T, options
205
213
  }
206
214
  }
207
215
 
208
- function evictOldestSequenceCacheEntries(): void {
209
- // FIX: Evict by lastAccessMs (access time), not insertion order.
210
- // Frequently accessed entries should be retained even if older.
211
- const toEvict = Math.ceil(MAX_SEQUENCE_CACHE_ENTRIES / 2);
212
- // Sort entries by lastAccessMs ascending (oldest first)
213
- const entries = [...sequenceCache.entries()].sort((a, b) => a[1].lastAccessMs - b[1].lastAccessMs);
214
- // Evict the oldest half
215
- for (let i = 0; i < toEvict && i < entries.length; i++) {
216
- sequenceCache.delete(entries[i][0]);
217
- }
218
- }
219
-
220
- /** @internal — exported for sequence-cache LRU testing (Round 19). */
221
- export function __test__sequenceCacheSize(): number {
222
- return sequenceCache.size;
223
- }
224
-
225
- /** @internal — seed an entry into the sequence cache for testing. */
226
- export function __test__seedSequenceCache(eventsPath: string, lastAccessMs: number): void {
227
- sequenceCache.set(eventsPath, {
228
- size: 1,
229
- mtimeMs: 0,
230
- seq: 0,
231
- lastAccessMs,
232
- });
233
- }
234
-
235
- /** @internal — expose eviction for testing. */
236
- export function __test__evictOldestSequenceCacheEntries(): void {
237
- evictOldestSequenceCacheEntries();
238
- }
239
-
240
- /** @internal — clear the sequence cache. */
241
- export function __test__clearSequenceCache(): void {
242
- sequenceCache.clear();
243
- }
244
-
245
- /** @internal — clear the in-process seqCounters Map so nextSequence seeds
246
- * fresh from the sidecar/file (simulates a process restart for testing). */
247
- export function __test__clearSeqCounters(): void {
248
- seqCounters.clear();
249
- }
250
-
251
- /** @internal — the raw nextSequence for testing (forces re-seed from disk
252
- * by requiring the caller to have already cleared both caches). */
253
- export function __test__nextSequence(eventsPath: string): number {
254
- return nextSequence(eventsPath);
255
- }
256
-
257
- /** @internal — the max sequence cache entries bound. */
258
- export const MAX_SEQUENCE_CACHE_ENTRIES_VALUE = MAX_SEQUENCE_CACHE_ENTRIES;
259
-
260
- export function sequencePath(eventsPath: string): string {
261
- return `${eventsPath}.seq`;
262
- }
263
-
264
- function parseSequence(raw: string): number | undefined {
265
- const value = Number.parseInt(raw.trim(), 10);
266
- return Number.isInteger(value) && value >= 0 ? value : undefined;
267
- }
268
-
269
- export function scanSequence(eventsPath: string): number {
270
- if (!fs.existsSync(eventsPath)) return 0;
271
- let max = 0;
272
- let skipped = 0;
273
- for (const line of fs.readFileSync(eventsPath, "utf-8").split("\n")) {
274
- if (!line.trim()) continue;
275
- try {
276
- const event = JSON.parse(line) as TeamEvent;
277
- max = Math.max(max, event.metadata?.seq ?? 0);
278
- } catch {
279
- skipped++;
280
- }
281
- }
282
- if (skipped > 0) {
283
- logInternalError("event-log.scanSequence.corrupt_lines", undefined, `${eventsPath}: skipped ${skipped} corrupt line(s)`);
284
- }
285
- return max;
286
- }
287
-
288
- function readStoredSequence(eventsPath: string): number | undefined {
289
- try {
290
- return parseSequence(fs.readFileSync(sequencePath(eventsPath), "utf-8"));
291
- } catch {
292
- return undefined;
293
- }
294
- }
295
-
296
- function nextSequence(eventsPath: string): number {
297
- if (!fs.existsSync(eventsPath)) return 1;
298
- const stat = fs.statSync(eventsPath);
299
- const cached = sequenceCache.get(eventsPath);
300
- if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
301
- return cached.seq + 1;
302
- }
303
- // FIX: Trust the sidecar seq file if it exists and the file is non-empty.
304
- // Explicitly check for file shrinkage (stat.size < cached.size) to trigger
305
- // re-scan when rotation or compaction has occurred.
306
- const stored = readStoredSequence(eventsPath);
307
- const fileShrunk = cached && stat.size < cached.size;
308
- if (stored !== undefined && !fileShrunk) {
309
- // Trust the sidecar, but guard against a REGRESSED sidecar (e.g. the
310
- // async path persisted a lower seq, rolling the sidecar back below the
311
- // file's true max). Take max with a full scan so a regressed sidecar
312
- // cannot produce a duplicate sequence number (EL-1 regression guard).
313
- // NOTE (ST-12): a full scan here is acceptable because all three append
314
- // paths now allocate seqs via reserveSequence/reserveSequenceUnderLock
315
- // (ST-5: re-read sidecar under lock + max with in-process counter);
316
- // nextSequence is only consulted for seeding/test helpers, NOT on the
317
- // production append path. ST-12's perf goal (avoid 4MB scan on first
318
- // append) is therefore met by ST-5 at the reserveSequence layer.
319
- const fileMax = scanSequence(eventsPath);
320
- const safeSeq = Math.max(stored, fileMax);
321
- sequenceCache.set(eventsPath, {
322
- size: stat.size,
323
- mtimeMs: stat.mtimeMs,
324
- seq: safeSeq,
325
- lastAccessMs: Date.now(),
326
- });
327
- return safeSeq + 1;
328
- }
329
- const current = scanSequence(eventsPath);
330
- sequenceCache.set(eventsPath, {
331
- size: stat.size,
332
- mtimeMs: stat.mtimeMs,
333
- seq: current,
334
- lastAccessMs: Date.now(),
335
- });
336
- persistSequence(eventsPath, current);
337
- return current + 1;
338
- }
339
-
340
- function persistSequence(eventsPath: string, seq: number): void {
341
- try {
342
- // P0-4: the .seq sidecar is disposable (event-reconstructor tolerates an
343
- // inconsistent tail); best-effort avoids 2 fsyncs per event append.
344
- atomicWriteFile(sequencePath(eventsPath), String(seq), { durability: "best-effort" });
345
- } catch (error) {
346
- logInternalError("event-log.persist-sequence-file", error, `eventsPath=${eventsPath}`);
347
- }
348
- }
349
-
350
- // B7: single in-process monotonic sequence counter per eventsPath. The three
351
- // append paths — sync appendEvent (withEventLogLockSync file lock), buffered
352
- // flush (asyncLocks promise chain), and direct appendEventAsync (asyncQueues
353
- // promise chain) — use DIFFERENT locks, so the old read-sidecar / compute /
354
- // persist-sidecar sequence logic in nextSequence() raced ACROSS paths and
355
- // produced duplicate sequence numbers (observed live: distinct events sharing
356
- // a seq; no data loss — only the counter collided). A single in-process counter
357
- // makes assignment atomic (JS is single-threaded); persistSequence() keeps the
358
- // sidecar durable for crash recovery across restarts.
359
- const seqCounters = new Map<string, number>();
360
- // H6 (2026-08-10): FIFO cap — mirrors appendCounters (APPEND_COUNTER_MAX_ENTRIES = 256)
361
- // and agentEventSeqCache (cap at :434). Without this, a long-lived parent pi process
362
- // that observes many runs (dev sessions with hundreds of background runs over a week)
363
- // accumulates one entry per distinct eventsPath forever. Eviction is safe: the next
364
- // append re-seeds from the `.seq` sidecar via reserveSequenceUnderLock.
365
- const SEQ_COUNTERS_MAX_ENTRIES = 256;
366
-
367
- /** Atomically reserve the next sequence number for `eventsPath`.
368
- *
369
- * ST-5 (v0.9.56): previously this seeded the in-process `seqCounters` counter
370
- * ONCE per process (reading the `.seq` sidecar a single time via
371
- * `nextSequence`) and then served every subsequent call purely from that
372
- * process-local counter. Two processes that both seeded before either had
373
- * persisted ended up sharing the same counter base -> duplicate sequence
374
- * numbers -> `sinceSeq` streaming readers silently dropped the second event.
375
- * The async path was already fixed via `reserveSequenceUnderLock` (which
376
- * re-reads the sidecar every call); the sync (`appendEvent`) and buffered
377
- * (`appendEventBatchInsideLock`) paths were NOT.
378
- *
379
- * Now ALL three append paths share one body: re-read the authoritative `.seq`
380
- * sidecar on EVERY call and take `max(sidecar, inProcess)` so a counter that
381
- * lags behind a sidecar advanced by another process can never assign a
382
- * regressed (duplicate) seq. Every call site already holds a cross-process
383
- * file lock (`withEventLogLockSync` for sync/buffered, `withEventLogLockAsync`
384
- * for async), so the sidecar re-read is race-free within each lock class. */
385
- function reserveSequence(eventsPath: string): number {
386
- return reserveSequenceUnderLock(eventsPath);
387
- }
388
-
389
- /** Keep the in-process counter monotonic w.r.t. an explicitly-provided seq
390
- * (e.g. baseMetadata.seq) so a later auto-assigned seq never collides with it. */
391
- function advanceSequenceCounter(eventsPath: string, seq: number): void {
392
- const last = seqCounters.get(eventsPath);
393
- if (last === undefined || seq > last) {
394
- seqCounters.set(eventsPath, seq);
395
- enforceSeqCountersCap();
396
- }
397
- }
398
-
399
- /** H6: FIFO eviction when the seqCounters map exceeds its bounded size. */
400
- function enforceSeqCountersCap(): void {
401
- if (seqCounters.size > SEQ_COUNTERS_MAX_ENTRIES) {
402
- const oldest = seqCounters.keys().next().value;
403
- if (oldest !== undefined) seqCounters.delete(oldest);
404
- }
405
- }
406
-
407
- /** C-01: Reserve sequence INSIDE the cross-process lock. Reads the authoritative
408
- * sidecar (.seq file) for the last seq persisted by ANY process, ensuring
409
- * cross-process uniqueness. Falls back to scanSequence if no sidecar exists.
410
- * The in-process seqCounters is kept monotonic via Math.max for defensive
411
- * consistency with any in-process sequencing that hasn't been persisted yet. */
412
- function reserveSequenceUnderLock(eventsPath: string): number {
413
- let stored = readStoredSequence(eventsPath);
414
- if (stored === undefined) {
415
- stored = scanSequence(eventsPath);
416
- }
417
- const inProcess = seqCounters.get(eventsPath) ?? 0;
418
- const last = Math.max(stored, inProcess);
419
- const next = last + 1;
420
- seqCounters.set(eventsPath, next);
421
- enforceSeqCountersCap();
422
- return next;
423
- }
424
-
425
216
  export function computeEventFingerprint(event: Pick<TeamEvent, "type" | "runId" | "taskId" | "data">): string {
426
217
  return createHash("sha256")
427
218
  .update(
@@ -451,6 +242,10 @@ export function appendEvent(eventsPath: string, event: AppendTeamEvent): TeamEve
451
242
  }
452
243
 
453
244
  // --- Async write queue (non-blocking alternative to withEventLogLockSync) ---
245
+ // R5-L4: safety cap for error-path reset entries (see the appendEventAsync
246
+ // error handler) — a path whose queue is reset after an error and then never
247
+ // re-accessed would otherwise retain its entry until process exit.
248
+ const MAX_ASYNC_QUEUES = 256;
454
249
  const asyncQueues = new Map<string, Promise<unknown>>();
455
250
 
456
251
  // --- Async lock for flush operations (non-blocking alternative to withEventLogLockSync) ---
@@ -493,6 +288,16 @@ async function drainAsyncQueues(): Promise<void> {
493
288
  * shared sidecar (extremely unlikely scenario — workers write to their own
494
289
  * run-scoped events.jsonl, not the parent's).
495
290
  *
291
+ * R16-B1/R17 CORRECTION (Phase 3.6): the "extremely unlikely" claim above
292
+ * was REFUTED empirically — 14 parent-side sync appendEvent sites write the
293
+ * live run's eventsPath while child async appends run, and Round 17 measured
294
+ * 75% duplicate events / 527 duplicate seqs over 3000 events with the natural
295
+ * (unwidened) rate. The family split itself is UNCHANGED (merging the two
296
+ * lock dirs would reintroduce the sleepSync-vs-async-timer deadlock this
297
+ * comment documents); the seq race is instead closed by the shared
298
+ * `.seqlock` (L3) around the .seq sidecar reservation in
299
+ * reserveSequenceUnderLock/Async — see the R16-B1 block near persistSequence.
300
+ *
496
301
  * NOT re-entrant: callers inside this lock must use unlocked compaction
497
302
  * variants (prepareCompaction + applyCompactionUnlocked, rotateEventLogUnlocked)
498
303
  * to avoid self-deadlock. */
@@ -597,15 +402,6 @@ async function withEventLogLockAsync<T>(
597
402
  }
598
403
  }
599
404
 
600
- /** Reset event log mode (for testing only). */
601
- export function resetEventLogMode(): void {
602
- asyncQueues.clear();
603
- asyncLocks.clear();
604
- // B7: clear in-process sequence counters alongside async state so tests
605
- // don't leak seq state between runs.
606
- seqCounters.clear();
607
- }
608
-
609
405
  /**
610
406
  * Append an event to the event log using non-blocking async I/O.
611
407
  *
@@ -644,16 +440,25 @@ export async function appendEventAsync(eventsPath: string, event: AppendTeamEven
644
440
  // the event is definitively written. If appendFile fails, the sidecar is
645
441
  // not updated and nextSequence() will re-scan on next call, returning the
646
442
  // correct value without reuse.
443
+ // R16-B1 (Phase 3.6) PARTIAL SUPERSESSION: the sidecar IS now advanced at
444
+ // reservation time INSIDE the .seqlock (advance-on-reserve, gap-not-dup —
445
+ // see reserveSequenceLocked); the post-append persist below is kept and is
446
+ // idempotent (writes the same value the reservation already persisted).
647
447
  const baseMetadata = event.metadata;
648
448
  let seq: number;
649
449
  if (baseMetadata?.seq !== undefined) {
650
450
  seq = baseMetadata.seq;
651
451
  advanceSequenceCounter(eventsPath, seq);
652
452
  } else {
653
- seq = reserveSequenceUnderLock(eventsPath);
654
- // NOTE: We do NOT call persistSequence here. It will be called AFTER
655
- // successful appendFile below to ensure sidecar is only updated when
656
- // the event is actually written.
453
+ // R16-B1 (Phase 3.6): reservation now acquires the shared `.seqlock`
454
+ // (async acquire wrapper — never sleepSync on the event loop) and
455
+ // PERSISTS the reserved seq inside it (advance-on-reserve). The old
456
+ // "do NOT call persistSequence here" note is superseded: without the
457
+ // in-lock persist, two processes in different family locks both read
458
+ // sidecar=N and both reserve N+1 (Round 17: 75% dup events). A failed
459
+ // append after reservation now yields a seq GAP (tolerated by the
460
+ // event-reconstructor's lossless recovery), never a duplicate.
461
+ seq = await reserveSequenceUnderLockAsync(eventsPath);
657
462
  }
658
463
  let metadata: TeamEventMetadata = {
659
464
  seq,
@@ -711,7 +516,18 @@ export async function appendEventAsync(eventsPath: string, event: AppendTeamEven
711
516
  }
712
517
  if (afterCompactStat) {
713
518
  if (afterCompactStat.size > MAX_EVENTS_BYTES) {
714
- rotateEventLogUnlocked(eventsPath);
519
+ // R17-S1 (Phase 3.8): a failed rotation leaves the file over the
520
+ // limit — the next non-terminal appends would be silently skipped.
521
+ // Check the boolean (previously ignored) and surface severity
522
+ // "error" so the chain signals at EVERY step.
523
+ if (!rotateEventLogUnlocked(eventsPath)) {
524
+ logInternalError(
525
+ "event-log.rotate-failed",
526
+ new Error(`event log rotation failed; file remains over ${MAX_EVENTS_BYTES} bytes`),
527
+ `eventsPath=${eventsPath}`,
528
+ "error",
529
+ );
530
+ }
715
531
  }
716
532
  }
717
533
  }
@@ -734,8 +550,17 @@ export async function appendEventAsync(eventsPath: string, event: AppendTeamEven
734
550
  "event-log.size-limit",
735
551
  new Error(`events file ${eventsPath} exceeds ${MAX_EVENTS_BYTES} bytes after compaction`),
736
552
  `eventsPath=${eventsPath}`,
553
+ // R17-S1 (Phase 3.8, Round 18 escalation to HIGH): this was default
554
+ // severity "debug" (PI_TEAMS_DEBUG-gated) → fully silent drop in
555
+ // production. "error" always emits.
556
+ "error",
737
557
  );
738
558
  skippedDueToSize = true;
559
+ // R17-S1: surface the drop on the returned event itself — resolve
560
+ // with an indicator instead of as-if-persisted success. Never
561
+ // throw on the skip path (Round 18 caution), and never emit a
562
+ // non-persisted event to the run-event-bus (gate below).
563
+ metadata.skippedDueToSize = true;
739
564
  }
740
565
  } catch (error) {
741
566
  logInternalError("event-log.size-check", error, `eventsPath=${eventsPath}`);
@@ -784,11 +609,14 @@ export async function appendEventAsync(eventsPath: string, event: AppendTeamEven
784
609
  await fd.close();
785
610
  }
786
611
  }
787
- // FIX: Persist sequence AFTER successful appendFile to ensure sidecar
788
- // is only updated when the event is definitively written. If appendFile
789
- // threw, we would not reach here and the sidecar would not be updated,
790
- // preventing sequence reuse on restart.
791
- persistSequence(eventsPath, seq);
612
+ // R16-B1 (Phase 3.6): advance-on-reserve already persisted this exact
613
+ // value INSIDE the .seqlock — re-persisting it here (outside the lock)
614
+ // can REGRESS the sidecar after another process reserved further
615
+ // (observed: re-reserved duplicate seqs in the b9 cross-process bench).
616
+ // Only EXPLICIT (pre-assigned) seqs bypassed the reservation — persist
617
+ // those, monotonically and under the .seqlock, so a lower explicit seq
618
+ // can never roll the sidecar back either.
619
+ if (baseMetadata?.seq !== undefined) persistSequenceMonotonic(eventsPath, seq);
792
620
  }
793
621
  // FIND-10: track whether compaction happened after the append so the
794
622
  // cache-update stat can safely reuse postAppendStat (file unchanged).
@@ -802,10 +630,15 @@ export async function appendEventAsync(eventsPath: string, event: AppendTeamEven
802
630
  logInternalError("event-log.rotation", error, `eventsPath=${eventsPath}`);
803
631
  }
804
632
  }
805
- try {
806
- emitFromTeamEvent(fullEvent);
807
- } catch (error) {
808
- logInternalError("event-log.emit", error);
633
+ // R17-S1 (Phase 3.8): gate the UI-bus emit on !skippedDueToSize — the bus
634
+ // must never receive events that were NOT persisted (live-UI vs
635
+ // reconstructed-state divergence, Round 18 reviewer addendum).
636
+ if (!skippedDueToSize) {
637
+ try {
638
+ emitFromTeamEvent(fullEvent);
639
+ } catch (error) {
640
+ logInternalError("event-log.emit", error);
641
+ }
809
642
  }
810
643
 
811
644
  // FIX: Sequence was persisted AFTER appendFile in the append block above.
@@ -871,6 +704,13 @@ export async function appendEventAsync(eventsPath: string, event: AppendTeamEven
871
704
  // FIX: Reset queue to a resolved state instead of deleting it.
872
705
  // This prevents cascading failures where a single transient error
873
706
  // (e.g., ENOSPC) causes all subsequent events on the same path to fail.
707
+ // R5-L4: cap the retained reset entries — evict oldest (Map insertion
708
+ // order) so paths that error and are never re-accessed cannot leak.
709
+ while (asyncQueues.size >= MAX_ASYNC_QUEUES) {
710
+ const oldestKey = asyncQueues.keys().next().value;
711
+ if (oldestKey === undefined) break;
712
+ asyncQueues.delete(oldestKey);
713
+ }
874
714
  asyncQueues.set(queueKey, Promise.resolve());
875
715
  },
876
716
  );
@@ -911,7 +751,17 @@ async function appendEventBatchInsideLock(eventsPath: string, queue: BufferedApp
911
751
  logInternalError("event-log.batch-immediate-compact", error, `eventsPath=${eventsPath}`);
912
752
  }
913
753
  if (fs.existsSync(eventsPath) && fs.statSync(eventsPath).size > MAX_EVENTS_BYTES) {
914
- rotateEventLogUnlocked(eventsPath);
754
+ // R17-S1 (Phase 3.8): check the rotation boolean (previously ignored)
755
+ // and surface severity "error" — a failed rotation leaves the file
756
+ // over the limit and the batch below will be rejected as a result.
757
+ if (!rotateEventLogUnlocked(eventsPath)) {
758
+ logInternalError(
759
+ "event-log.rotate-failed",
760
+ new Error(`event log rotation failed; file remains over ${MAX_EVENTS_BYTES} bytes`),
761
+ `eventsPath=${eventsPath}`,
762
+ "error",
763
+ );
764
+ }
915
765
  }
916
766
  }
917
767
  }
@@ -927,7 +777,11 @@ async function appendEventBatchInsideLock(eventsPath: string, queue: BufferedApp
927
777
  // seq, breaking the "unique monotonic seq" contract. The cache update +
928
778
  // persistSequence at the end refreshes the sidecar to the last assigned seq.
929
779
  // B7: use reserveSequence for atomic seq assignment across all paths.
930
- const startingSeq = queue[0]?.event.metadata?.seq ?? reserveSequence(eventsPath);
780
+ // R16-B1 (Phase 3.6): reserve the WHOLE batch range (count = queue.length)
781
+ // under the .seqlock in one acquire — the locally incremented nextSeq below
782
+ // then stays inside the reserved range even when explicit baseMetadata.seq
783
+ // values skip some of the reserved slots (those become gaps, never dups).
784
+ const startingSeq = queue[0]?.event.metadata?.seq ?? reserveSequence(eventsPath, queue.length);
931
785
  let nextSeq = startingSeq;
932
786
  const finalized: { item: BufferedAppend; line: string; fullEvent: TeamEvent }[] = [];
933
787
  let lastSeq = 0;
@@ -992,7 +846,11 @@ async function appendEventBatchInsideLock(eventsPath: string, queue: BufferedApp
992
846
  } finally {
993
847
  fs.closeSync(fd);
994
848
  }
995
- persistSequence(eventsPath, lastSeq);
849
+ // R16-B1 (Phase 3.6): monotonic persist under the .seqlock — the reservation
850
+ // already persisted the batch range end; this covers explicit (pre-assigned)
851
+ // seqs that may exceed it and can never REGRESS the sidecar (the old bare
852
+ // persistSequence could write a lower lastSeq over a higher reserved value).
853
+ persistSequenceMonotonic(eventsPath, lastSeq);
996
854
 
997
855
  // Phase 3: cache update + resolve all promises.
998
856
  try {
@@ -1069,7 +927,18 @@ function appendEventInsideLock(eventsPath: string, event: AppendTeamEvent): Team
1069
927
  if (fs.existsSync(eventsPath)) {
1070
928
  const afterCompact = fs.statSync(eventsPath);
1071
929
  if (afterCompact.size > MAX_EVENTS_BYTES) {
1072
- rotateEventLogUnlocked(eventsPath);
930
+ // R17-S1 (Phase 3.8): check the rotation boolean (previously
931
+ // ignored) and surface severity "error" — a failed rotation
932
+ // leaves the file over the limit and the size check below will
933
+ // silently skip every subsequent non-terminal append.
934
+ if (!rotateEventLogUnlocked(eventsPath)) {
935
+ logInternalError(
936
+ "event-log.rotate-failed",
937
+ new Error(`event log rotation failed; file remains over ${MAX_EVENTS_BYTES} bytes`),
938
+ `eventsPath=${eventsPath}`,
939
+ "error",
940
+ );
941
+ }
1073
942
  }
1074
943
  }
1075
944
  }
@@ -1082,8 +951,18 @@ function appendEventInsideLock(eventsPath: string, event: AppendTeamEvent): Team
1082
951
  "event-log.size-limit",
1083
952
  new Error(`events file ${eventsPath} exceeds ${MAX_EVENTS_BYTES} bytes after compaction`),
1084
953
  `eventsPath=${eventsPath}`,
954
+ // R17-S1 (Phase 3.8, Round 18 escalation to HIGH): was default "debug"
955
+ // (PI_TEAMS_DEBUG-gated) → fully silent drop in production. "error"
956
+ // always emits.
957
+ "error",
1085
958
  );
1086
959
  skippedDueToSize = true;
960
+ // R17-S1: surface the drop on the RETURNED event (never throw — callers
961
+ // run inside withRunLockSync and a throw would fail cancel/save critical
962
+ // sections; Round 18 fix-design caution). The returned TeamEvent carries
963
+ // skippedDueToSize=true so live-path callers can detect the drop; the
964
+ // event is NOT persisted and NOT emitted (gate below).
965
+ metadata.skippedDueToSize = true;
1087
966
  }
1088
967
  } catch (error) {
1089
968
  logInternalError("event-log.size-check", error, `eventsPath=${eventsPath}`);
@@ -1111,7 +990,13 @@ function appendEventInsideLock(eventsPath: string, event: AppendTeamEvent): Team
1111
990
  }
1112
991
  // FIX: Persist sequence AFTER the event append to prevent sequence reuse
1113
992
  // on crash. Only update the sidecar when the event is definitively written.
1114
- persistSequence(eventsPath, seq);
993
+ // R16-B1 (Phase 3.6): advance-on-reserve already persisted this exact
994
+ // value INSIDE the .seqlock — re-persisting it here (outside the lock)
995
+ // can REGRESS the sidecar after another process reserved further
996
+ // (observed: re-reserved duplicate seqs in the b9 cross-process bench).
997
+ // Only EXPLICIT (pre-assigned) seqs bypassed the reservation — persist
998
+ // those, monotonically and under the .seqlock.
999
+ if (explicitSeq !== undefined) persistSequenceMonotonic(eventsPath, seq);
1115
1000
  // FIX: Update cache AFTER append so cache and log are consistent with each other.
1116
1001
  // This matches the async path behavior where cache is updated after the append.
1117
1002
  // If a crash occurs after append but before cache update, the .seq file is
@@ -1144,10 +1029,15 @@ function appendEventInsideLock(eventsPath: string, event: AppendTeamEvent): Team
1144
1029
  logInternalError("event-log.rotation", error, `eventsPath=${eventsPath}`);
1145
1030
  }
1146
1031
  }
1147
- try {
1148
- emitFromTeamEvent(fullEvent);
1149
- } catch (error) {
1150
- logInternalError("event-log.emit", error);
1032
+ // R17-S1 (Phase 3.8): gate the UI-bus emit on !skippedDueToSize — the bus
1033
+ // must never receive events that were NOT persisted (live-UI vs
1034
+ // reconstructed-state divergence, Round 18 reviewer addendum).
1035
+ if (!skippedDueToSize) {
1036
+ try {
1037
+ emitFromTeamEvent(fullEvent);
1038
+ } catch (error) {
1039
+ logInternalError("event-log.emit", error);
1040
+ }
1151
1041
  }
1152
1042
  return fullEvent;
1153
1043
  }
@@ -1369,117 +1259,6 @@ process.on("uncaughtException", (error) => {
1369
1259
  throw error;
1370
1260
  });
1371
1261
 
1372
- export function readEvents(eventsPath: string): TeamEvent[] {
1373
- if (!fs.existsSync(eventsPath)) return [];
1374
- return fs
1375
- .readFileSync(eventsPath, "utf-8")
1376
- .split("\n")
1377
- .map((line) => line.trim())
1378
- .filter(Boolean)
1379
- .flatMap((line) => {
1380
- try {
1381
- return [JSON.parse(line) as TeamEvent];
1382
- } catch {
1383
- return [];
1384
- }
1385
- });
1386
- }
1387
-
1388
- export interface EventCursorOptions {
1389
- sinceSeq?: number;
1390
- limit?: number;
1391
- fromByteOffset?: number;
1392
- /** R-03: generation the caller captured on its previous read. When set, a
1393
- * mismatch with the live generation signals the file was rotated/truncated
1394
- * and the byte offset is stale — the cursor resets to 0 so the new file is
1395
- * re-read from its start instead of missing post-rotation events. */
1396
- generation?: number;
1397
- }
1398
-
1399
- export interface EventCursorResult {
1400
- events: TeamEvent[];
1401
- nextSeq: number;
1402
- total: number;
1403
- nextByteOffset?: number;
1404
- /** R-03: live generation of the events file at read time. Callers doing
1405
- * streaming byte-offset reads should echo this back as `generation` on the
1406
- * next call so rotation is detected and the cursor resets. */
1407
- generation?: number;
1408
- }
1409
-
1410
- function positiveInteger(value: number | undefined): number | undefined {
1411
- return value !== undefined && Number.isInteger(value) && value >= 0 ? value : undefined;
1412
- }
1413
-
1414
- export function readEventsCursor(eventsPath: string, options: EventCursorOptions = {}): EventCursorResult {
1415
- // Incremental byte-offset path: read only new bytes since last known offset
1416
- if (options.fromByteOffset !== undefined) {
1417
- // R-03: detect file rotation/truncation via the generation sidecar BEFORE
1418
- // reusing the byte offset. If the file was rotated since the caller last
1419
- // read, it was truncated to empty (pre-rotation content archived to
1420
- // `<eventsPath>.<ts>.archive.jsonl`) and is growing again from 0 — the
1421
- // caller's offset now points past EOF, so post-rotation events would be
1422
- // silently missed. Reset to offset 0 to re-read the current file from its
1423
- // start. Re-reading from 0 re-delivers no previously-returned events:
1424
- // those live in the archive, not the (now fresh) current file.
1425
- const liveGen = currentGeneration(eventsPath);
1426
- const staleCursor = options.generation !== undefined && options.generation !== liveGen;
1427
- const byteOffset = staleCursor ? 0 : (positiveInteger(options.fromByteOffset) ?? 0);
1428
- const initialState: IncrementalReadState = { byteOffset, lineCount: 0 };
1429
- const { items, state: newState, eof } = readJsonlSince<TeamEvent>(eventsPath, initialState);
1430
- const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
1431
- const filtered = items.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
1432
- const limit = positiveInteger(options.limit);
1433
- const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
1434
- const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
1435
- return {
1436
- events,
1437
- nextSeq: returnedMaxSeq,
1438
- total: filtered.length,
1439
- nextByteOffset: newState.byteOffset,
1440
- generation: liveGen,
1441
- };
1442
- }
1443
-
1444
- // FIND-05 default path: byte-level tail read (last 4MB) instead of
1445
- // full-file read. Bounds CPU to O(tail bytes) instead of O(total
1446
- // events). The legacy readEvents() full parse path is preserved for
1447
- // callers that explicitly need the full history (e.g. tests that
1448
- // assert exact contents) and as a small-file fallback.
1449
- //
1450
- // The 5000-event tail cap and the "event-log.cursor-full-read"
1451
- // warning are preserved. A separate cursor-tail-truncated warning is
1452
- // emitted whenever the file exceeds the 4MB tail budget, signalling
1453
- // that a prefix was dropped and callers should pass fromByteOffset for
1454
- // streaming reads.
1455
- const TAIL_BYTES = 4 * 1024 * 1024; // 4 MB
1456
- const TAIL_EVENT_CAP = 5000;
1457
- const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
1458
- const limit = positiveInteger(options.limit);
1459
-
1460
- const tail = readJsonlTail<TeamEvent>(eventsPath, TAIL_BYTES);
1461
- let all = tail.items;
1462
- if (tail.truncated) {
1463
- logInternalError("event-log.cursor-tail-truncated", {
1464
- eventsPath,
1465
- returned: all.length,
1466
- tailBytes: TAIL_BYTES,
1467
- });
1468
- }
1469
- if (all.length > TAIL_EVENT_CAP) {
1470
- logInternalError(
1471
- "event-log.cursor-full-read",
1472
- new Error(`readEventsCursor tail read dropped events from a larger log; pass fromByteOffset for incremental reads`),
1473
- `eventsPath=${eventsPath}`,
1474
- );
1475
- all = all.slice(-TAIL_EVENT_CAP);
1476
- }
1477
- const filtered = all.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
1478
- const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
1479
- const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
1480
- return { events, nextSeq: returnedMaxSeq, total: filtered.length };
1481
- }
1482
-
1483
1262
  export function dedupeTerminalEvents(events: TeamEvent[]): TeamEvent[] {
1484
1263
  const seen = new Set<string>();
1485
1264
  const output: TeamEvent[] = [];
@@ -1493,3 +1272,7 @@ export function dedupeTerminalEvents(events: TeamEvent[]): TeamEvent[] {
1493
1272
  }
1494
1273
  return output;
1495
1274
  }
1275
+
1276
+ // --- Re-export shim (Phase 2.4): moved modules stay reachable from this path ---
1277
+ export * from "./cursor.ts";
1278
+ export * from "./sequence-cache.ts";