@exaudeus/workrail 3.67.0 → 3.68.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 (140) hide show
  1. package/dist/application/services/compiler/template-registry.js +10 -1
  2. package/dist/cli/commands/worktrain-init.js +1 -1
  3. package/dist/console-ui/assets/{index-tOl8Vowf.js → index-CyzltI6D.js} +1 -1
  4. package/dist/console-ui/index.html +1 -1
  5. package/dist/coordinators/modes/full-pipeline.js +4 -4
  6. package/dist/coordinators/modes/implement-shared.js +5 -5
  7. package/dist/coordinators/modes/implement.js +4 -4
  8. package/dist/coordinators/pr-review.js +4 -4
  9. package/dist/daemon/workflow-runner.d.ts +1 -0
  10. package/dist/daemon/workflow-runner.js +1 -0
  11. package/dist/manifest.json +25 -25
  12. package/dist/mcp/handlers/v2-workflow.js +1 -1
  13. package/dist/mcp/workflow-protocol-contracts.js +2 -2
  14. package/docs/authoring-v2.md +4 -4
  15. package/docs/changelog-recent.md +3 -3
  16. package/docs/configuration.md +1 -1
  17. package/docs/design/adaptive-coordinator-context-candidates.md +1 -1
  18. package/docs/design/adaptive-coordinator-context.md +1 -1
  19. package/docs/design/adaptive-coordinator-routing-candidates.md +18 -18
  20. package/docs/design/adaptive-coordinator-routing-review.md +1 -1
  21. package/docs/design/adaptive-coordinator-routing.md +34 -34
  22. package/docs/design/agent-cascade-protocol.md +2 -2
  23. package/docs/design/console-daemon-separation-discovery.md +323 -0
  24. package/docs/design/context-assembly-design-candidates.md +1 -1
  25. package/docs/design/context-assembly-implementation-plan.md +1 -1
  26. package/docs/design/context-assembly-layer.md +2 -2
  27. package/docs/design/context-assembly-review-findings.md +1 -1
  28. package/docs/design/coordinator-access-audit.md +293 -0
  29. package/docs/design/coordinator-architecture-audit.md +62 -0
  30. package/docs/design/coordinator-error-handling-audit.md +240 -0
  31. package/docs/design/coordinator-testability-audit.md +426 -0
  32. package/docs/design/daemon-architecture-discovery.md +1 -1
  33. package/docs/design/daemon-console-separation-discovery.md +242 -0
  34. package/docs/design/daemon-memory-audit.md +203 -0
  35. package/docs/design/design-candidates-console-daemon-separation.md +256 -0
  36. package/docs/design/design-candidates-discovery-loop-fix.md +141 -0
  37. package/docs/design/design-review-findings-console-daemon-separation.md +106 -0
  38. package/docs/design/design-review-findings-discovery-loop-fix.md +81 -0
  39. package/docs/design/discovery-loop-fix-candidates.md +161 -0
  40. package/docs/design/discovery-loop-fix-design-review.md +106 -0
  41. package/docs/design/discovery-loop-fix-validation.md +258 -0
  42. package/docs/design/discovery-loop-investigation-A.md +188 -0
  43. package/docs/design/discovery-loop-investigation-B.md +287 -0
  44. package/docs/design/exploration-workflow-candidates.md +205 -0
  45. package/docs/design/exploration-workflow-design-review.md +166 -0
  46. package/docs/design/exploration-workflow-discovery.md +443 -0
  47. package/docs/design/ide-context-files-candidates.md +231 -0
  48. package/docs/design/ide-context-files-design-review.md +85 -0
  49. package/docs/design/ide-context-files.md +615 -0
  50. package/docs/design/implementation-plan-discovery-loop-fix.md +199 -0
  51. package/docs/design/implementation-plan-queue-poll-rotation.md +102 -0
  52. package/docs/design/in-process-http-audit.md +190 -0
  53. package/docs/design/layer3b-ghost-nodes-design-candidates.md +2 -2
  54. package/docs/design/loadSessionNotes-candidates.md +108 -0
  55. package/docs/design/loadSessionNotes-test-coverage-discovery.md +297 -0
  56. package/docs/design/loadSessionNotes-test-coverage-session4.md +209 -0
  57. package/docs/design/loadSessionNotes-test-coverage-v3.md +321 -0
  58. package/docs/design/probe-session-design-candidates.md +261 -0
  59. package/docs/design/probe-session-phase0.md +490 -0
  60. package/docs/design/routines-guide.md +7 -7
  61. package/docs/design/session-metrics-attribution-candidates.md +250 -0
  62. package/docs/design/session-metrics-attribution-design-review.md +115 -0
  63. package/docs/design/session-metrics-attribution-discovery.md +319 -0
  64. package/docs/design/session-metrics-candidates.md +227 -0
  65. package/docs/design/session-metrics-design-review.md +104 -0
  66. package/docs/design/session-metrics-discovery.md +454 -0
  67. package/docs/design/spawn-session-debug.md +202 -0
  68. package/docs/design/trigger-validator-candidates.md +214 -0
  69. package/docs/design/trigger-validator-review.md +109 -0
  70. package/docs/design/trigger-validator-shaping-phase0.md +239 -0
  71. package/docs/design/trigger-validator.md +454 -0
  72. package/docs/design/v2-core-design-locks.md +2 -2
  73. package/docs/design/workflow-extension-points.md +15 -15
  74. package/docs/design/workflow-id-validation-at-startup.md +1 -1
  75. package/docs/design/workflow-id-validation-implementation-plan.md +2 -2
  76. package/docs/design/workflow-trigger-lifecycle-audit.md +175 -0
  77. package/docs/design/worktrain-task-queue-candidates.md +5 -5
  78. package/docs/design/worktrain-task-queue.md +4 -4
  79. package/docs/discovery/coordinator-script-design.md +1 -1
  80. package/docs/discovery/coordinator-ux-discovery.md +3 -3
  81. package/docs/discovery/simulation-report.md +1 -1
  82. package/docs/discovery/workflow-modernization-discovery.md +326 -0
  83. package/docs/discovery/workflow-selection-for-discovery-tasks.md +33 -33
  84. package/docs/discovery/worktrain-status-briefing.md +1 -1
  85. package/docs/discovery/wr-discovery-goal-reframing.md +1 -1
  86. package/docs/docker.md +1 -1
  87. package/docs/ideas/backlog.md +227 -0
  88. package/docs/ideas/third-party-workflow-setup-design-thinking.md +1 -1
  89. package/docs/integrations/claude-code.md +5 -5
  90. package/docs/integrations/firebender.md +1 -1
  91. package/docs/plans/agentic-orchestration-roadmap.md +2 -2
  92. package/docs/plans/mr-review-workflow-redesign.md +9 -9
  93. package/docs/plans/ui-ux-workflow-design-candidates.md +4 -4
  94. package/docs/plans/ui-ux-workflow-discovery.md +2 -2
  95. package/docs/plans/workflow-categories-candidates.md +8 -8
  96. package/docs/plans/workflow-categories-discovery.md +4 -4
  97. package/docs/plans/workflow-modernization-design.md +430 -0
  98. package/docs/plans/workflow-staleness-detection-candidates.md +11 -11
  99. package/docs/plans/workflow-staleness-detection-review.md +4 -4
  100. package/docs/plans/workflow-staleness-detection.md +9 -9
  101. package/docs/plans/workrail-platform-vision.md +3 -3
  102. package/docs/reference/agent-context-cleaner-snippet.md +1 -1
  103. package/docs/reference/agent-context-guidance.md +4 -4
  104. package/docs/reference/context-optimization.md +2 -2
  105. package/docs/roadmap/now-next-later.md +2 -2
  106. package/docs/roadmap/open-work-inventory.md +16 -16
  107. package/docs/workflows.md +31 -31
  108. package/package.json +1 -1
  109. package/spec/workflow-tags.json +47 -47
  110. package/workflows/adaptive-ticket-creation.json +16 -16
  111. package/workflows/architecture-scalability-audit.json +22 -22
  112. package/workflows/bug-investigation.agentic.v2.json +3 -3
  113. package/workflows/classify-task-workflow.json +1 -1
  114. package/workflows/coding-task-workflow-agentic.json +6 -6
  115. package/workflows/cross-platform-code-conversion.v2.json +8 -8
  116. package/workflows/document-creation-workflow.json +8 -8
  117. package/workflows/documentation-update-workflow.json +8 -8
  118. package/workflows/intelligent-test-case-generation.json +2 -2
  119. package/workflows/learner-centered-course-workflow.json +2 -2
  120. package/workflows/mr-review-workflow.agentic.v2.json +4 -4
  121. package/workflows/personal-learning-materials-creation-branched.json +8 -8
  122. package/workflows/presentation-creation.json +5 -5
  123. package/workflows/production-readiness-audit.json +1 -1
  124. package/workflows/relocation-workflow-us.json +31 -31
  125. package/workflows/routines/context-gathering.json +1 -1
  126. package/workflows/routines/design-review.json +1 -1
  127. package/workflows/routines/execution-simulation.json +1 -1
  128. package/workflows/routines/feature-implementation.json +3 -3
  129. package/workflows/routines/final-verification.json +1 -1
  130. package/workflows/routines/hypothesis-challenge.json +1 -1
  131. package/workflows/routines/ideation.json +1 -1
  132. package/workflows/routines/parallel-work-partitioning.json +3 -3
  133. package/workflows/routines/philosophy-alignment.json +2 -2
  134. package/workflows/routines/plan-analysis.json +1 -1
  135. package/workflows/routines/plan-generation.json +1 -1
  136. package/workflows/routines/tension-driven-design.json +6 -6
  137. package/workflows/scoped-documentation-workflow.json +26 -26
  138. package/workflows/ui-ux-design-workflow.json +14 -14
  139. package/workflows/workflow-diagnose-environment.json +1 -1
  140. package/workflows/workflow-for-workflows.json +1 -1
