@exaudeus/workrail 3.66.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.
- package/dist/application/services/compiler/template-registry.js +10 -1
- package/dist/application/validation.js +1 -1
- package/dist/cli/commands/worktrain-init.js +1 -1
- package/dist/console/standalone-console.js +4 -1
- package/dist/console-ui/assets/{index-BynU38Vu.js → index-CyzltI6D.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/modes/full-pipeline.js +4 -4
- package/dist/coordinators/modes/implement-shared.js +5 -5
- package/dist/coordinators/modes/implement.js +4 -4
- package/dist/coordinators/pr-review.js +4 -4
- package/dist/daemon/workflow-runner.d.ts +1 -0
- package/dist/daemon/workflow-runner.js +1 -0
- package/dist/infrastructure/storage/schema-validating-workflow-storage.d.ts +21 -2
- package/dist/infrastructure/storage/schema-validating-workflow-storage.js +48 -0
- package/dist/manifest.json +41 -41
- package/dist/mcp/handlers/v2-workflow.js +24 -7
- package/dist/mcp/output-schemas.d.ts +36 -0
- package/dist/mcp/output-schemas.js +11 -1
- package/dist/mcp/workflow-protocol-contracts.js +2 -2
- package/dist/v2/projections/session-metrics.d.ts +1 -1
- package/dist/v2/projections/session-metrics.js +16 -35
- package/dist/v2/usecases/console-routes.d.ts +2 -2
- package/docs/authoring-v2.md +4 -4
- package/docs/changelog-recent.md +3 -3
- package/docs/configuration.md +1 -1
- package/docs/design/adaptive-coordinator-context-candidates.md +1 -1
- package/docs/design/adaptive-coordinator-context.md +1 -1
- package/docs/design/adaptive-coordinator-routing-candidates.md +18 -18
- package/docs/design/adaptive-coordinator-routing-review.md +1 -1
- package/docs/design/adaptive-coordinator-routing.md +34 -34
- package/docs/design/agent-cascade-protocol.md +2 -2
- package/docs/design/console-daemon-separation-discovery.md +323 -0
- package/docs/design/context-assembly-design-candidates.md +1 -1
- package/docs/design/context-assembly-implementation-plan.md +1 -1
- package/docs/design/context-assembly-layer.md +2 -2
- package/docs/design/context-assembly-review-findings.md +1 -1
- package/docs/design/coordinator-access-audit.md +293 -0
- package/docs/design/coordinator-architecture-audit.md +62 -0
- package/docs/design/coordinator-error-handling-audit.md +240 -0
- package/docs/design/coordinator-testability-audit.md +426 -0
- package/docs/design/daemon-architecture-discovery.md +1 -1
- package/docs/design/daemon-console-separation-discovery.md +242 -0
- package/docs/design/daemon-memory-audit.md +203 -0
- package/docs/design/design-candidates-console-daemon-separation.md +256 -0
- package/docs/design/design-candidates-discovery-loop-fix.md +141 -0
- package/docs/design/design-review-findings-console-daemon-separation.md +106 -0
- package/docs/design/design-review-findings-discovery-loop-fix.md +81 -0
- package/docs/design/discovery-loop-fix-candidates.md +161 -0
- package/docs/design/discovery-loop-fix-design-review.md +106 -0
- package/docs/design/discovery-loop-fix-validation.md +258 -0
- package/docs/design/discovery-loop-investigation-A.md +188 -0
- package/docs/design/discovery-loop-investigation-B.md +287 -0
- package/docs/design/exploration-workflow-candidates.md +205 -0
- package/docs/design/exploration-workflow-design-review.md +166 -0
- package/docs/design/exploration-workflow-discovery.md +443 -0
- package/docs/design/ide-context-files-candidates.md +231 -0
- package/docs/design/ide-context-files-design-review.md +85 -0
- package/docs/design/ide-context-files.md +615 -0
- package/docs/design/implementation-plan-discovery-loop-fix.md +199 -0
- package/docs/design/implementation-plan-queue-poll-rotation.md +102 -0
- package/docs/design/in-process-http-audit.md +190 -0
- package/docs/design/layer3b-ghost-nodes-design-candidates.md +2 -2
- package/docs/design/loadSessionNotes-candidates.md +108 -0
- package/docs/design/loadSessionNotes-test-coverage-discovery.md +297 -0
- package/docs/design/loadSessionNotes-test-coverage-session4.md +209 -0
- package/docs/design/loadSessionNotes-test-coverage-v3.md +321 -0
- package/docs/design/probe-session-design-candidates.md +261 -0
- package/docs/design/probe-session-phase0.md +490 -0
- package/docs/design/routines-guide.md +7 -7
- package/docs/design/session-metrics-attribution-candidates.md +250 -0
- package/docs/design/session-metrics-attribution-design-review.md +115 -0
- package/docs/design/session-metrics-attribution-discovery.md +319 -0
- package/docs/design/session-metrics-candidates.md +227 -0
- package/docs/design/session-metrics-design-review.md +104 -0
- package/docs/design/session-metrics-discovery.md +454 -0
- package/docs/design/spawn-session-debug.md +202 -0
- package/docs/design/trigger-validator-candidates.md +214 -0
- package/docs/design/trigger-validator-review.md +109 -0
- package/docs/design/trigger-validator-shaping-phase0.md +239 -0
- package/docs/design/trigger-validator.md +454 -0
- package/docs/design/v2-core-design-locks.md +2 -2
- package/docs/design/workflow-extension-points.md +15 -15
- package/docs/design/workflow-id-validation-at-startup.md +1 -1
- package/docs/design/workflow-id-validation-implementation-plan.md +2 -2
- package/docs/design/workflow-trigger-lifecycle-audit.md +175 -0
- package/docs/design/worktrain-task-queue-candidates.md +5 -5
- package/docs/design/worktrain-task-queue.md +4 -4
- package/docs/discovery/coordinator-script-design.md +1 -1
- package/docs/discovery/coordinator-ux-discovery.md +3 -3
- package/docs/discovery/simulation-report.md +1 -1
- package/docs/discovery/workflow-modernization-discovery.md +326 -0
- package/docs/discovery/workflow-selection-for-discovery-tasks.md +33 -33
- package/docs/discovery/worktrain-status-briefing.md +1 -1
- package/docs/discovery/wr-discovery-goal-reframing.md +1 -1
- package/docs/docker.md +1 -1
- package/docs/ideas/backlog.md +227 -0
- package/docs/ideas/third-party-workflow-setup-design-thinking.md +1 -1
- package/docs/integrations/claude-code.md +5 -5
- package/docs/integrations/firebender.md +1 -1
- package/docs/plans/agentic-orchestration-roadmap.md +2 -2
- package/docs/plans/mr-review-workflow-redesign.md +9 -9
- package/docs/plans/ui-ux-workflow-design-candidates.md +4 -4
- package/docs/plans/ui-ux-workflow-discovery.md +2 -2
- package/docs/plans/workflow-categories-candidates.md +8 -8
- package/docs/plans/workflow-categories-discovery.md +4 -4
- package/docs/plans/workflow-modernization-design.md +430 -0
- package/docs/plans/workflow-staleness-detection-candidates.md +11 -11
- package/docs/plans/workflow-staleness-detection-review.md +4 -4
- package/docs/plans/workflow-staleness-detection.md +9 -9
- package/docs/plans/workrail-platform-vision.md +3 -3
- package/docs/reference/agent-context-cleaner-snippet.md +1 -1
- package/docs/reference/agent-context-guidance.md +4 -4
- package/docs/reference/context-optimization.md +2 -2
- package/docs/roadmap/now-next-later.md +2 -2
- package/docs/roadmap/open-work-inventory.md +16 -16
- package/docs/workflows.md +31 -31
- package/package.json +1 -1
- package/spec/workflow-tags.json +47 -47
- package/workflows/adaptive-ticket-creation.json +16 -16
- package/workflows/architecture-scalability-audit.json +22 -22
- package/workflows/bug-investigation.agentic.v2.json +3 -3
- package/workflows/classify-task-workflow.json +1 -1
- package/workflows/coding-task-workflow-agentic.json +6 -6
- package/workflows/cross-platform-code-conversion.v2.json +8 -8
- package/workflows/document-creation-workflow.json +8 -8
- package/workflows/documentation-update-workflow.json +8 -8
- package/workflows/intelligent-test-case-generation.json +2 -2
- package/workflows/learner-centered-course-workflow.json +2 -2
- package/workflows/mr-review-workflow.agentic.v2.json +4 -4
- package/workflows/personal-learning-materials-creation-branched.json +8 -8
- package/workflows/presentation-creation.json +5 -5
- package/workflows/production-readiness-audit.json +1 -1
- package/workflows/relocation-workflow-us.json +31 -31
- package/workflows/routines/context-gathering.json +1 -1
- package/workflows/routines/design-review.json +1 -1
- package/workflows/routines/execution-simulation.json +1 -1
- package/workflows/routines/feature-implementation.json +3 -3
- package/workflows/routines/final-verification.json +1 -1
- package/workflows/routines/hypothesis-challenge.json +1 -1
- package/workflows/routines/ideation.json +1 -1
- package/workflows/routines/parallel-work-partitioning.json +3 -3
- package/workflows/routines/philosophy-alignment.json +2 -2
- package/workflows/routines/plan-analysis.json +1 -1
- package/workflows/routines/plan-generation.json +1 -1
- package/workflows/routines/tension-driven-design.json +6 -6
- package/workflows/scoped-documentation-workflow.json +26 -26
- package/workflows/ui-ux-design-workflow.json +14 -14
- package/workflows/workflow-diagnose-environment.json +1 -1
- package/workflows/workflow-for-workflows.json +32 -77
- package/workflows/workflow-for-workflows.v2.json +0 -788
|
@@ -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).
|