agentfootprint 2.14.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/dist/cache/CacheDecisionSubflow.js +3 -1
  2. package/dist/cache/CacheDecisionSubflow.js.map +1 -1
  3. package/dist/conventions.js +3 -2
  4. package/dist/conventions.js.map +1 -1
  5. package/dist/core/Agent.js +69 -23
  6. package/dist/core/Agent.js.map +1 -1
  7. package/dist/core/LLMCall.js +44 -5
  8. package/dist/core/LLMCall.js.map +1 -1
  9. package/dist/core/RunnerBase.js +173 -0
  10. package/dist/core/RunnerBase.js.map +1 -1
  11. package/dist/core/agent/buildAgentChart.js +12 -2
  12. package/dist/core/agent/buildAgentChart.js.map +1 -1
  13. package/dist/core/runner.js +4 -3
  14. package/dist/core/runner.js.map +1 -1
  15. package/dist/core/slots/buildMessagesSlot.js +1 -1
  16. package/dist/core/slots/buildMessagesSlot.js.map +1 -1
  17. package/dist/core/slots/buildSystemPromptSlot.js +1 -1
  18. package/dist/core/slots/buildSystemPromptSlot.js.map +1 -1
  19. package/dist/core/slots/buildThinkingSubflow.js +1 -1
  20. package/dist/core/slots/buildThinkingSubflow.js.map +1 -1
  21. package/dist/core/slots/buildToolsSlot.js +3 -1
  22. package/dist/core/slots/buildToolsSlot.js.map +1 -1
  23. package/dist/core/translator.js +32 -0
  24. package/dist/core/translator.js.map +1 -0
  25. package/dist/core-flow/Conditional.js +73 -10
  26. package/dist/core-flow/Conditional.js.map +1 -1
  27. package/dist/core-flow/Loop.js +60 -16
  28. package/dist/core-flow/Loop.js.map +1 -1
  29. package/dist/core-flow/Parallel.js +241 -92
  30. package/dist/core-flow/Parallel.js.map +1 -1
  31. package/dist/core-flow/Sequence.js +51 -8
  32. package/dist/core-flow/Sequence.js.map +1 -1
  33. package/dist/esm/cache/CacheDecisionSubflow.js +3 -1
  34. package/dist/esm/cache/CacheDecisionSubflow.js.map +1 -1
  35. package/dist/esm/conventions.js +3 -2
  36. package/dist/esm/conventions.js.map +1 -1
  37. package/dist/esm/core/Agent.js +69 -23
  38. package/dist/esm/core/Agent.js.map +1 -1
  39. package/dist/esm/core/LLMCall.js +44 -5
  40. package/dist/esm/core/LLMCall.js.map +1 -1
  41. package/dist/esm/core/RunnerBase.js +173 -0
  42. package/dist/esm/core/RunnerBase.js.map +1 -1
  43. package/dist/esm/core/agent/buildAgentChart.js +12 -2
  44. package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
  45. package/dist/esm/core/runner.js +4 -3
  46. package/dist/esm/core/runner.js.map +1 -1
  47. package/dist/esm/core/slots/buildMessagesSlot.js +1 -1
  48. package/dist/esm/core/slots/buildMessagesSlot.js.map +1 -1
  49. package/dist/esm/core/slots/buildSystemPromptSlot.js +1 -1
  50. package/dist/esm/core/slots/buildSystemPromptSlot.js.map +1 -1
  51. package/dist/esm/core/slots/buildThinkingSubflow.js +1 -1
  52. package/dist/esm/core/slots/buildThinkingSubflow.js.map +1 -1
  53. package/dist/esm/core/slots/buildToolsSlot.js +3 -1
  54. package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
  55. package/dist/esm/core/translator.js +31 -0
  56. package/dist/esm/core/translator.js.map +1 -0
  57. package/dist/esm/core-flow/Conditional.js +73 -10
  58. package/dist/esm/core-flow/Conditional.js.map +1 -1
  59. package/dist/esm/core-flow/Loop.js +60 -16
  60. package/dist/esm/core-flow/Loop.js.map +1 -1
  61. package/dist/esm/core-flow/Parallel.js +242 -93
  62. package/dist/esm/core-flow/Parallel.js.map +1 -1
  63. package/dist/esm/core-flow/Sequence.js +51 -8
  64. package/dist/esm/core-flow/Sequence.js.map +1 -1
  65. package/dist/esm/index.js +1 -0
  66. package/dist/esm/index.js.map +1 -1
  67. package/dist/esm/lib/injection-engine/buildInjectionEngineSubflow.js +1 -1
  68. package/dist/esm/lib/injection-engine/buildInjectionEngineSubflow.js.map +1 -1
  69. package/dist/esm/memory/causal/snapshotPipeline.js +6 -2
  70. package/dist/esm/memory/causal/snapshotPipeline.js.map +1 -1
  71. package/dist/esm/memory/pipeline/auto.js +2 -2
  72. package/dist/esm/memory/pipeline/auto.js.map +1 -1
  73. package/dist/esm/memory/pipeline/default.js +4 -2
  74. package/dist/esm/memory/pipeline/default.js.map +1 -1
  75. package/dist/esm/memory/pipeline/ephemeral.js +3 -1
  76. package/dist/esm/memory/pipeline/ephemeral.js.map +1 -1
  77. package/dist/esm/memory/pipeline/fact.js +4 -2
  78. package/dist/esm/memory/pipeline/fact.js.map +1 -1
  79. package/dist/esm/memory/pipeline/narrative.js +4 -2
  80. package/dist/esm/memory/pipeline/narrative.js.map +1 -1
  81. package/dist/esm/memory/pipeline/semantic.js +2 -2
  82. package/dist/esm/memory/pipeline/semantic.js.map +1 -1
  83. package/dist/esm/observe.js +1 -1
  84. package/dist/esm/observe.js.map +1 -1
  85. package/dist/esm/patterns/MapReduce.js +5 -5
  86. package/dist/esm/patterns/MapReduce.js.map +1 -1
  87. package/dist/esm/patterns/Swarm.js +1 -1
  88. package/dist/esm/patterns/Swarm.js.map +1 -1
  89. package/dist/esm/recorders/observability/BoundaryRecorder.js +315 -36
  90. package/dist/esm/recorders/observability/BoundaryRecorder.js.map +1 -1
  91. package/dist/esm/recorders/observability/FlowchartRecorder.js +10 -0
  92. package/dist/esm/recorders/observability/FlowchartRecorder.js.map +1 -1
  93. package/dist/esm/recorders/observability/LiveStateRecorder.js +112 -21
  94. package/dist/esm/recorders/observability/LiveStateRecorder.js.map +1 -1
  95. package/dist/esm/recorders/observability/RunStepRecorder.js +652 -0
  96. package/dist/esm/recorders/observability/RunStepRecorder.js.map +1 -0
  97. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js +3 -4
  98. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  99. package/dist/esm/recorders/observability/internal/ActorArrowClassifier.js +34 -0
  100. package/dist/esm/recorders/observability/internal/ActorArrowClassifier.js.map +1 -0
  101. package/dist/esm/recorders/observability/internal/CandidateAnswerBuffer.js +32 -0
  102. package/dist/esm/recorders/observability/internal/CandidateAnswerBuffer.js.map +1 -0
  103. package/dist/esm/recorders/observability/internal/ForkTracker.js +84 -0
  104. package/dist/esm/recorders/observability/internal/ForkTracker.js.map +1 -0
  105. package/dist/esm/recorders/observability/internal/RootInferrer.js +114 -0
  106. package/dist/esm/recorders/observability/internal/RootInferrer.js.map +1 -0
  107. package/dist/esm/recorders/observability/internal/SequenceSiblingTracker.js +31 -0
  108. package/dist/esm/recorders/observability/internal/SequenceSiblingTracker.js.map +1 -0
  109. package/dist/esm/recorders/observability/observeRunId.js +21 -0
  110. package/dist/esm/recorders/observability/observeRunId.js.map +1 -0
  111. package/dist/esm/reliability/buildReliabilityGateChart.js +1 -1
  112. package/dist/esm/reliability/buildReliabilityGateChart.js.map +1 -1
  113. package/dist/index.js +7 -3
  114. package/dist/index.js.map +1 -1
  115. package/dist/lib/injection-engine/buildInjectionEngineSubflow.js +1 -1
  116. package/dist/lib/injection-engine/buildInjectionEngineSubflow.js.map +1 -1
  117. package/dist/memory/causal/snapshotPipeline.js +6 -2
  118. package/dist/memory/causal/snapshotPipeline.js.map +1 -1
  119. package/dist/memory/pipeline/auto.js +2 -2
  120. package/dist/memory/pipeline/auto.js.map +1 -1
  121. package/dist/memory/pipeline/default.js +4 -2
  122. package/dist/memory/pipeline/default.js.map +1 -1
  123. package/dist/memory/pipeline/ephemeral.js +3 -1
  124. package/dist/memory/pipeline/ephemeral.js.map +1 -1
  125. package/dist/memory/pipeline/fact.js +4 -2
  126. package/dist/memory/pipeline/fact.js.map +1 -1
  127. package/dist/memory/pipeline/narrative.js +4 -2
  128. package/dist/memory/pipeline/narrative.js.map +1 -1
  129. package/dist/memory/pipeline/semantic.js +2 -2
  130. package/dist/memory/pipeline/semantic.js.map +1 -1
  131. package/dist/observe.js +1 -1
  132. package/dist/observe.js.map +1 -1
  133. package/dist/patterns/MapReduce.js +5 -5
  134. package/dist/patterns/MapReduce.js.map +1 -1
  135. package/dist/patterns/Swarm.js +1 -1
  136. package/dist/patterns/Swarm.js.map +1 -1
  137. package/dist/recorders/observability/BoundaryRecorder.js +314 -35
  138. package/dist/recorders/observability/BoundaryRecorder.js.map +1 -1
  139. package/dist/recorders/observability/FlowchartRecorder.js +10 -0
  140. package/dist/recorders/observability/FlowchartRecorder.js.map +1 -1
  141. package/dist/recorders/observability/LiveStateRecorder.js +111 -20
  142. package/dist/recorders/observability/LiveStateRecorder.js.map +1 -1
  143. package/dist/recorders/observability/RunStepRecorder.js +658 -0
  144. package/dist/recorders/observability/RunStepRecorder.js.map +1 -0
  145. package/dist/recorders/observability/commentary/commentaryTemplates.js +3 -4
  146. package/dist/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  147. package/dist/recorders/observability/internal/ActorArrowClassifier.js +38 -0
  148. package/dist/recorders/observability/internal/ActorArrowClassifier.js.map +1 -0
  149. package/dist/recorders/observability/internal/CandidateAnswerBuffer.js +36 -0
  150. package/dist/recorders/observability/internal/CandidateAnswerBuffer.js.map +1 -0
  151. package/dist/recorders/observability/internal/ForkTracker.js +88 -0
  152. package/dist/recorders/observability/internal/ForkTracker.js.map +1 -0
  153. package/dist/recorders/observability/internal/RootInferrer.js +118 -0
  154. package/dist/recorders/observability/internal/RootInferrer.js.map +1 -0
  155. package/dist/recorders/observability/internal/SequenceSiblingTracker.js +35 -0
  156. package/dist/recorders/observability/internal/SequenceSiblingTracker.js.map +1 -0
  157. package/dist/recorders/observability/observeRunId.js +25 -0
  158. package/dist/recorders/observability/observeRunId.js.map +1 -0
  159. package/dist/reliability/buildReliabilityGateChart.js +1 -1
  160. package/dist/reliability/buildReliabilityGateChart.js.map +1 -1
  161. package/dist/types/cache/CacheDecisionSubflow.d.ts.map +1 -1
  162. package/dist/types/conventions.d.ts.map +1 -1
  163. package/dist/types/core/Agent.d.ts +20 -18
  164. package/dist/types/core/Agent.d.ts.map +1 -1
  165. package/dist/types/core/LLMCall.d.ts +28 -2
  166. package/dist/types/core/LLMCall.d.ts.map +1 -1
  167. package/dist/types/core/RunnerBase.d.ts +124 -4
  168. package/dist/types/core/RunnerBase.d.ts.map +1 -1
  169. package/dist/types/core/agent/buildAgentChart.d.ts +7 -1
  170. package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
  171. package/dist/types/core/agent/types.d.ts +29 -0
  172. package/dist/types/core/agent/types.d.ts.map +1 -1
  173. package/dist/types/core/runner.d.ts +51 -5
  174. package/dist/types/core/runner.d.ts.map +1 -1
  175. package/dist/types/core/slots/buildMessagesSlot.d.ts.map +1 -1
  176. package/dist/types/core/slots/buildSystemPromptSlot.d.ts.map +1 -1
  177. package/dist/types/core/slots/buildThinkingSubflow.d.ts.map +1 -1
  178. package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
  179. package/dist/types/core/translator.d.ts +95 -0
  180. package/dist/types/core/translator.d.ts.map +1 -0
  181. package/dist/types/core-flow/Conditional.d.ts +48 -4
  182. package/dist/types/core-flow/Conditional.d.ts.map +1 -1
  183. package/dist/types/core-flow/Loop.d.ts +42 -3
  184. package/dist/types/core-flow/Loop.d.ts.map +1 -1
  185. package/dist/types/core-flow/Parallel.d.ts +99 -4
  186. package/dist/types/core-flow/Parallel.d.ts.map +1 -1
  187. package/dist/types/core-flow/Sequence.d.ts +49 -3
  188. package/dist/types/core-flow/Sequence.d.ts.map +1 -1
  189. package/dist/types/events/payloads.d.ts +22 -1
  190. package/dist/types/events/payloads.d.ts.map +1 -1
  191. package/dist/types/index.d.ts +4 -2
  192. package/dist/types/index.d.ts.map +1 -1
  193. package/dist/types/lib/injection-engine/buildInjectionEngineSubflow.d.ts.map +1 -1
  194. package/dist/types/memory/causal/snapshotPipeline.d.ts.map +1 -1
  195. package/dist/types/memory/pipeline/auto.d.ts.map +1 -1
  196. package/dist/types/memory/pipeline/default.d.ts.map +1 -1
  197. package/dist/types/memory/pipeline/ephemeral.d.ts.map +1 -1
  198. package/dist/types/memory/pipeline/fact.d.ts.map +1 -1
  199. package/dist/types/memory/pipeline/narrative.d.ts.map +1 -1
  200. package/dist/types/memory/pipeline/semantic.d.ts.map +1 -1
  201. package/dist/types/observe.d.ts +2 -2
  202. package/dist/types/observe.d.ts.map +1 -1
  203. package/dist/types/recorders/observability/BoundaryRecorder.d.ts +160 -6
  204. package/dist/types/recorders/observability/BoundaryRecorder.d.ts.map +1 -1
  205. package/dist/types/recorders/observability/FlowchartRecorder.d.ts.map +1 -1
  206. package/dist/types/recorders/observability/LiveStateRecorder.d.ts +42 -6
  207. package/dist/types/recorders/observability/LiveStateRecorder.d.ts.map +1 -1
  208. package/dist/types/recorders/observability/RunStepRecorder.d.ts +232 -0
  209. package/dist/types/recorders/observability/RunStepRecorder.d.ts.map +1 -0
  210. package/dist/types/recorders/observability/commentary/commentaryTemplates.d.ts.map +1 -1
  211. package/dist/types/recorders/observability/internal/ActorArrowClassifier.d.ts +26 -0
  212. package/dist/types/recorders/observability/internal/ActorArrowClassifier.d.ts.map +1 -0
  213. package/dist/types/recorders/observability/internal/CandidateAnswerBuffer.d.ts +29 -0
  214. package/dist/types/recorders/observability/internal/CandidateAnswerBuffer.d.ts.map +1 -0
  215. package/dist/types/recorders/observability/internal/ForkTracker.d.ts +61 -0
  216. package/dist/types/recorders/observability/internal/ForkTracker.d.ts.map +1 -0
  217. package/dist/types/recorders/observability/internal/RootInferrer.d.ts +52 -0
  218. package/dist/types/recorders/observability/internal/RootInferrer.d.ts.map +1 -0
  219. package/dist/types/recorders/observability/internal/SequenceSiblingTracker.d.ts +25 -0
  220. package/dist/types/recorders/observability/internal/SequenceSiblingTracker.d.ts.map +1 -0
  221. package/dist/types/recorders/observability/observeRunId.d.ts +37 -0
  222. package/dist/types/recorders/observability/observeRunId.d.ts.map +1 -0
  223. package/dist/types/reliability/buildReliabilityGateChart.d.ts.map +1 -1
  224. package/package.json +3 -3