@@ -0,0 +1,454 @@
1
+ # Session Metrics Discovery
2
+
3
+ **Date:** 2026-04-21
4
+ **Status:** Discovery complete -- recommendation ready
5
+
6
+ ---
7
+
8
+ ## About This Document
9
+
10
+ This document is a human-readable artifact for sharing findings and recommendations. It is NOT execution truth -- if a chat rewind occurs, the durable notes and context variables in the WorkRail session survive; this file may not. Read it for understanding, not for resuming the workflow.
11
+
12
+ ---
13
+
14
+ ## Context / Ask
15
+
16
+ **Stated goal (original):** Discover the best architectural approach for recording metrics and tracking data within WorkRail workflow runs, to answer questions like: LOC changed, files touched, PRs created, and eventually token cost per session.
17
+
18
+ **Note:** The original goal is a solution-statement. The actual problem is stated below.
19
+
20
+ **Reframed problem:** How can WorkRail sessions surface structured, queryable evidence of what was accomplished (code changes, artifacts created, time elapsed) without requiring workflow authors to manually instrument every step?
21
+
22
+ ---
23
+
24
+ ## Path Recommendation
25
+
26
+ **Selected path:** `full_spectrum`
27
+
28
+ **Rationale:**
29
+ - The original framing is solution-biased (it assumes WorkRail needs new architecture). Full-spectrum challenges this before generating candidates.
30
+ - The landscape must be understood first: the current event schema already has `observation_recorded`, `context_set`, and `node_output_appended`. Whether these are sufficient is a landscape question.
31
+ - A design-first angle is also needed: what should be attributed to WorkRail's responsibility vs. external tooling?
32
+ - `landscape_first` alone would skip the reframing needed. `design_first` alone would skip reading the actual event schema.
33
+
34
+ ---
35
+
36
+ ## Constraints / Anti-goals
37
+
38
+ **Core constraints:**
39
+ - WorkRail cannot directly observe what the agent does between steps -- it only sees what the agent explicitly reports via `continue_workflow`
40
+ - Token usage is NOT accessible to the MCP layer today -- Claude Code does not expose per-tool-call token counts to MCP servers
41
+ - Sessions are stored as append-only event logs in `~/.workrail/data/sessions/`
42
+ - The `observation_recorded` event has a closed key set: `['git_branch', 'git_head_sha', 'repo_root_hash', 'repo_root']`
43
+ - `context_set` carries `JsonValue` data with no schema enforcement on the `context` field itself
44
+ - Event envelopes have no wall-clock timestamps today
45
+
46
+ **Anti-goals:**
47
+ - Do not build a new mechanism if the existing `context_set` or artifact output is already sufficient with a convention
48
+ - Do not require all existing workflows to be updated to get basic metrics
49
+ - Do not make token cost a blocking requirement (it is not achievable today)
50
+ - Do not conflate WorkRail-observable facts (step count, duration, what agent self-reported) with external facts (actual git diff, actual LOC) -- these require different mechanisms
51
+
52
+ ---
53
+
54
+ ## Landscape Packet
55
+
56
+ ### What exists today
57
+
58
+ **Event types relevant to metrics:**
59
+ - `observation_recorded` -- closed key set (`git_branch`, `git_head_sha`, `repo_root_hash`, `repo_root`). Emitted at session start by MCP handler. NOT agent-reportable today.
60
+ - `context_set` -- carries arbitrary `JsonValue` data. Source is `'initial'` or `'agent_delta'`. Already used to carry `goal`, `is_autonomous`, `parentSessionId`. This IS agent-reportable via the `context` field of `continue_workflow`.
61
+ - `node_output_appended` -- carries per-step notes (markdown) and artifact refs. Already carries structured artifacts (assessment, loop_control, coordinator_signal, review_verdict, discovery_handoff).
62
+ - `advance_recorded` -- records that an advance happened. No metrics data.
63
+ - `gap_recorded` -- records gaps/omissions. No metrics data.
64
+ - No wall-clock timestamps on any events today.
65
+
66
+ **Structured artifact contracts today:**
67
+ - `wr.contracts.assessment` -- assessment dimensions + scores
68
+ - `wr.contracts.loop_control` -- loop exit/continue decisions
69
+ - `wr.contracts.coordinator_signal` -- spawn/complete coordinator signals
70
+ - `wr.contracts.review_verdict` -- review pass/fail
71
+ - `wr.contracts.discovery_handoff` -- discovery findings
72
+
73
+ None of these are metrics-shaped.
74
+
75
+ **What the console shows today (from `ConsoleSessionSummary`):**
76
+ - sessionId, workflowId, workflowName
77
+ - status, health
78
+ - nodeCount, edgeCount, tipCount
79
+ - hasUnresolvedGaps, recapSnippet
80
+ - gitBranch (from `observation_recorded`)
81
+ - lastModifiedMs (derived from filesystem mtime, not event timestamps)
82
+ - isAutonomous, parentSessionId
83
+
84
+ The console already reads worktree git state (changedFiles, aheadCount, unpushedCommits) via background enrichment scans from the daemon. This is NOT session-scoped -- it is worktree-scoped (current state, not what changed during a session).
85
+
86
+ **What is NOT recorded today (in durable events):**
87
+ - Wall-clock session duration (no event timestamps; `lastModifiedMs` is filesystem mtime only)
88
+ - Step count per run (only `nodeCount`, which includes checkpoints + blocked_attempts)
89
+ - Files touched during a session (vs. current files changed in the worktree)
90
+ - LOC changed during a session
91
+ - PRs created during a session
92
+ - Agent HEAD SHA at session end (start SHA IS captured via `observation_recorded`)
93
+ - Token usage (not accessible at MCP layer)
94
+
95
+ **What IS available for deriving duration:**
96
+ - The daemon registry tracks `startedAtMs` and `lastHeartbeatMs` per session -- but these are in-memory and ephemeral (lost on daemon restart)
97
+ - Filesystem mtime is used as `lastModifiedMs` in the console -- this is a proxy for "last event", not a start timestamp
98
+ - The very first event's `eventIndex = 0` is always `session_created`, but there is no timestamp in the event envelope
99
+
100
+ **Important finding:** The `context_set` projection (`projectRunContextV2`) and `SessionIndex` only retain the LATEST `context_set` event per run. This means agent-reported context keys are cumulative-overwrite, not append-only. A metrics key like `metrics.pr_numbers: [123]` submitted at step 3, then overwritten at step 5 with `metrics.pr_numbers: [123, 456]`, would work correctly. But if an agent only partially updates context at step 5, earlier keys survive (merge semantics).
101
+
102
+ ### Key architectural constraint
103
+
104
+ WorkRail has NO read access to the filesystem or git. It can only record what agents explicitly emit. This means:
105
+ - "Files changed during a session" requires the agent to self-report, OR WorkRail to capture HEAD SHA at start and end and compute the diff externally
106
+ - The git HEAD SHA IS already captured at session start via `observation_recorded` (key: `git_head_sha`). There is no end-of-session equivalent.
107
+
108
+ ---
109
+
110
+ ## Problem Frame Packet
111
+
112
+ **Primaryuncertainty:** Is this a convention gap (agents don't know what to report and where) or a mechanism gap (no suitable mechanism exists to record metrics)?
113
+
114
+ **Known approaches (from the original prompt):**
115
+ 1. (a) Auto-injected final step -- WorkRail adds a synthetic step at the end of every workflow asking the agent to report metrics
116
+ 2. (b) Step-type-aware mid-session injection -- WorkRail injects metric-collection prompts at specific step types
117
+ 3. (c) New `metrics` output contract type -- a new artifact contract `wr.contracts.metrics` that agents can submit at any step
118
+ 4. (d) `observation_recorded` events emitted by the agent at any step -- extend the observation key set
119
+ 5. (e) MCP handler layer interception -- WorkRail computes metrics automatically at the boundary without requiring agent participation
120
+
121
+ **Additional options surfaced from codebase reading:**
122
+ - (f) `context_set` as the metrics carrier -- use the existing `context_set` mechanism (already carries `JsonValue`) with a defined convention for metric keys (e.g., `metrics.pr_numbers`, `metrics.files_changed`)
123
+ - (g) Post-hoc session analysis tool -- a CLI or console feature that reads the event log + git log and derives metrics without any in-session changes
124
+ - (h) Capture HEAD SHA at session end in addition to start, enable external diff
125
+
126
+ ---
127
+
128
+ ## Problem Frame Packet
129
+
130
+ ### Primary users and jobs
131
+
132
+ **User 1: Developer reviewing completed sessions in the console**
133
+ - Job: "I want to glance at a session and immediately know what was accomplished -- what files changed, what PR was opened, how long it took."
134
+ - Current pain: The console only shows session status, node count, and a recap snippet. Nothing about code impact.
135
+
136
+ **User 2: Workflow author**
137
+ - Job: "I want basic metrics surfaced without adding boilerplate to every workflow I write."
138
+ - Current pain: There is no convention for which step to report what. Even if `context_set` is the right carrier, there is no prompt guidance, no schema, no validation.
139
+
140
+ **User 3: WorkTrain autonomous daemon operator**
141
+ - Job: "I want to see a summary of what each autonomous session accomplished, including LOC, PRs created, and whether it succeeded."
142
+ - Current pain: The daemon has `startedAtMs`/`lastHeartbeatMs` ephemerally, but nothing structured is persisted about outcomes.
143
+
144
+ ### Core tensions
145
+
146
+ 1. **Convention vs. enforcement.** Using `context_set` as a metrics carrier is zero-schema-change but relies entirely on agents following a convention. Without a schema, projection/query tools won't know where to look. With a schema (e.g., a new `wr.contracts.metrics` artifact), the bar is higher but the data is machine-verifiable.
147
+
148
+ 2. **Agent self-reporting vs. external observation.** The richest metrics (LOC changed, files touched) require the agent to run `git diff --stat` and report the numbers -- WorkRail cannot compute this. But agents are unreliable reporters: they may forget, lie, or be at a mid-session checkpoint. External observation (capturing HEAD SHA at start and end, then computing the diff post-hoc) is more trustworthy but requires capturing the end SHA somehow.
149
+
150
+ 3. **Per-step vs. end-of-session.** Some metrics (PR number, branch name) are only known at specific steps. Others (total LOC, final status) are only meaningful at completion. A single end-of-session mechanism is simpler but cannot capture mid-session facts.
151
+
152
+ 4. **Zero-workflow-change vs. author investment.** An auto-injected final step would work for any workflow without modification. But it intrudes on the agent experience and may be confusing in short utility workflows.
153
+
154
+ ### HMW questions
155
+
156
+ - How might we capture the HEAD SHA automatically at session completion without requiring the agent to self-report?
157
+ - How might we make it trivially easy for workflow authors to ask for metrics without designing a whole new step?
158
+
159
+ ### Primary framing risk
160
+
161
+ **The primary framing risk is: the problem is actually a query/visualization gap, not a capture gap.**
162
+
163
+ If agents ARE already reporting meaningful data via `context_set` (e.g., `pr_number`, `branch`, files touched) in the step notes -- even informally, in plain markdown -- then the right solution is a post-hoc parser or a richer console query, not new capture infrastructure. If this is true, the architecture we design for capture would add friction without improving the actual signal available to users.
164
+
165
+ Evidence that would confirm this risk: analyze a set of real session event logs and check whether agents already self-report outcomes. (We cannot do this here without access to live session data, but this should be verified before implementing any new mechanism.)
166
+
167
+ ## Candidate Generation Expectations
168
+
169
+ For this `full_spectrum` pass, candidates must:
170
+
171
+ 1. **Cover the full spectrum from minimal-change to structured-schema** -- at least one "zero new infrastructure" direction, and at least one "properly typed new mechanism" direction.
172
+ 2. **Include at least one direction that reframes the problem** -- specifically, the "post-hoc analysis" direction that asks: what if we don't change the capture layer at all and instead improve query/extraction of what's already there?
173
+ 3. **Respect landscape precedents** -- candidates must explain how they interact with `context_set`, `observation_recorded`, and the artifact contract system, not ignore them.
174
+ 4. **Be layered** -- each candidate must address the immediate need (self-reported metadata) separately from the longer-term need (automatic end-of-session observation, timestamps).
175
+ 5. **Be discriminating** -- candidates must be meaningfully different from each other, not just different names for the same approach.
176
+
177
+ ## Candidate Directions
178
+
179
+ ### Candidate A: `context_set` convention with a metrics projection layer (simplest sufficient)
180
+
181
+ **Summary:** Establish a documented key convention under `context.metrics.*` in `context_set` events. Add a `projectSessionMetricsV2` projection that extracts these keys. Expose `metrics: SessionMetricsV2 | null` in `ConsoleSessionSummary`.
182
+
183
+ **Tensions resolved:** minimal author burden, backward-compatible, zero schema change, queryable
184
+ **Tensions accepted:** weak type safety (JsonValue), no enforcement, agents must self-compute git stats
185
+
186
+ **Convention spec:**
187
+ - Agents set `context.metrics = { pr_numbers?: number[], files_changed_count?: number, lines_added?: number, lines_removed?: number, outcome?: 'success' | 'partial' | 'abandoned' | 'error' }` via `continue_workflow` `context` field
188
+ - Multiple `context_set` events: last one wins per run (existing semantics)
189
+ - New projection: `src/v2/projections/session-metrics.ts` → `projectSessionMetricsV2`
190
+ - New console field: `ConsoleSessionSummary.metrics: SessionMetricsV2 | null`
191
+
192
+ **Failure mode:** Inconsistent key names across agents. No enforcement signal.
193
+ **Pattern:** Follows `projectRunContextV2`. Same as how `isAutonomous` works.
194
+ **Gain:** Zero schema change, works today for any workflow.
195
+ **Give up:** Type safety, enforcement, reliable git quantitative metrics.
196
+ **Scope:** Best-fit for immediate need.
197
+ **Philosophy:** Honors YAGNI. Conflicts with "prefer explicit domain types."
198
+
199
+ ---
200
+
201
+ ### Candidate B: New `wr.contracts.metrics` artifact type (follow the artifact contract pattern)
202
+
203
+ **Summary:** Define a new Zod-validated artifact contract `wr.contracts.metrics`, placed on the `outputContract` of a workflow's final/summary step. Machine-verifiable at `continue_workflow` boundary.
204
+
205
+ **Artifact schema:**
206
+ ```typescript
207
+ MetricsArtifactV1Schema = z.object({
208
+ contractRef: z.literal('wr.contracts.metrics'),
209
+ v: z.literal(1),
210
+ outcome: z.enum(['success', 'partial', 'abandoned', 'error']),
211
+ prNumbers: z.array(z.number().int().positive()).optional(),
212
+ filesChangedCount: z.number().int().nonnegative().optional(),
213
+ linesAdded: z.number().int().nonnegative().optional(),
214
+ linesRemoved: z.number().int().nonnegative().optional(),
215
+ durationHint: z.string().optional(),
216
+ notes: z.string().optional(),
217
+ })
218
+ ```
219
+
220
+ **Tensions resolved:** type safety, validation at boundary, machine-queryable
221
+ **Tensions accepted:** author burden (outputContract required on final step), node-scoped (metrics should be session-scoped), not universal for existing workflows
222
+
223
+ **Failure mode:** Most workflows don't have a dedicated final step; existing workflows never report metrics without modification.
224
+ **Pattern:** Adapts `wr.contracts.assessment`, `wr.contracts.review_verdict`.
225
+ **Gain:** Type safety, enforcement, machine-queryable via existing artifact projection.
226
+ **Give up:** Universality, backward-compatibility (all workflows need update), simplicity.
227
+ **Scope:** Too broad for immediate need; appropriate as Phase 2 upgrade.
228
+ **Philosophy:** Honors "prefer explicit domain types", "validate at boundaries". Conflicts with YAGNI.
229
+
230
+ ---
231
+
232
+ ### Candidate C: End-of-session HEAD SHA observation + post-hoc diff (reframe: no agent self-reporting for git stats)
233
+
234
+ **Summary:** Emit a second `observation_recorded` with key `git_head_sha_end` when the workflow reaches `complete` state. Provide a console endpoint `GET /api/v2/sessions/:id/diff-summary` that computes `git diff --stat <start_sha>..<end_sha>` to derive authoritative LOC and files-changed.
235
+
236
+ **Tensions resolved:** agent reliability (git stats from git, not agent claims), no false precision, zero author burden for git-stat metrics
237
+ **Tensions accepted:** requires extending the closed `observation_recorded` enum, requires WorkRail to access git at query time (new capability), complex lifecycle hook
238
+
239
+ **Failure mode:** observation_recorded key set is a CLOSED discriminated union -- extending it requires a schema union update and exhaustive handler updates across the codebase. WorkRail currently has no git access at the MCP handler level.
240
+ **Pattern:** Extends the existing `observation_recorded` start-of-session pattern. The `advance_recorded` → `complete` path would need a new hook.
241
+ **Gain:** Authoritative git metrics without agent participation.
242
+ **Give up:** Schema change, git filesystem access in WorkRail (new boundary), significant implementation scope.
243
+ **Scope:** Best-fit for "authoritative git metrics" but too broad for Phase 1; should be Phase 2.
244
+ **Philosophy:** Honors "architectural fixes over patches", "determinism". Conflicts with YAGNI.
245
+
246
+ ---
247
+
248
+ ### Candidate D: Add `timestampMs` to the event envelope (foundational duration infrastructure)
249
+
250
+ **Summary:** Add optional `timestampMs: number` to `DomainEventEnvelopeV1Schema`, emitted by the MCP handler clock injection. Session duration = `last_event.timestampMs - first_event.timestampMs`.
251
+
252
+ **Tensions resolved:** session duration computable from event log, no agent participation, enables future event-level analytics
253
+ **Tensions accepted:** significant schema change, not backward-compatible (older sessions show null), adds size to every event
254
+
255
+ **Failure mode:** Existing sessions have no timestamps. Schema locks doc requires update. Requires clock injection into all event builders.
256
+ **Pattern:** Departs from existing pattern (events have no timestamps today). Requires updating `docs/design/v2-core-design-locks.md`.
257
+ **Gain:** Duration metrics without agent participation; future event-timing analytics.
258
+ **Give up:** Significant schema blast radius, YAGNI for "one metric."
259
+ **Scope:** Too broad as a "metrics" change; this is foundational infrastructure. Should be a separate proposal.
260
+ **Philosophy:** Honors "architectural fixes over patches". Conflicts with YAGNI significantly.
261
+
262
+ ---
263
+
264
+ ## Challenge Notes
265
+
266
+ **Challenged assumptions:**
267
+
268
+ 1. **WorkRail should own metric collection**
269
+ - Assumption: WorkRail is the right system to capture git stats, LOC, files changed.
270
+ - Why it might be wrong: WorkRail has no filesystem access. The git worktree state data is already read by the console's background enrichment scan -- but that is current state, not session-scoped history.
271
+ - Evidence needed: determine whether the delta (HEAD SHA at start vs. end) is sufficient for a post-hoc diff.
272
+
273
+ 2. **New schema/mechanism needed**
274
+ - Assumption: recording metrics requires new event types or output contracts.
275
+ - Why it might be wrong: `context_set` already accepts arbitrary `JsonValue` with agent deltas. The gap may be that agents don't know they should report metrics here, and that there's no extraction/projection layer consuming them.
276
+ - Evidence: `context_set` data carries `goal`, `is_autonomous`, `parentSessionId` already -- arbitrary agent-reported data is already possible.
277
+
278
+ 3. **Token cost per session is achievable**
279
+ - Assumption: token cost can be captured as part of this design.
280
+ - Why it's wrong: explicitly stated in the problem context -- MCP layer cannot observe per-tool-call token counts. Building architecture around this would be premature optimization.
281
+ - Evidence: confirmed by provided context.
282
+
283
+ ---
284
+
285
+ ## Resolution Notes
286
+
287
+ ### Recommendation: Phase 1 = Candidate A (with flat-key convention), Phase 2 = Candidate C
288
+
289
+ **Phase 1 recommendation (Candidate A, modified):**
290
+
291
+ Use `context_set` as the metrics carrier, but with FLAT TOP-LEVEL keys (not a nested `metrics` object), to avoid the shallow-merge wipe problem.
292
+
293
+ **Why flat keys, not nested:**
294
+
295
+ `mergeContext` (`src/v2/durable-core/domain/context-merge.ts`) uses SHALLOW merge semantics. Top-level keys are merged individually; nested objects are REPLACED. If agents write `context.metrics = { pr_numbers: [...] }` and later write `context.metrics = { lines_added: 50 }`, the `pr_numbers` key is lost. Flat keys (`context.metrics_pr_numbers`, `context.metrics_outcome`) avoid this by exploiting the per-top-level-key merge semantics.
296
+
297
+ **Flat-key convention:**
298
+ - `metrics_outcome: 'success' | 'partial' | 'abandoned' | 'error'` -- session outcome
299
+ - `metrics_pr_numbers: number[]` -- PR numbers created (array, last write wins per-key)
300
+ - `metrics_files_changed: number` -- advisory, agent-reported
301
+ - `metrics_lines_added: number` -- advisory, agent-reported
302
+ - `metrics_lines_removed: number` -- advisory, agent-reported
303
+ - `metrics_git_head_end: string` -- HEAD SHA at completion (agent self-reports; advisory until Phase 2 automates this)
304
+
305
+ **What changes:**
306
+ 1. `src/v2/projections/session-metrics.ts` -- new file, `projectSessionMetricsV2` reads `context_set` latest-wins projection and extracts `metrics_*` keys into a typed `SessionMetricsV2` object
307
+ 2. `src/v2/usecases/console-types.ts` -- add `metrics: SessionMetricsV2 | null` to `ConsoleSessionSummary`
308
+ 3. `src/v2/usecases/console-service.ts` -- call `projectSessionMetricsV2` and populate the field
309
+ 4. `docs/authoring-v2.md` -- document the `metrics_*` convention so workflow authors know what keys to set and when
310
+ 5. Console UI -- minimal "Metrics" row in session summary list and/or session detail panel
311
+
312
+ **What does NOT change:**
313
+ - No event schema changes
314
+ - No workflow compilation changes
315
+ - No existing workflow breakage
316
+ - No new event kinds
317
+ - No artifact contract changes
318
+
319
+ **Strongest counter-argument:** Flat metrics keys pollute the context namespace. A workflow author using `context.pr_number` (custom key) would silently conflict with `context.metrics_pr_numbers`. Convention requires discipline.
320
+
321
+ **Narrower option that lost:** Document the convention without adding a projection. Lost because queryability is a success criterion.
322
+
323
+ **Phase 2 recommendation (Candidate C):**
324
+
325
+ After Phase 1 establishes the convention and validates demand, add an end-of-session HEAD SHA mechanism:
326
+ - Extend `observation_recorded` enum with `git_head_sha_end` (or a new `session_completed` event that carries the end SHA and a step count)
327
+ - Console endpoint `GET /api/v2/sessions/:id/diff-summary` for authoritative LOC/files
328
+ - This should be a SEPARATE GitHub issue
329
+
330
+ **Candidates B and D:** Not recommended for this proposal.
331
+ - Candidate B (artifact contract): correct philosophy but wrong granularity (node-scoped). If enforcement is needed later, add it as an opt-in contract on a dedicated final step.
332
+ - Candidate D (event timestamps): separate infrastructure proposal. Track separately as `feat(engine): add event envelope timestamps`.
333
+
334
+ ### Comparison summary
335
+
336
+ | Criterion | A (flat context_set) | B (artifact contract) | C (end SHA) | D (timestamps) |
337
+ |-----------|---------------------|----------------------|-------------|----------------|
338
+ | Backward compatible | ✅ | ❌ | ⚠️ | ⚠️ |
339
+ | Author burden | minimal | high | zero | zero |
340
+ | Type safety | weak (projection) | strong | strong (git) | N/A |
341
+ | Scope | best-fit | too broad | best-fit (Phase 2) | too broad |
342
+ | Phase | 1 | 2 (optional) | 2 | separate |
343
+
344
+ ---
345
+
346
+ ## Decision Log
347
+
348
+ ### Selection: Candidate A (flat `context_set` convention + projection layer) for Phase 1
349
+
350
+ **Why Candidate A won:**
351
+ - Zero schema change: no event schema modifications, no workflow compilation changes, no breaking changes
352
+ - Best-fit boundary: the projection layer is exactly where existing advisory metadata (isAutonomous, gitBranch, parentSessionId) already lives
353
+ - `mergeContext` shallow semantics are safe for flat top-level keys (each `metrics_*` key persists independently)
354
+ - Backward-compatible: any existing session and any existing workflow continues to work unchanged
355
+ - The `isAutonomous` / `parentSessionId` precedent proves this is the approved WorkRail pattern for session-level advisory metadata
356
+
357
+ **Adversarial challenges survived:**
358
+ 1. "Convention will never be adopted" -- real concern; implementation must include workflow step prompting guidance, not just docs. Does not invalidate the architecture.
359
+ 2. "Flat keys pollute context" -- manageable with `metrics_` prefix convention. Not blocking.
360
+ 3. "Phase 2 should be Phase 1" -- Phase 1 reliably delivers `metrics_outcome` and `metrics_pr_numbers` (agent knows these). Phase 2 delivers authoritative git stats (agent can't reliably compute LOC). The phase split is valid.
361
+ 4. "Shallow merge erases history" -- REJECTED. Flat keys with `mergeContext` are safe; per-key persistence is correct.
362
+ 5. "Auto-injected final step is more reliable" -- REJECTED. Auto-injection is a patch, not an architectural fix. Convention + prompting guidance is more flexible.
363
+
364
+ **Why Candidate C (runner-up) lost Phase 1:**
365
+ - Requires extending the closed `observation_recorded` discriminated union (schema migration, exhaustive handler updates)
366
+ - Requires git access at the WorkRail MCP/console boundary (new capability not currently present)
367
+ - Complex lifecycle hook at session completion
368
+ - These are real engineering investments appropriate for Phase 2, not Phase 1
369
+
370
+ **Why Candidate B lost:**
371
+ - Node-scoped artifacts are architecturally mismatched for session-level metrics
372
+ - High author burden (outputContract on final step) for advisory data
373
+ - All existing workflows require modification
374
+
375
+ **Why Candidate D is deferred:**
376
+ - Event timestamp addition is foundational infrastructure orthogonal to metrics capture
377
+ - Separate proposal: `feat(engine): add event envelope timestamps`
378
+
379
+ ### Implementation notes (from adversarial challenge)
380
+
381
+ - Phase 1 MUST include concrete workflow step prompting guidance (not just convention docs) -- without prompting in the workflow itself, adoption is near-zero
382
+ - The most reliably self-reported metrics are: `metrics_outcome`, `metrics_pr_numbers`, `metrics_git_head_end` (advisory). Quantitative git stats (LOC, files) should wait for Phase 2
383
+ - Convention must be explicit that `metrics_pr_numbers` expects an array of integers (PR numbers), not URLs or strings
384
+
385
+ ---
386
+
387
+ ## Final Summary
388
+
389
+ ### Recommendation
390
+
391
+ **Phase 1: Flat `context_set` key convention + `projectSessionMetricsV2` projection layer**
392
+
393
+ **Confidence: HIGH.** The design has been adversarially challenged, reviewed, and all tradeoffs/failure modes are explicitly addressed. No blocking issues were found.
394
+
395
+ **What to build:**
396
+
397
+ 1. **Convention:** Agents use these flat top-level context keys (set via `continue_workflow`'s `context` field):
398
+ - `metrics_outcome: 'success' | 'partial' | 'abandoned' | 'error'` -- required, highest-value metric
399
+ - `metrics_pr_numbers: number[]` -- optional, array of PR numbers created
400
+ - `metrics_git_head_end: string` -- optional, HEAD SHA at session completion (advisory, will be automated in Phase 2)
401
+ - `metrics_files_changed: number` -- optional, agent-reported (advisory)
402
+ - `metrics_lines_added: number` -- optional, agent-reported (advisory)
403
+ - `metrics_lines_removed: number` -- optional, agent-reported (advisory)
404
+
405
+ 2. **New projection:** `src/v2/projections/session-metrics.ts`
406
+ - Pure function: `projectSessionMetricsV2(events: SortedEventLog): SessionMetricsV2 | null`
407
+ - Reads the latest `context_set` event per run, extracts `metrics_*` keys
408
+ - Defensive coercion: malformed values return null for that field (never throw)
409
+ - Returns `null` if no `metrics_*` keys are present
410
+
411
+ 3. **Console DTO update:** Add `metrics: SessionMetricsV2 | null` to `ConsoleSessionSummary` in `src/v2/usecases/console-types.ts`
412
+
413
+ 4. **Console service update:** Call `projectSessionMetricsV2` in `console-service.ts` and populate the field
414
+
415
+ 5. **Authoring docs (REQUIRED, not optional):**
416
+ - `docs/authoring-v2.md`: Document the `metrics_*` convention
417
+ - Include a concrete copy-paste step prompt template for the "report outcomes" step
418
+ - Include explicit WARNING: DO NOT use `context.metrics = {...}` (nested objects are replaced, not merged)
419
+
420
+ 6. **Console UI:** Advisory "agent-reported" label on all Phase 1 metric values
421
+
422
+ **Why flat keys, not nested `context.metrics`:** `mergeContext` is a shallow merge. Nested objects are replaced, not merged. If an agent sets `context.metrics = { pr_numbers: [123] }` at step 7 and `context.metrics = { lines_added: 50 }` at step 9, the `pr_numbers` key is LOST. Flat top-level keys (`metrics_pr_numbers`, `metrics_lines_added`) are independently persistent across multiple `context_set` events.
423
+
424
+ **Why `context_set` and not `wr.contracts.metrics` artifact:**
425
+ - Metrics are session-scoped; artifacts are node-scoped (wrong granularity)
426
+ - Artifact contracts require `outputContract` on specific steps; advisory metrics should be reportable at any step
427
+ - `context_set` is already the WorkRail pattern for session-level advisory metadata (`isAutonomous`, `parentSessionId`, `goal`)
428
+ - The advisory nature of self-reported data makes enforcement-heavy typed contracts over-engineered for Phase 1
429
+
430
+ ### Phase 2 (separate proposal)
431
+
432
+ **Authoritative git metrics via end-of-session HEAD SHA observation**
433
+
434
+ - Add `git_head_sha_end` to the `observation_recorded` key set (requires discriminated union update)
435
+ - Emit it automatically when the session reaches `complete` state
436
+ - Console endpoint `GET /api/v2/sessions/:id/diff-summary` that computes `git diff --stat <start>..<end>`
437
+ - This supersedes agent-reported `metrics_git_head_end`, `metrics_files_changed`, `metrics_lines_added/removed`
438
+
439
+ Prerequisite: `metrics_git_head_end` in Phase 1 convention creates a forward-compatible bridge (same key, automated source in Phase 2).
440
+
441
+ ### Deferred (separate proposals)
442
+
443
+ - **Event timestamps** (`timestampMs` in event envelope): Enables session duration from event log. Separate proposal: `feat(engine): add event envelope timestamps`
444
+ - **`wr.contracts.metrics` artifact contract**: If metrics ever need to drive workflow behavior (conditional branching), migrate from advisory `context_set` to a typed validated artifact. Escalation trigger documented.
445
+
446
+ ### Strongest alternative (runner-up)
447
+
448
+ Candidate C (end-of-session HEAD SHA + post-hoc diff) would be selected if the primary goal were authoritative git stats rather than general-purpose outcome reporting. It is the right Phase 2.
449
+
450
+ ### Residual risks
451
+
452
+ 1. **Framing risk unverified:** Sample 10-20 real session logs before shipping to confirm agents don't already self-report informally in a conflicting way.
453
+ 2. **Advisory `metrics_git_head_end` reliability:** Agents may report wrong commit (e.g., HEAD before final commit). Label as advisory; Phase 2 makes it authoritative.
454
+ 3. **Convention projection callsite latency:** `projectSessionMetricsV2` adds a projection call to session summary loading. Should be lightweight (single context key read) and covered by the existing summary cache.
@@ -0,0 +1,202 @@
1
+ # spawn-session-debug: Why dispatch() Never Starts Sessions
2
+
3
+ **Diagnosis type:** single_cause
4
+ **Confidence:** HIGH
5
+ **Date:** 2026-04-19
6
+
7
+ ---
8
+
9
+ ## Executive Summary
10
+
11
+ `routerRef.dispatch()` in `spawnSession` is silently killed by the 30-second deduplication
12
+ guard inside `TriggerRouter.dispatch()`. The session is pre-created by `executeStartWorkflow`
13
+ but the agent loop (`runWorkflowFn`) is never started. The session exists in the store as a
14
+ zombie: it has a `run_started` event but the `session_started` event and all subsequent
15
+ agent-loop events never appear.
16
+
17
+ ---
18
+
19
+ ## Exact Failure Point
20
+
21
+ **File:** `src/trigger/trigger-router.ts`, lines 847-866 (`dispatch()` method)
22
+
23
+ ```
24
+ dispatch(workflowTrigger: WorkflowTrigger): string {
25
+ {
26
+ const dedupeKey = `${workflowTrigger.goal}::${workflowTrigger.workspacePath}`;
27
+ ...
28
+ const lastDispatch = this._recentAdaptiveDispatches.get(dedupeKey);
29
+ if (lastDispatch !== undefined && now - lastDispatch < TriggerRouter.ADAPTIVE_DEDUPE_TTL_MS) {
30
+ console.log(`[TriggerRouter] Skipping duplicate dispatch: ...`);
31
+ return workflowTrigger.workflowId; // <-- EARLY RETURN, queue.enqueue() never called
32
+ }
33
+ this._recentAdaptiveDispatches.set(dedupeKey, now);
34
+ }
35
+
36
+ void this.queue.enqueue(workflowTrigger.workflowId, async () => { // never reached
37
+ ...
38
+ result = await this.runWorkflowFn(...); // never called
39
+ });
40
+ }
41
+ ```
42
+
43
+ The guard fires because `_recentAdaptiveDispatches` is a **shared map** on the `TriggerRouter`
44
+ instance, and `dispatchAdaptivePipeline()` writes to that same map with the same key format
45
+ (`goal::workspacePath`) moments before the child `dispatch()` call.
46
+
47
+ ---
48
+
49
+ ## Full Call Chain
50
+
51
+ ```
52
+ dispatchAdaptivePipeline(goal, workspace) [trigger-router.ts:1035]
53
+ --> _recentAdaptiveDispatches['goal::workspace'] = now <-- KEY SET HERE
54
+ --> runAdaptivePipeline(...)
55
+ --> runFullPipeline(...)
56
+ --> deps.stderr('[full-pipeline] Spawning wr.discovery session')
57
+ --> await deps.spawnSession('wr.discovery', goal, workspace)
58
+ --> executeStartWorkflow(...) SUCCESS: session + run written to store
59
+ --> parseContinueTokenOrFail(...) SUCCESS: sessionHandle decoded
60
+ --> routerRef.dispatch({
61
+ workflowId: 'wr.discovery',
62
+ goal, // <-- same string
63
+ workspacePath: workspace, // <-- same string
64
+ _preAllocatedStartResponse: ...
65
+ })
66
+ dedupeKey = 'goal::workspace' <-- SAME KEY
67
+ now - lastDispatch = ~0ms < 30000ms <-- DEDUP FIRES
68
+ return 'wr.discovery' <-- queue.enqueue() NEVER CALLED
69
+ --> return { kind: 'ok', value: sessionHandle }
70
+ --> awaitSessions([sessionHandle], 35 * 60 * 1000)
71
+ --> polls every 3s
72
+ --> session exists, run exists, status = in_progress (forever)
73
+ --> 35 minutes later: { outcome: 'timeout' }
74
+ ```
75
+
76
+ The `session_started` event (emitted by `workflow-runner.ts:3050`) only fires inside
77
+ `runWorkflow()`. Since `runWorkflowFn` is never called, `session_started` never appears.
78
+
79
+ ---
80
+
81
+ ## Why route() Works
82
+
83
+ `route()` constructs `workflowTrigger.goal` from `trigger.goal` in triggers.yml (or from
84
+ goalTemplate interpolation). That goal is a **different string** from the coordinator's
85
+ `opts.goal`. Different dedupeKey -> no collision -> `queue.enqueue()` is reached ->
86
+ `runWorkflowFn` runs.
87
+
88
+ Example:
89
+ - Coordinator's `opts.goal`: `"Implement feature X for workspace /Users/etienneb/git/personal/workrail"`
90
+ - Trigger's `trigger.goal` in triggers.yml: `"Review incoming PR"`
91
+ - Dedup keys: different -> no collision
92
+
93
+ ---
94
+
95
+ ## Why the Failure Is Silent
96
+
97
+ `dispatch()` returns `workflowTrigger.workflowId` (not an error) on the early-return path.
98
+ `spawnSession` only errors on explicit `{kind:'err'}` returns from `dispatch()` -- but it calls
99
+ `executeStartWorkflow` and `parseContinueTokenOrFail` BEFORE calling `dispatch()`, so by the
100
+ time `dispatch()` silently skips, `spawnSession` already has a valid `sessionHandle` and
101
+ returns `{kind:'ok', value:sessionHandle}`.
102
+
103
+ The coordinator receives a valid-looking handle, logs nothing unusual, and starts polling.
104
+
105
+ ---
106
+
107
+ ## Why Worktrees Are Never Created
108
+
109
+ Worktree creation happens inside `runWorkflow()` (the same function that never runs). This is a
110
+ downstream symptom of the same root cause, not a separate bug.
111
+
112
+ ---
113
+
114
+ ## Alternatives Ruled Out
115
+
116
+ | Hypothesis | Verdict | Reason |
117
+ |---|---|---|
118
+ | `executeStartWorkflow` fails silently | Eliminated | Session IS in the store; awaitSessions polls it but status never completes |
119
+ | `routerRef` undefined at call time | Eliminated | '[full-pipeline] Spawning...' log confirms full-pipeline ran past the routerRef guard |
120
+ | Queue serialization behind a stalled prior run | Not primary cause | Dedup fires before `queue.enqueue()` is ever reached |
121
+ | Workflow loader can't find `wr.discovery` | Eliminated | `executeStartWorkflow` succeeds (session created); failure here would cause spawnSession to return `{kind:'err'}`, not a hanging awaitSessions |
122
+
123
+ ---
124
+
125
+ ## Minimal Fix
126
+
127
+ The deduplication in `dispatch()` was designed to prevent the same top-level pipeline from
128
+ being dispatched twice in rapid succession (e.g. from webhook retries). It must not apply
129
+ to child sessions spawned by an already-running pipeline.
130
+
131
+ The `dispatch()` early return at lines 861-863 of `trigger-router.ts` should not fire when
132
+ the call comes from `spawnSession` (i.e. when `_preAllocatedStartResponse` is set). Three
133
+ approaches -- listed from lowest-blast-radius to highest:
134
+
135
+ ### Option A: Skip dedup when `_preAllocatedStartResponse` is present (targeted)
136
+
137
+ In `dispatch()`, bypass the dedup check when `_preAllocatedStartResponse` is set. This field
138
+ is only set by `spawnSession` (which already allocated the session -- dropping it would be
139
+ catastrophic). The presence of a pre-allocated response is authoritative evidence that the
140
+ caller explicitly intends to start this session.
141
+
142
+ ```typescript
143
+ // In dispatch(), before the dedup block:
144
+ if (workflowTrigger._preAllocatedStartResponse !== undefined) {
145
+ // Pre-allocated session: the caller already created the session in the store.
146
+ // Deduplication must not apply here -- dropping this dispatch would zombie the session.
147
+ void this.queue.enqueue(workflowTrigger.workflowId, async () => { ... });
148
+ return workflowTrigger.workflowId;
149
+ }
150
+ // ... existing dedup block follows
151
+ ```
152
+
153
+ ### Option B: Remove dedup from `dispatch()` entirely (simpler)
154
+
155
+ The dedup in `dispatch()` was never well-justified for child sessions. The HTTP console route
156
+ that calls `dispatch()` (console-routes.ts:868) is already protected by HTTP request
157
+ deduplication at a higher layer. Removing the dedup from `dispatch()` leaves only
158
+ `dispatchAdaptivePipeline()`'s own dedup (which is the correct protection point for
159
+ top-level pipeline dedup).
160
+
161
+ ### Option C: Separate dedup maps (cleanest isolation)
162
+
163
+ Give `dispatchAdaptivePipeline` its own dedup map instead of sharing
164
+ `_recentAdaptiveDispatches` with `dispatch()` and `route()`. This removes the cross-path
165
+ coupling entirely.
166
+
167
+ **Recommended: Option A.** It is the smallest possible change, is self-documenting, and
168
+ directly models the invariant: a pre-allocated session must always be started.
169
+
170
+ ---
171
+
172
+ ## Files Involved
173
+
174
+ - `src/trigger/trigger-router.ts`
175
+ - Lines 847-866: `dispatch()` dedup block (the exact failure point)
176
+ - Lines 996-1035: `dispatchAdaptivePipeline()` dedup block (the writer that poisons the key)
177
+ - Line 500: `_recentAdaptiveDispatches` declaration (shared map)
178
+ - `src/trigger/trigger-listener.ts`
179
+ - Lines 492-498: `spawnSession` calls `routerRef.dispatch()`
180
+
181
+ ---
182
+
183
+ ## Verification Recommendations
184
+
185
+ After applying a fix:
186
+
187
+ 1. **Unit test (regression):** Call `dispatchAdaptivePipeline(goal, workspace)`, then
188
+ immediately call `dispatch({workflowId:'wr.discovery', goal, workspacePath:workspace, _preAllocatedStartResponse:...})`.
189
+ Assert that `runWorkflowFn` is called exactly once (currently fails: `runWorkflowFn` is
190
+ never called due to dedup).
191
+
192
+ 2. **Integration smoke test:** Run the full-pipeline coordinator against a test workspace.
193
+ Verify `session_started` appears in the daemon event log for the spawned `wr.discovery`
194
+ session within 5 seconds.
195
+
196
+ 3. **Dedup regression:** Verify that calling `dispatchAdaptivePipeline` twice with the same
197
+ goal+workspace within 30 seconds still only starts one pipeline (the dedup guard in
198
+ `dispatchAdaptivePipeline` must still fire correctly).
199
+
200
+ 4. **Direct dispatch regression:** Verify that calling `dispatch()` (without
201
+ `_preAllocatedStartResponse`) twice with the same goal+workspace within 30 seconds still
202
+ deduplicates (if that behavior is intentional for the HTTP console route path).