@@ -75,8 +75,9 @@
75
75
  * }
76
76
  * ```
77
77
  */
78
- import { ROOT_RUNTIME_STAGE_ID, ROOT_SUBFLOW_ID, SequenceRecorder } from 'footprintjs/trace';
78
+ import { ROOT_RUNTIME_STAGE_ID, ROOT_SUBFLOW_ID, SequenceStore, CommitRangeIndex, } from 'footprintjs/trace';
79
79
  import { SUBFLOW_IDS, STAGE_IDS, slotFromSubflowId } from '../../conventions.js';
80
+ import { createRunIdObserver } from './observeRunId.js';
80
81
  /** Closed set of routing/wrapper subflow IDs that are pure plumbing.
81
82
  * Slot subflows (`sf-system-prompt` / `sf-messages` / `sf-tools`) are
82
83
  * NOT in this set — they're real context-engineering moments.
@@ -124,6 +125,53 @@ function isAgentInternalId(localId) {
124
125
  }
125
126
  return false;
126
127
  }
128
+ function toBoundaryLabel(e) {
129
+ if (e.type === 'subflow.entry') {
130
+ return {
131
+ type: 'subflow.entry',
132
+ runtimeStageId: e.runtimeStageId,
133
+ subflowPath: e.subflowPath,
134
+ depth: e.depth,
135
+ ts: e.ts,
136
+ subflowId: e.subflowId,
137
+ localSubflowId: e.localSubflowId,
138
+ subflowName: e.subflowName,
139
+ ...(e.description !== undefined ? { description: e.description } : {}),
140
+ ...(e.primitiveKind !== undefined ? { primitiveKind: e.primitiveKind } : {}),
141
+ ...(e.slotKind !== undefined ? { slotKind: e.slotKind } : {}),
142
+ isAgentInternal: e.isAgentInternal,
143
+ };
144
+ }
145
+ return {
146
+ type: 'run.entry',
147
+ runtimeStageId: e.runtimeStageId,
148
+ subflowPath: e.subflowPath,
149
+ depth: e.depth,
150
+ ts: e.ts,
151
+ };
152
+ }
153
+ /** Build a BoundaryRangeLabel for the open side of a composition pair. */
154
+ function toCompositionBoundaryLabel(e) {
155
+ return {
156
+ type: 'composition.start',
157
+ runtimeStageId: e.runtimeStageId,
158
+ subflowPath: e.subflowPath,
159
+ depth: e.depth,
160
+ ts: e.ts,
161
+ compositionKind: e.kind,
162
+ compositionName: e.name,
163
+ };
164
+ }
165
+ /** Clamp `getCommitCount()` returns to a safe non-negative integer.
166
+ * Defensive against malformed injections returning NaN/Infinity/negatives
167
+ * (security panel review YELLOW #2). */
168
+ function sanitizeCommitCount(n) {
169
+ if (!Number.isFinite(n))
170
+ return 0;
171
+ if (n < 0)
172
+ return 0;
173
+ return n;
174
+ }
127
175
  let _counter = 0;
128
176
  /** Factory — matches the `inOutRecorder()` / `topologyRecorder()` style. */
129
177
  export function boundaryRecorder(options = {}) {
@@ -134,12 +182,35 @@ export function boundaryRecorder(options = {}) {
134
182
  * attach to the executor's FlowRecorder channel; exposes `subscribe()`
135
183
  * to wire to the agentfootprint typed-event dispatcher.
136
184
  *
137
- * Internally stores events in a `SequenceRecorder<DomainEvent>` so the
138
- * usual time-travel utilities (`getEntryRanges`, `accumulate`) work
139
- * out of the box.
185
+ * v5: composes a `SequenceStore<DomainEvent>` (storage) instead of
186
+ * extending the deprecated `SequenceRecorder<T>` base. Time-travel
187
+ * utilities (`getEntryRanges`, `accumulate`) are accessed through the
188
+ * store via the public read API on this class.
140
189
  */
141
- export class BoundaryRecorder extends SequenceRecorder {
190
+ export class BoundaryRecorder {
142
191
  id;
192
+ /** Composition: storage shelf. */
193
+ store = new SequenceStore();
194
+ /**
195
+ * Phase 5 Layer 2 — interval index over commit indices, populated
196
+ * live as boundary entry/exit pairs fire. Consumers (Lens) read
197
+ * `enclosing(commitIdx)` for breadcrumbs and `overlapping(slice)`
198
+ * for time-range queries. Empty when `getCommitCount` is not
199
+ * injected. See `docs/design/boundary-commit-ranges.md`.
200
+ */
201
+ boundaryIndex = new CommitRangeIndex();
202
+ /** Open-range tokens keyed by `runtimeStageId` so the matching exit
203
+ * can close the correct range. Pure side-table; cleared on runId
204
+ * reset. Not exposed externally. */
205
+ openTokens = new Map();
206
+ /** Live commit-count accessor injected by the runner. Sanitized
207
+ * (NaN/Infinity/negative → 0) before use. */
208
+ getCommitCount;
209
+ /** True when `getCommitCount` was explicitly injected. In LEGACY
210
+ * MODE (false), `boundaryIndex` is intentionally NOT populated —
211
+ * zero-width [0,0] ranges would mislead consumers querying the
212
+ * index. Multi-panel review flagged this footgun. */
213
+ hasCommitTracking;
143
214
  /**
144
215
  * Tracks whether the most recent `llm.end` had toolCalls. Used to
145
216
  * classify the NEXT `llm.start` as `'tool→llm'` (vs `'user→llm'` if
@@ -147,50 +218,167 @@ export class BoundaryRecorder extends SequenceRecorder {
147
218
  * `llm.start` event after the classification is applied.
148
219
  */
149
220
  prevLLMEndHadTools = false;
221
+ /**
222
+ * Run-boundary observer — fires resetForNewRun() when
223
+ * traversalContext.runId changes between events AND no boundary is
224
+ * currently open. The "no open boundary" gate distinguishes:
225
+ *
226
+ * - **Legitimate new run** — consumer reuses one recorder across
227
+ * sequential `executor.run()` calls. All prior boundaries closed
228
+ * before the second run began; openTokens is empty when the new
229
+ * runId arrives → safe to wipe state so the second run doesn't
230
+ * alias with the first.
231
+ * - **Composition sub-run** — primitives like `LLMCall`, `Sequence`,
232
+ * and `Parallel` internally spawn their own `FlowChartExecutor`
233
+ * instances. Each sub-executor mints a NEW runId. When that
234
+ * sub-executor fires events on the SHARED recorder, the recorder
235
+ * is still inside the parent run — `openTokens` is non-empty.
236
+ * Resetting here would wipe the parent's boundary index mid-run
237
+ * (the bug Layer 4 surfaced in agentfootprint-lens fanout).
238
+ *
239
+ * The `openTokens.size === 0` check is the cleanest semantic signal:
240
+ * if nothing is in-flight, a runId change means "the consumer started
241
+ * fresh"; if something is open, the new runId is from a sub-executor
242
+ * nested inside the still-ongoing parent.
243
+ */
244
+ runIdGuard = createRunIdObserver(() => {
245
+ if (this.openTokens.size > 0) {
246
+ // Inside an active run — new runId is from a composition sub-
247
+ // executor (LLMCall / Sequence / Parallel). Do NOT reset.
248
+ return;
249
+ }
250
+ this.store.clear();
251
+ this.boundaryIndex.clear();
252
+ this.openTokens.clear();
253
+ this.prevLLMEndHadTools = false;
254
+ });
150
255
  constructor(options = {}) {
151
- super();
152
256
  this.id = options.id ?? `boundary-${++_counter}`;
257
+ this.hasCommitTracking = options.getCommitCount !== undefined;
258
+ const raw = options.getCommitCount;
259
+ this.getCommitCount = raw === undefined ? () => 0 : () => sanitizeCommitCount(raw());
153
260
  }
261
+ /**
262
+ * Reset all transient state.
263
+ *
264
+ * **Composition-safe gate (Phase 5 Layer 4):** if `openTokens.size > 0`
265
+ * the call is a no-op. Rationale: `FlowChartExecutor.run()` calls
266
+ * `r.clear?.()` on every attached recorder during its pre-run loop.
267
+ * When agentfootprint composition primitives (LLMCall, Sequence,
268
+ * Parallel, etc.) propagate the parent's recorders to nested
269
+ * sub-executors, EACH sub-executor's pre-run clear loop calls
270
+ * `clear()` on the SHARED parent recorder mid-run — wiping live
271
+ * parent state. The `openTokens.size > 0` check distinguishes:
272
+ *
273
+ * - **Legitimate reset** — consumer or executor calls `clear()`
274
+ * when no boundary is in-flight (`openTokens` empty). Safe to
275
+ * wipe; the recorder is idle.
276
+ * - **Composition wipe** — sub-executor's pre-run clear fires
277
+ * while the parent has open boundaries (`openTokens` non-empty).
278
+ * Skip the wipe; the parent's state must be preserved.
279
+ *
280
+ * If a consumer needs to forcibly wipe state even with open tokens
281
+ * (e.g., manual recovery after a crashed run), pair `clear()` with
282
+ * an explicit `forceClear()` (TODO — add when the use case shows up;
283
+ * today the recorder lifecycle pattern is "one recorder per logical
284
+ * run" so leaked tokens shouldn't occur).
285
+ */
154
286
  clear() {
155
- super.clear();
287
+ if (this.openTokens.size > 0) {
288
+ // Mid-run wipe attempt — almost certainly a sub-executor's
289
+ // pre-run clear via composition propagation. Skip.
290
+ return;
291
+ }
292
+ this.store.clear();
293
+ this.boundaryIndex.clear();
294
+ this.openTokens.clear();
156
295
  this.prevLLMEndHadTools = false;
296
+ this.runIdGuard.reset();
297
+ }
298
+ observeRunId(runId) {
299
+ this.runIdGuard.observe(runId);
157
300
  }
158
301
  // ── FlowRecorder hooks (footprintjs side) ───────────────────────────
159
302
  onRunStart(event) {
160
- this.emit(buildRunEvent('run.entry', event.payload));
303
+ this.observeRunId(event.traversalContext?.runId);
304
+ const commitIdxBefore = this.getCommitCount();
305
+ const e = buildRunEvent('run.entry', event.payload, commitIdxBefore);
306
+ // Open range BEFORE the store push so a failed push doesn't leak
307
+ // an unclosed range (DS+logic panel review). The label is the
308
+ // stripped projection (no payload) — security-panel YELLOW #1.
309
+ if (this.hasCommitTracking) {
310
+ const token = this.boundaryIndex.open(toBoundaryLabel(e), commitIdxBefore);
311
+ this.openTokens.set(e.runtimeStageId, token);
312
+ }
313
+ this.store.push(e);
161
314
  }
162
315
  onRunEnd(event) {
163
- this.emit(buildRunEvent('run.exit', event.payload));
316
+ this.observeRunId(event.traversalContext?.runId);
317
+ const commitIdxBefore = this.getCommitCount();
318
+ const e = buildRunEvent('run.exit', event.payload, commitIdxBefore);
319
+ // Close the range BEFORE store.push so a failed push doesn't
320
+ // leak a permanently-open range. The range is the canonical
321
+ // truth; the store entry is downstream telemetry.
322
+ if (this.hasCommitTracking) {
323
+ const token = this.openTokens.get(e.runtimeStageId);
324
+ if (token) {
325
+ this.boundaryIndex.close(token, commitIdxBefore);
326
+ this.openTokens.delete(e.runtimeStageId);
327
+ }
328
+ }
329
+ this.store.push(e);
164
330
  }
165
331
  onSubflowEntry(event) {
166
- const e = buildSubflowEvent(event, 'subflow.entry');
167
- if (e)
168
- this.emit(e);
332
+ this.observeRunId(event.traversalContext?.runId);
333
+ const commitIdxBefore = this.getCommitCount();
334
+ const e = buildSubflowEvent(event, 'subflow.entry', commitIdxBefore);
335
+ if (!e)
336
+ return;
337
+ if (this.hasCommitTracking) {
338
+ const token = this.boundaryIndex.open(toBoundaryLabel(e), commitIdxBefore);
339
+ this.openTokens.set(e.runtimeStageId, token);
340
+ }
341
+ this.store.push(e);
169
342
  }
170
343
  onSubflowExit(event) {
171
- const e = buildSubflowEvent(event, 'subflow.exit');
172
- if (e)
173
- this.emit(e);
344
+ this.observeRunId(event.traversalContext?.runId);
345
+ const commitIdxBefore = this.getCommitCount();
346
+ const e = buildSubflowEvent(event, 'subflow.exit', commitIdxBefore);
347
+ if (!e)
348
+ return;
349
+ if (this.hasCommitTracking) {
350
+ const token = this.openTokens.get(e.runtimeStageId);
351
+ if (token) {
352
+ this.boundaryIndex.close(token, commitIdxBefore);
353
+ this.openTokens.delete(e.runtimeStageId);
354
+ }
355
+ }
356
+ this.store.push(e);
174
357
  }
175
358
  onFork(event) {
359
+ this.observeRunId(event.traversalContext?.runId);
176
360
  const ts = Date.now();
177
361
  const ctx = event.traversalContext;
178
362
  const runtimeStageId = ctx?.runtimeStageId ?? '';
179
363
  const segments = ctx?.subflowPath ? ctx.subflowPath.split('/').filter(Boolean) : [];
180
364
  const subflowPath = [ROOT_SUBFLOW_ID, ...segments];
365
+ const commitIdxBefore = this.getCommitCount();
181
366
  for (const childName of event.children) {
182
- this.emit({
367
+ this.store.push({
183
368
  type: 'fork.branch',
184
369
  runtimeStageId,
185
370
  subflowPath,
186
371
  depth: subflowPath.length - 1,
187
372
  ts,
373
+ commitIdxBefore,
374
+ commitIdxAfter: commitIdxBefore,
188
375
  parentSubflowId: event.parent,
189
376
  childName,
190
377
  });
191
378
  }
192
379
  }
193
380
  onDecision(event) {
381
+ this.observeRunId(event.traversalContext?.runId);
194
382
  const ctx = event.traversalContext;
195
383
  // Agent-internal decisions (Route picking tool-calls / final) are
196
384
  // identified by the deciding stage's stableId matching one of the
@@ -204,12 +392,15 @@ export class BoundaryRecorder extends SequenceRecorder {
204
392
  ? stageId.slice(stageId.lastIndexOf('/') + 1)
205
393
  : stageId;
206
394
  const isAgentInternal = isAgentInternalId(localStageId);
207
- this.emit({
395
+ const commitIdxBefore = this.getCommitCount();
396
+ this.store.push({
208
397
  type: 'decision.branch',
209
398
  runtimeStageId: ctx?.runtimeStageId ?? '',
210
399
  subflowPath: pathFromCtx(ctx?.subflowPath),
211
400
  depth: ctxDepth(ctx?.subflowPath),
212
401
  ts: Date.now(),
402
+ commitIdxBefore,
403
+ commitIdxAfter: commitIdxBefore,
213
404
  decider: event.decider,
214
405
  chosen: event.chosen,
215
406
  ...(event.rationale ? { rationale: event.rationale } : {}),
@@ -217,13 +408,17 @@ export class BoundaryRecorder extends SequenceRecorder {
217
408
  });
218
409
  }
219
410
  onLoop(event) {
411
+ this.observeRunId(event.traversalContext?.runId);
220
412
  const ctx = event.traversalContext;
221
- this.emit({
413
+ const commitIdxBefore = this.getCommitCount();
414
+ this.store.push({
222
415
  type: 'loop.iteration',
223
416
  runtimeStageId: ctx?.runtimeStageId ?? '',
224
417
  subflowPath: pathFromCtx(ctx?.subflowPath),
225
418
  depth: ctxDepth(ctx?.subflowPath),
226
419
  ts: Date.now(),
420
+ commitIdxBefore,
421
+ commitIdxAfter: commitIdxBefore,
227
422
  target: event.target,
228
423
  iteration: event.iteration,
229
424
  });
@@ -241,11 +436,22 @@ export class BoundaryRecorder extends SequenceRecorder {
241
436
  return dispatcher.on('*', (event) => this.ingestTypedEvent(event));
242
437
  }
243
438
  ingestTypedEvent(event) {
439
+ // NOTE: deliberately does NOT call observeRunId(event.meta.runId).
440
+ // The agentfootprint dispatcher's runId is generated by a DIFFERENT
441
+ // generator than footprintjs's traversalContext.runId. Mixing them
442
+ // would toggle lastRunId on every event and trigger a false reset.
443
+ // Run-boundary detection happens reliably via the FlowRecorder hooks
444
+ // (onRunStart fires FIRST in any new run, before any typed event).
244
445
  const meta = event.meta;
245
446
  const runtimeStageId = meta.runtimeStageId ?? '';
246
447
  const subflowPath = [ROOT_SUBFLOW_ID, ...(meta.subflowPath ?? [])];
247
448
  const depth = subflowPath.length - 1;
248
449
  const ts = meta.wallClockMs;
450
+ // Phase 5 Layer 2: stamp commit index on every typed event for
451
+ // consumers that want to join domain events with the commit log
452
+ // (e.g., "which LLM call happened during this commit slice?").
453
+ // Typed events don't write to scope themselves, so before === after.
454
+ const commitIdxBefore = this.getCommitCount();
249
455
  switch (event.type) {
250
456
  case 'agentfootprint.stream.llm_start': {
251
457
  const p = event.payload;
@@ -257,12 +463,14 @@ export class BoundaryRecorder extends SequenceRecorder {
257
463
  ? 'tool→llm'
258
464
  : 'user→llm';
259
465
  this.prevLLMEndHadTools = false;
260
- this.emit({
466
+ this.store.push({
261
467
  type: 'llm.start',
262
468
  runtimeStageId,
263
469
  subflowPath,
264
470
  depth,
265
471
  ts,
472
+ commitIdxBefore,
473
+ commitIdxAfter: commitIdxBefore,
266
474
  model: p.model,
267
475
  provider: p.provider,
268
476
  ...(p.systemPromptChars !== undefined ? { systemPromptChars: p.systemPromptChars } : {}),
@@ -279,12 +487,14 @@ export class BoundaryRecorder extends SequenceRecorder {
279
487
  // terminal call (toolCallCount === 0) leaves the flag false so
280
488
  // a hypothetical follow-up call would correctly be 'user→llm'.
281
489
  this.prevLLMEndHadTools = p.toolCallCount > 0;
282
- this.emit({
490
+ this.store.push({
283
491
  type: 'llm.end',
284
492
  runtimeStageId,
285
493
  subflowPath,
286
494
  depth,
287
495
  ts,
496
+ commitIdxBefore,
497
+ commitIdxAfter: commitIdxBefore,
288
498
  content: p.content,
289
499
  toolCallCount: p.toolCallCount,
290
500
  usage: { input: p.usage.input, output: p.usage.output },
@@ -295,12 +505,14 @@ export class BoundaryRecorder extends SequenceRecorder {
295
505
  }
296
506
  case 'agentfootprint.stream.tool_start': {
297
507
  const p = event.payload;
298
- this.emit({
508
+ this.store.push({
299
509
  type: 'tool.start',
300
510
  runtimeStageId,
301
511
  subflowPath,
302
512
  depth,
303
513
  ts,
514
+ commitIdxBefore,
515
+ commitIdxAfter: commitIdxBefore,
304
516
  toolName: p.toolName,
305
517
  toolCallId: p.toolCallId,
306
518
  ...(p.args !== undefined ? { args: p.args } : {}),
@@ -309,12 +521,14 @@ export class BoundaryRecorder extends SequenceRecorder {
309
521
  }
310
522
  case 'agentfootprint.stream.tool_end': {
311
523
  const p = event.payload;
312
- this.emit({
524
+ this.store.push({
313
525
  type: 'tool.end',
314
526
  runtimeStageId,
315
527
  subflowPath,
316
528
  depth,
317
529
  ts,
530
+ commitIdxBefore,
531
+ commitIdxAfter: commitIdxBefore,
318
532
  toolCallId: p.toolCallId,
319
533
  ...(p.result !== undefined ? { result: p.result } : {}),
320
534
  ...(p.durationMs !== undefined ? { durationMs: p.durationMs } : {}),
@@ -324,12 +538,14 @@ export class BoundaryRecorder extends SequenceRecorder {
324
538
  }
325
539
  case 'agentfootprint.context.injected': {
326
540
  const p = event.payload;
327
- this.emit({
541
+ this.store.push({
328
542
  type: 'context.injected',
329
543
  runtimeStageId,
330
544
  subflowPath,
331
545
  depth,
332
546
  ts,
547
+ commitIdxBefore,
548
+ commitIdxAfter: commitIdxBefore,
333
549
  slot: p.slot,
334
550
  source: p.source ?? 'unknown',
335
551
  ...(p.sourceId ? { sourceId: p.sourceId } : {}),
@@ -347,23 +563,82 @@ export class BoundaryRecorder extends SequenceRecorder {
347
563
  });
348
564
  break;
349
565
  }
566
+ case 'agentfootprint.composition.enter': {
567
+ // Open a boundary range for the composition. The MATCHING KEY
568
+ // for open/close is `payload.id` (the composition's stable id),
569
+ // NOT `meta.runtimeStageId`. Reason: the composition's enter
570
+ // event fires from a different stage (entry hook) than its
571
+ // exit event (merge / exit hook) — different `meta.runtimeStageId`s.
572
+ // The composition's `id` is the only field that's the same on
573
+ // both. The boundary range's runtimeStageId (used as the Lens
574
+ // group identity) is the ENTER event's `meta.runtimeStageId`
575
+ // (the entry stage's id) — that's the "fork moment."
576
+ const p = event.payload;
577
+ const e = {
578
+ type: 'composition.start',
579
+ runtimeStageId,
580
+ subflowPath,
581
+ depth,
582
+ ts,
583
+ commitIdxBefore,
584
+ commitIdxAfter: commitIdxBefore,
585
+ kind: p.kind,
586
+ compositionId: p.id,
587
+ name: p.name,
588
+ };
589
+ if (this.hasCommitTracking) {
590
+ const token = this.boundaryIndex.open(toCompositionBoundaryLabel(e), commitIdxBefore);
591
+ this.openTokens.set(`composition:${p.id}`, token);
592
+ }
593
+ this.store.push(e);
594
+ break;
595
+ }
596
+ case 'agentfootprint.composition.exit': {
597
+ // Close the matching composition range. Keyed by `payload.id`
598
+ // — see the enter handler for why this differs from
599
+ // meta.runtimeStageId.
600
+ const p = event.payload;
601
+ const e = {
602
+ type: 'composition.end',
603
+ runtimeStageId,
604
+ subflowPath,
605
+ depth,
606
+ ts,
607
+ commitIdxBefore,
608
+ commitIdxAfter: commitIdxBefore,
609
+ kind: p.kind,
610
+ compositionId: p.id,
611
+ name: p.name ?? '',
612
+ status: p.status,
613
+ durationMs: p.durationMs,
614
+ };
615
+ if (this.hasCommitTracking) {
616
+ const key = `composition:${p.id}`;
617
+ const token = this.openTokens.get(key);
618
+ if (token) {
619
+ this.boundaryIndex.close(token, commitIdxBefore);
620
+ this.openTokens.delete(key);
621
+ }
622
+ }
623
+ this.store.push(e);
624
+ break;
625
+ }
350
626
  default:
351
- // Other typed events (composition.*, agent.*, etc.) are not
352
- // mapped to DomainEvent for now — they're either implied by
353
- // FlowRecorder events (composition) or higher-level summaries
354
- // (agent.turn_*) that downstream selectors derive on demand.
627
+ // Other typed events (agent.*, eval.*, etc.) are not mapped to
628
+ // DomainEvent for now — they're higher-level summaries that
629
+ // downstream selectors derive on demand.
355
630
  break;
356
631
  }
357
632
  }
358
633
  // ── Read API ────────────────────────────────────────────────────────
359
634
  /** All events in capture order (the canonical projection). */
360
635
  getEvents() {
361
- return this.getEntries();
636
+ return this.store.getAll();
362
637
  }
363
638
  /** Type-narrowed lookup: all events of one kind. */
364
639
  getEventsByType(type) {
365
640
  const out = [];
366
- for (const e of this.getEntries()) {
641
+ for (const e of this.store.getAll()) {
367
642
  if (e.type === type)
368
643
  out.push(e);
369
644
  }
@@ -373,7 +648,7 @@ export class BoundaryRecorder extends SequenceRecorder {
373
648
  /** All boundary events (run + subflow, entry + exit interleaved). */
374
649
  getBoundaries() {
375
650
  const out = [];
376
- for (const e of this.getEntries()) {
651
+ for (const e of this.store.getAll()) {
377
652
  if (e.type === 'run.entry' ||
378
653
  e.type === 'run.exit' ||
379
654
  e.type === 'subflow.entry' ||
@@ -393,7 +668,7 @@ export class BoundaryRecorder extends SequenceRecorder {
393
668
  }
394
669
  /** Entry/exit pair for one chart execution by `runtimeStageId`. */
395
670
  getBoundary(runtimeStageId) {
396
- const matches = this.getEntriesForStep(runtimeStageId);
671
+ const matches = this.store.getByKey(runtimeStageId);
397
672
  let entry;
398
673
  let exit;
399
674
  for (const e of matches) {
@@ -420,7 +695,7 @@ export class BoundaryRecorder extends SequenceRecorder {
420
695
  const systemPrompt = [];
421
696
  const messages = [];
422
697
  const tools = [];
423
- for (const e of this.getEntries()) {
698
+ for (const e of this.store.getAll()) {
424
699
  if (e.type !== 'subflow.entry' && e.type !== 'subflow.exit')
425
700
  continue;
426
701
  if (e.slotKind === 'system-prompt')
@@ -459,7 +734,7 @@ export class BoundaryRecorder extends SequenceRecorder {
459
734
  * matches `runtimeStageId`.
460
735
  */
461
736
  aggregateForBoundary(runtimeStageId) {
462
- const events = this.getEntries();
737
+ const events = this.store.getAll();
463
738
  let entry;
464
739
  let exit;
465
740
  for (const e of events) {
@@ -484,7 +759,7 @@ export class BoundaryRecorder extends SequenceRecorder {
484
759
  * not user-facing rollup units.
485
760
  */
486
761
  aggregateAllBoundaries() {
487
- const events = this.getEntries();
762
+ const events = this.store.getAll();
488
763
  const out = [];
489
764
  // Index exits by runtimeStageId for O(1) pair-up.
490
765
  const exitByRid = new Map();
@@ -513,18 +788,20 @@ export class BoundaryRecorder extends SequenceRecorder {
513
788
  }
514
789
  }
515
790
  // ── Internal helpers ─────────────────────────────────────────────────
516
- function buildRunEvent(type, payload) {
791
+ function buildRunEvent(type, payload, commitIdxBefore) {
517
792
  return {
518
793
  type,
519
794
  runtimeStageId: ROOT_RUNTIME_STAGE_ID,
520
795
  subflowPath: [ROOT_SUBFLOW_ID],
521
796
  depth: 0,
522
797
  ts: Date.now(),
798
+ commitIdxBefore,
799
+ commitIdxAfter: commitIdxBefore,
523
800
  payload,
524
801
  isRoot: true,
525
802
  };
526
803
  }
527
- function buildSubflowEvent(event, type) {
804
+ function buildSubflowEvent(event, type, commitIdxBefore) {
528
805
  const subflowId = event.subflowId;
529
806
  if (!subflowId)
530
807
  return undefined;
@@ -545,6 +822,8 @@ function buildSubflowEvent(event, type) {
545
822
  subflowPath,
546
823
  depth,
547
824
  ts: Date.now(),
825
+ commitIdxBefore,
826
+ commitIdxAfter: commitIdxBefore,
548
827
  subflowId,
549
828
  localSubflowId,
550
829
  subflowName: event.name,