@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.
- package/dist/application/services/compiler/template-registry.js +10 -1
- package/dist/cli/commands/worktrain-init.js +1 -1
- package/dist/console-ui/assets/{index-tOl8Vowf.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/manifest.json +25 -25
- package/dist/mcp/handlers/v2-workflow.js +1 -1
- package/dist/mcp/workflow-protocol-contracts.js +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 +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Implementation Plan: Discovery Loop Fix
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-19
|
|
4
|
+
**Branch:** `fix/discovery-loop-timeout-and-label`
|
|
5
|
+
**Commit:** `fix(coordinator): thread session timeouts, inspect PipelineOutcome, add sidecar idempotency`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Problem Statement
|
|
10
|
+
|
|
11
|
+
WorkTrain's pipeline re-ran `wr.discovery` on issue #393 at least 73 times over 19+ hours. Three root causes create an infinite re-selection loop:
|
|
12
|
+
|
|
13
|
+
1. `spawnSession()` passes no `agentConfig` -- sessions inherit `DEFAULT_SESSION_TIMEOUT_MINUTES=30`, but the coordinator waits 55 minutes. Sessions die at 30m; coordinator times out at 55m and escalates.
|
|
14
|
+
2. `PipelineOutcome` is silently discarded in `polling-scheduler.ts` -- no label is applied on escalation, so the issue is re-selected on every poll cycle.
|
|
15
|
+
3. `checkIdempotency()` sidecar scan is dead -- `persistTokens()` never writes a `context` field, so every session file is treated as `'clear'`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. Acceptance Criteria
|
|
20
|
+
|
|
21
|
+
- `spawnSession` with `agentConfig.maxSessionMinutes` threads through to `runWorkflowFn` (Fix 1)
|
|
22
|
+
- On `PipelineOutcome.kind === 'escalated'`, `applyGitHubLabel` is called with `worktrain:in-progress` (Fix 2)
|
|
23
|
+
- On `PipelineOutcome.kind === 'success'`, no label is applied (Fix 2)
|
|
24
|
+
- Issue-ownership sidecar is written before dispatch and deleted on completion (Fix 3)
|
|
25
|
+
- Expired sidecar (TTL exceeded) returns `'clear'` from `checkIdempotency` (Fix 3)
|
|
26
|
+
- `npm run build` passes with no errors
|
|
27
|
+
- `npx vitest run` passes with no regressions
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 3. Non-Goals
|
|
32
|
+
|
|
33
|
+
- Do NOT touch `src/mcp/`
|
|
34
|
+
- N-strike mechanism (label applied only after N escalations) -- deferred
|
|
35
|
+
- Coordinator-owns-termination refactor -- deferred
|
|
36
|
+
- Unassigning bot from issue on escalation -- not in spec
|
|
37
|
+
- Daemon startup sidecar cleanup -- deferred
|
|
38
|
+
- Exhaustive TypeScript switch on PipelineOutcome kinds -- deferred (if-check satisfies spec)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 4. Philosophy-Driven Constraints
|
|
43
|
+
|
|
44
|
+
- All new interface fields are `readonly`
|
|
45
|
+
- `applyGitHubLabel` uses injected `fetchFn` (not `globalThis.fetch` directly)
|
|
46
|
+
- Sidecar write failure must NOT block dispatch (fire-and-forget, log warn)
|
|
47
|
+
- `applyGitHubLabel` failure must NOT block poll cycle cleanup (log warn, continue)
|
|
48
|
+
- ESM imports use `.js` extension
|
|
49
|
+
- New types: discriminated union `PipelineOutcome` already exists -- use it, don't re-create
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 5. Invariants
|
|
54
|
+
|
|
55
|
+
1. **Fix 1 + Fix 2 ship together** -- deploying Fix 2 without Fix 1 causes every FULL-mode issue to escalate (30m timeout) and get permanently labeled
|
|
56
|
+
2. **Label = `worktrain:in-progress`** -- already in `queueConfig.excludeLabels` (polling-scheduler.ts:505); using a new label requires operator config change with no enforcement
|
|
57
|
+
3. **Sidecar TTL = `DISCOVERY_TIMEOUT_MS + 60_000`** ms -- handles crash case; expired sidecar = eligible for re-dispatch
|
|
58
|
+
4. **Conservative idempotency**: malformed sidecar = 'active' (never allow double-dispatch)
|
|
59
|
+
5. **Sidecar written before dispatch, deleted in both .then() and .catch()**
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 6. Selected Approach
|
|
64
|
+
|
|
65
|
+
**Candidate A**: Exact spec implementation.
|
|
66
|
+
|
|
67
|
+
- Fix 1: Add optional 5th param `agentConfig?` to `CoordinatorDeps.spawnSession` in `pr-review.ts`, thread through `trigger-listener.ts` to `routerRef.dispatch()`, pass correct timeouts at 4 spawn sites in `full-pipeline.ts`
|
|
68
|
+
- Fix 2: Change `Promise<unknown>` to `Promise<PipelineOutcome>` in `polling-scheduler.ts`, inspect outcome, add `applyGitHubLabel` private method
|
|
69
|
+
- Fix 3: Write `queue-issue-<N>.json` sidecar in `doPollGitHubQueue`, extend `checkIdempotency` to check sidecar files by filename pattern with TTL check
|
|
70
|
+
|
|
71
|
+
**Runner-up**: Candidate B (separate sidecar function). Lost: creates coordination risk with two callsites.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 7. Vertical Slices
|
|
76
|
+
|
|
77
|
+
### Slice 1: Fix 1 -- Thread maxSessionMinutes through spawnSession
|
|
78
|
+
|
|
79
|
+
**Files changed**:
|
|
80
|
+
- `src/coordinators/pr-review.ts` -- add optional `agentConfig?: { readonly maxSessionMinutes?: number; readonly maxTurns?: number }` as 5th param to `CoordinatorDeps.spawnSession`
|
|
81
|
+
- `src/coordinators/adaptive-pipeline.ts` -- `AdaptiveCoordinatorDeps` extends `CoordinatorDeps`, so this is automatically propagated; no change needed here
|
|
82
|
+
- `src/trigger/trigger-listener.ts` -- add `agentConfig?: { readonly maxSessionMinutes?: number; readonly maxTurns?: number }` as 5th param to `spawnSession` closure, forward to `routerRef.dispatch({ ..., agentConfig })`
|
|
83
|
+
- `src/coordinators/modes/full-pipeline.ts` -- pass `{ maxSessionMinutes: Math.ceil(DISCOVERY_TIMEOUT_MS / 60_000) }` (=55) at discovery spawn, `{ maxSessionMinutes: Math.ceil(SHAPING_TIMEOUT_MS / 60_000) }` (=35) at shaping spawn, `{ maxSessionMinutes: Math.ceil(REVIEW_TIMEOUT_MS / 60_000) }` (=25) at UX design spawn, `{ maxSessionMinutes: Math.ceil(CODING_TIMEOUT_MS / 60_000) }` (=65) at coding spawn
|
|
84
|
+
|
|
85
|
+
**Done when**: TypeScript compiles cleanly; `spawnSession` at all call sites passes the correct agentConfig
|
|
86
|
+
|
|
87
|
+
### Slice 2: Fix 2 -- Inspect PipelineOutcome and apply worktrain:in-progress label
|
|
88
|
+
|
|
89
|
+
**Files changed**:
|
|
90
|
+
- `src/trigger/polling-scheduler.ts`:
|
|
91
|
+
- Add import: `import type { PipelineOutcome } from '../coordinators/adaptive-pipeline.js';`
|
|
92
|
+
- Add import: `import { DISCOVERY_TIMEOUT_MS } from '../coordinators/adaptive-pipeline.js';`
|
|
93
|
+
- Change `Promise<unknown>` to `Promise<PipelineOutcome>` at L605-610
|
|
94
|
+
- Change `.then(() => {` to `.then((outcome: PipelineOutcome) => {`
|
|
95
|
+
- Add label application for `outcome.kind === 'escalated' || outcome.kind === 'dry_run'`
|
|
96
|
+
- Add private method `applyGitHubLabel(issueNumber: number, label: string, token: string, repo: string): Promise<void>`
|
|
97
|
+
- POST `https://api.github.com/repos/${repo}/issues/${issueNumber}/labels`
|
|
98
|
+
- Body: `JSON.stringify({ labels: [label] })`
|
|
99
|
+
- Headers: `Authorization: Bearer ${token}`, `Accept: application/vnd.github+json`, `Content-Type: application/json`
|
|
100
|
+
- Uses `(this.fetchFn as QueueFetchFn | undefined) ?? globalThis.fetch`
|
|
101
|
+
- Non-fatal: catch error, console.warn, return
|
|
102
|
+
|
|
103
|
+
**Done when**: On escalated/dry_run outcome, `applyGitHubLabel` is called with `('worktrain:in-progress', queueConfig.token, source.repo)`
|
|
104
|
+
|
|
105
|
+
### Slice 3: Fix 3 -- Write/delete sidecar and extend checkIdempotency
|
|
106
|
+
|
|
107
|
+
**Files changed**:
|
|
108
|
+
- `src/trigger/polling-scheduler.ts`:
|
|
109
|
+
- Before dispatch call: write `queue-issue-<N>.json` to `sessionsDir` with `{ issueNumber, triggerId, dispatchedAt: Date.now(), ttlMs: DISCOVERY_TIMEOUT_MS + 60_000 }`
|
|
110
|
+
- In `.then()` handler: delete the sidecar file (fire-and-forget, ignore errors)
|
|
111
|
+
- In `.catch()` handler: delete the sidecar file (fire-and-forget, ignore errors)
|
|
112
|
+
- `src/trigger/adapters/github-queue-poller.ts`:
|
|
113
|
+
- In `checkIdempotency`, before the main scan loop, check if `queue-issue-${issueNumber}.json` exists in `sessionsDir`
|
|
114
|
+
- If file exists: parse it, check `dispatchedAt + ttlMs > Date.now()` -- if true (not expired), return 'active'; if false (expired), return 'clear' for this file
|
|
115
|
+
- On any parse error for the sidecar: return 'active' (conservative)
|
|
116
|
+
- Update JSDoc to describe sidecar file format
|
|
117
|
+
|
|
118
|
+
**Done when**: Sidecar is written before dispatch, deleted on completion; `checkIdempotency` returns 'active' for active sidecar and 'clear' for expired sidecar
|
|
119
|
+
|
|
120
|
+
### Slice 4: Tests
|
|
121
|
+
|
|
122
|
+
**New file**: `tests/unit/discovery-loop-fix.test.ts`
|
|
123
|
+
|
|
124
|
+
**Test cases**:
|
|
125
|
+
|
|
126
|
+
1. `spawnSession with agentConfig.maxSessionMinutes threads through to dispatch call`
|
|
127
|
+
- Create a fake `routerRef` that captures dispatch calls
|
|
128
|
+
- Call `coordinatorDeps.spawnSession('wr.discovery', goal, workspace, undefined, { maxSessionMinutes: 55 })`
|
|
129
|
+
- Assert dispatch was called with `agentConfig: { maxSessionMinutes: 55 }`
|
|
130
|
+
- Note: This tests trigger-listener.ts via integration or by testing the coordinator deps implementation directly
|
|
131
|
+
|
|
132
|
+
2. `On PipelineOutcome.kind === escalated, applyGitHubLabel is called with worktrain:in-progress`
|
|
133
|
+
- Create a fake router with `dispatchAdaptivePipeline` returning `{ kind: 'escalated', escalationReason: { phase: 'discovery', reason: 'timeout' } }`
|
|
134
|
+
- Create a fake fetchFn that captures calls
|
|
135
|
+
- Run `doPollGitHubQueue` (via private method cast)
|
|
136
|
+
- Assert fetchFn was called with URL containing `/labels` and body containing `worktrain:in-progress`
|
|
137
|
+
|
|
138
|
+
3. `On PipelineOutcome.kind === success, no label is applied`
|
|
139
|
+
- Same setup but `dispatchAdaptivePipeline` returns `{ kind: 'merged', prUrl: 'https://github.com/...' }`
|
|
140
|
+
- Assert fetchFn was NOT called with a labels URL
|
|
141
|
+
|
|
142
|
+
4. `Issue-ownership sidecar is written before dispatch and deleted on completion`
|
|
143
|
+
- Create a tmpDir for sessionsDir
|
|
144
|
+
- Run `doPollGitHubQueue`
|
|
145
|
+
- Assert sidecar file exists after dispatch setup but is deleted after completion
|
|
146
|
+
- Note: Since dispatch is async, check sidecar presence inside the `dispatchAdaptivePipeline` mock
|
|
147
|
+
|
|
148
|
+
5. `Expired sidecar (TTL exceeded) returns 'clear' from checkIdempotency`
|
|
149
|
+
- Write a sidecar file with `{ issueNumber: 42, triggerId: 'x', dispatchedAt: 0, ttlMs: 1 }` (already expired)
|
|
150
|
+
- Call `checkIdempotency(42, tmpDir)`
|
|
151
|
+
- Assert result is `'clear'`
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 8. Test Design
|
|
156
|
+
|
|
157
|
+
**File**: `tests/unit/discovery-loop-fix.test.ts`
|
|
158
|
+
**Framework**: Vitest with `vi.fn()` fakes
|
|
159
|
+
**Fixtures**: tmpDir for session files, fake fetchFn capturing calls, fake router returning specific PipelineOutcome kinds
|
|
160
|
+
|
|
161
|
+
Key patterns from existing tests:
|
|
162
|
+
- Private method access: `(scheduler as unknown as { doPollGitHubQueue(...) }).doPollGitHubQueue(...)`
|
|
163
|
+
- Fake router: `{ dispatchAdaptivePipeline: async (...) => { return { kind: 'escalated', ... } } }`
|
|
164
|
+
- Queue config: use `vi.mock` or provide a mock `loadQueueConfig`
|
|
165
|
+
|
|
166
|
+
Note: The `spawnSession` agentConfig threading test (test case 1) may be better placed in an integration test that creates a real `trigger-listener.ts` environment. Given the complexity, a unit test that mocks the router and verifies the dispatch call args is acceptable.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 9. Risk Register
|
|
171
|
+
|
|
172
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
173
|
+
|---|---|---|---|
|
|
174
|
+
| GitHub token expired at label application time | Low | Issue loops again | Warn log is the signal; no code mitigation needed |
|
|
175
|
+
| Sidecar accumulation if delete fails | Low | Manual cleanup needed | TTL handles cleanup after 56m |
|
|
176
|
+
| TypeScript error from 5th optional param on test fakes | Low | Build fails | vi.fn() mocks don't enforce param count |
|
|
177
|
+
| Import of DISCOVERY_TIMEOUT_MS creates circular dep | Low | Build fails | adaptive-pipeline.ts has no deps on polling-scheduler.ts |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 10. PR Packaging Strategy
|
|
182
|
+
|
|
183
|
+
**Single PR** on branch `fix/discovery-loop-timeout-and-label`
|
|
184
|
+
All 3 fixes + tests in one PR. Fixes 1 and 2 are coupled -- they cannot be split.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 11. Philosophy Alignment
|
|
189
|
+
|
|
190
|
+
| Principle | Slice | Status |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| Immutability by default | 1 (agentConfig fields readonly) | Satisfied |
|
|
193
|
+
| Type safety | 2 (Promise<PipelineOutcome>) | Satisfied |
|
|
194
|
+
| Errors are data | 2 (applyGitHubLabel fire-and-forget) | Tension -- acceptable for non-fatal I/O |
|
|
195
|
+
| Dependency injection | 2 (fetchFn injected) | Satisfied |
|
|
196
|
+
| Exhaustiveness | 2 (all 3 outcome kinds handled) | Satisfied |
|
|
197
|
+
| YAGNI | All | Satisfied -- no extra abstractions |
|
|
198
|
+
| Document why not what | 3 (JSDoc update for checkIdempotency) | Satisfied |
|
|
199
|
+
| Determinism | 3 (TTL check adds wall-clock dep) | Tension -- acceptable for crash recovery |
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Implementation Plan: queue-poll.jsonl rotation
|
|
2
|
+
|
|
3
|
+
_Date: 2026-04-21_
|
|
4
|
+
|
|
5
|
+
## 1. Problem Statement
|
|
6
|
+
|
|
7
|
+
`~/.workrail/queue-poll.jsonl` grows without bound. At 5-minute polling intervals it accumulates ~8.7 MB/month; at 1-minute intervals ~87 MB/month. The daemon memory audit rates this Critical. Additionally, the `worktrain logs --follow` command uses an offset-based reader that assumes the file only grows -- after rotation (when the file is replaced), the stale offset permanently stops the reader from showing new events.
|
|
8
|
+
|
|
9
|
+
## 2. Acceptance Criteria
|
|
10
|
+
|
|
11
|
+
1. `queue-poll.jsonl` never exceeds ~10 MB + one write cycle.
|
|
12
|
+
2. `queue-poll.jsonl.1` exists after the first rotation and contains the most recent pre-rotation entries.
|
|
13
|
+
3. `worktrain logs --follow` continues showing events after rotation (offset reset on shrink detection).
|
|
14
|
+
4. Rotation failures log a warning via `console.warn` and do not crash or stop polling.
|
|
15
|
+
5. The 'permanent file that never rotates' comment in `src/cli-worktrain.ts` is updated.
|
|
16
|
+
6. `npx tsc --noEmit` passes.
|
|
17
|
+
7. `npx vitest run` passes.
|
|
18
|
+
|
|
19
|
+
## 3. Non-Goals
|
|
20
|
+
|
|
21
|
+
- No configurable size threshold (hardcoded 10 MB).
|
|
22
|
+
- No date-named rotation files (backup is `queue-poll.jsonl.1` only).
|
|
23
|
+
- No changes to `daemon.stderr.log`.
|
|
24
|
+
- `worktrain logs` does NOT show backup file content.
|
|
25
|
+
- No multiple backup generations (no `.2`, `.3`, etc.).
|
|
26
|
+
|
|
27
|
+
## 4. Philosophy-Driven Constraints
|
|
28
|
+
|
|
29
|
+
- **Errors are data**: rotation failures use `console.warn`, never throw.
|
|
30
|
+
- **YAGNI**: no helper function extracted (single use case).
|
|
31
|
+
- **Architectural fixes over patches**: update the 'permanent file' comment so code and documentation stay in sync.
|
|
32
|
+
- **Determinism**: stat before append ensures rotation decision is based on current state.
|
|
33
|
+
|
|
34
|
+
## 5. Invariants
|
|
35
|
+
|
|
36
|
+
- I1: File size checked BEFORE each append (stat before appendFile).
|
|
37
|
+
- I2: If size >= 10 MB, rename to `.1` (overwriting existing backup) before appending.
|
|
38
|
+
- I3: Reader: if `stat.size < queuePollOffset`, reset `queuePollOffset = 0`.
|
|
39
|
+
- I4: Rotation is fire-and-forget -- inner try/catch around stat/rename; outer try/catch for the full function.
|
|
40
|
+
- I5: ENOENT on stat is caught by inner try/catch and falls through to appendFile (creates file).
|
|
41
|
+
|
|
42
|
+
## 6. Selected Approach + Rationale + Runner-Up
|
|
43
|
+
|
|
44
|
+
**Selected**: Candidate A -- inline stat+rename in `appendQueuePollLog` + shrink detection in `--follow` loop.
|
|
45
|
+
|
|
46
|
+
**Rationale**: Minimal footprint, follows existing fire-and-forget pattern exactly, zero new abstractions. Both writer and reader fixes in the correct location.
|
|
47
|
+
|
|
48
|
+
**Runner-up**: Candidate B (extracted `rotateIfNeeded` helper). Lost because YAGNI -- no other callers need the function.
|
|
49
|
+
|
|
50
|
+
## 7. Vertical Slices
|
|
51
|
+
|
|
52
|
+
### Slice 1: Writer fix (`src/trigger/polling-scheduler.ts`)
|
|
53
|
+
- Add `const MAX_QUEUE_POLL_FILE_SIZE = 10 * 1024 * 1024` constant before the class.
|
|
54
|
+
- Rewrite `appendQueuePollLog` to: stat file, rename to `.1` if size >= threshold, then append.
|
|
55
|
+
- Update or remove the existing comment about never rotating (if any).
|
|
56
|
+
- Done when: `appendQueuePollLog` rotates the file at >= 10 MB and the backup exists.
|
|
57
|
+
|
|
58
|
+
### Slice 2: Reader fix (`src/cli-worktrain.ts`)
|
|
59
|
+
- Update the comment at lines 685 and 892-893 from 'permanent file that never rotates' to reflect rotation.
|
|
60
|
+
- Add shrink detection before `readNewLines(queuePollPath, queuePollOffset)` in the `--follow` loop: `if (stat.size < queuePollOffset) { queuePollOffset = 0; }`.
|
|
61
|
+
- Done when: `--follow` resets offset on file shrinkage and continues showing events.
|
|
62
|
+
|
|
63
|
+
### Slice 3: Verification
|
|
64
|
+
- Run `npx tsc --noEmit` -- must pass.
|
|
65
|
+
- Run `npx vitest run` -- must pass.
|
|
66
|
+
|
|
67
|
+
## 8. Test Design
|
|
68
|
+
|
|
69
|
+
The existing `tests/unit/polling-scheduler.test.ts` does not mock `os.homedir()`, making it difficult to test `appendQueuePollLog` rotation in isolation without significant test infrastructure changes. The pitch does not require new unit tests for the rotation logic (only 'CI passes'). Verification is through TypeScript compilation and the existing test suite.
|
|
70
|
+
|
|
71
|
+
If future tests are added for rotation, they should mock `fs.stat`, `fs.rename`, and `fs.appendFile` using vitest's `vi.mock` or inject a file-system abstraction.
|
|
72
|
+
|
|
73
|
+
## 9. Risk Register
|
|
74
|
+
|
|
75
|
+
| Risk | Severity | Mitigation |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Concurrent rotation race | Yellow | Acknowledged and accepted per pitch. At most 1-2 log lines lost. |
|
|
78
|
+
| EACCES causing unbounded growth | Yellow | console.warn via try/catch. Acceptable for diagnostic log. |
|
|
79
|
+
| Reader fix missed (writer-only PR) | Red | Both slices MUST ship in the same PR. |
|
|
80
|
+
|
|
81
|
+
## 10. PR Packaging Strategy
|
|
82
|
+
|
|
83
|
+
**SinglePR**: `fix/etienneb/queue-poll-rotation`
|
|
84
|
+
- Commit: `fix(engine): add size-capped rotation for queue-poll.jsonl at 10 MB`
|
|
85
|
+
- Both slices in one commit.
|
|
86
|
+
- MUST NOT be split into writer-only and reader-only PRs.
|
|
87
|
+
|
|
88
|
+
## 11. Philosophy Alignment per Slice
|
|
89
|
+
|
|
90
|
+
### Slice 1 (Writer)
|
|
91
|
+
- Errors are data -> satisfied (console.warn not throw)
|
|
92
|
+
- YAGNI -> satisfied (no helper extracted)
|
|
93
|
+
- Determinism -> satisfied (stat before append)
|
|
94
|
+
- Architectural fixes over patches -> satisfied (not a special case, changes the invariant)
|
|
95
|
+
|
|
96
|
+
### Slice 2 (Reader)
|
|
97
|
+
- Architectural fixes over patches -> satisfied (shrink detection is the correct invariant change)
|
|
98
|
+
- Document why not what -> satisfied (comment update explains rotation now happens)
|
|
99
|
+
- Errors are data -> N/A (statSync in try block for reader)
|
|
100
|
+
|
|
101
|
+
### Slice 3 (Verification)
|
|
102
|
+
- Type safety as first line of defense -> satisfied (tsc --noEmit)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# In-Process HTTP Audit: Daemon Calling Its Own API
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-19
|
|
4
|
+
**Scope:** WorkTrain daemon coordinator deps -- calls from inside the daemon process to the daemon's own HTTP console (port 3456) or webhook (port 3200) servers.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
The daemon's in-process `coordinatorDeps` block (wired in `src/trigger/trigger-listener.ts:410-752`) contains **two functions** that make HTTP calls to the daemon's own console server at port 3456. Both functions should use `ConsoleService` directly instead.
|
|
11
|
+
|
|
12
|
+
**Bugs confirmed: 2**
|
|
13
|
+
**False positives excluded: CLI HTTP calls (correct design)**
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Every HTTP-to-Self Call Found
|
|
18
|
+
|
|
19
|
+
### Bug 1: `awaitSessions` -- HTTP polling for session status
|
|
20
|
+
|
|
21
|
+
**File:line:** `src/trigger/trigger-listener.ts:499-540`
|
|
22
|
+
|
|
23
|
+
**What it does:**
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
awaitSessions: async (handles: readonly string[], timeoutMs: number) => {
|
|
27
|
+
const { executeWorktrainAwaitCommand } = await import('../cli/commands/worktrain-await.js');
|
|
28
|
+
await executeWorktrainAwaitCommand(
|
|
29
|
+
{
|
|
30
|
+
fetch: (url: string) => globalThis.fetch(url), // <-- real HTTP fetch
|
|
31
|
+
...
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
sessions: [...handles].join(','),
|
|
35
|
+
port: DAEMON_CONSOLE_PORT, // 3456
|
|
36
|
+
...
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`executeWorktrainAwaitCommand` calls `pollSession()` in `worktrain-await.ts` which builds:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
GET http://127.0.0.1:3456/api/v2/sessions/<sessionHandle>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
...every 3 seconds until the session status is terminal (`complete`, `complete_with_gaps`, `blocked`, `dormant`).
|
|
48
|
+
|
|
49
|
+
**Is it broken today?** Yes. Two active failure modes:
|
|
50
|
+
1. **Port unavailable:** If another process holds port 3456, or if `startDaemonConsole()` hasn't been called yet, the `fetch()` calls return `ECONNREFUSED` and all sessions are reported as `failed`.
|
|
51
|
+
2. **Race condition:** A session created in-process by `spawnSession()` may not yet be visible to the HTTP layer when the first poll fires. The HTTP layer reads from the same session store, but there is no synchronization guarantee between the in-process write and the HTTP server's view.
|
|
52
|
+
|
|
53
|
+
**Used by:** `full-pipeline.ts` (discovery, shaping, ux-gate, coding sessions), `implement.ts` (ux-gate, coding), `implement-shared.ts` (review, fix-agent, audit, re-review), `pr-review.ts` (review, fix, re-review).
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### Bug 2: `getAgentResult` -- HTTP fetching for artifacts and recap
|
|
58
|
+
|
|
59
|
+
**File:line:** `src/trigger/trigger-listener.ts:542-609`
|
|
60
|
+
|
|
61
|
+
**What it does:**
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
getAgentResult: async (sessionHandle: string) => {
|
|
65
|
+
// Step 1: get session detail
|
|
66
|
+
const sessionUrl = `http://127.0.0.1:${DAEMON_CONSOLE_PORT}/api/v2/sessions/${encodeURIComponent(sessionHandle)}`;
|
|
67
|
+
const sessionRes = await globalThis.fetch(sessionUrl, { signal: AbortSignal.timeout(30_000) });
|
|
68
|
+
// ... extracts runs[0].preferredTipNodeId and all nodeIds ...
|
|
69
|
+
|
|
70
|
+
// Step 2: fetch each node
|
|
71
|
+
const baseNodeUrl = `http://127.0.0.1:${DAEMON_CONSOLE_PORT}/api/v2/sessions/${encodeURIComponent(sessionHandle)}/nodes/`;
|
|
72
|
+
for (const nodeId of nodeIdsToFetch) {
|
|
73
|
+
const nodeRes = await globalThis.fetch(baseNodeUrl + encodeURIComponent(nodeId), ...);
|
|
74
|
+
// ... extracts recapMarkdown and artifacts ...
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Makes 1 + N HTTP calls (1 for session detail, N for each node) to the daemon's own console server to collect `recapMarkdown` (for verdict keyword-scan fallback) and `artifacts` (for typed verdict reading via `readVerdictArtifact`).
|
|
80
|
+
|
|
81
|
+
**Is it broken today?** Yes. Same failure modes as Bug 1. Additionally:
|
|
82
|
+
- Artifacts written during in-process session execution may not be flushed to the HTTP-visible layer before `getAgentResult` is called
|
|
83
|
+
- Each node fetch has a 30-second timeout; for sessions with many nodes this adds latency
|
|
84
|
+
|
|
85
|
+
**Used by:** `full-pipeline.ts` (after discovery, to read handoff artifact), `implement-shared.ts` (after review, to read verdict artifact and recap).
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 2. Priority Ranking
|
|
90
|
+
|
|
91
|
+
| Rank | Bug | Impact | Blocks pipeline today? |
|
|
92
|
+
|------|-----|--------|----------------------|
|
|
93
|
+
| 1 | `awaitSessions` HTTP polling (Bug 1) | All pipelines hang or report sessions as failed if port 3456 unavailable | Yes |
|
|
94
|
+
| 2 | `getAgentResult` HTTP fetching (Bug 2) | Discovery handoff context missing; review verdict read fails if port 3456 unavailable | Yes -- silently degrades to empty artifacts / keyword-scan only |
|
|
95
|
+
|
|
96
|
+
Both bugs are blocking, not cosmetic. Bug 1 is higher priority because it affects every pipeline phase (all spawned sessions must be awaited). Bug 2 causes a cascade: if `getAgentResult` returns empty artifacts after a coding session, the review verdict cannot be read via the typed path and falls back to keyword-scan on `recapMarkdown`, which is itself also empty -- causing an escalation with reason `review verdict parse failed`.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 3. Recommended Fix for Each
|
|
101
|
+
|
|
102
|
+
### Fix for Bug 1: `awaitSessions` -- In-process polling via ConsoleService
|
|
103
|
+
|
|
104
|
+
**Replace** the `executeWorktrainAwaitCommand` delegation with a direct polling loop using `ConsoleService.getSessionDetail()`.
|
|
105
|
+
|
|
106
|
+
**In-process equivalent:** `ConsoleService.getSessionDetail(sessionId)` returns `ConsoleSessionDetail` which includes `runs[0].status: ConsoleRunStatus`. The status values (`complete`, `complete_with_gaps`, `blocked`) map directly to the existing `statusToOutcome()` logic in `worktrain-await.ts`.
|
|
107
|
+
|
|
108
|
+
**What to build:**
|
|
109
|
+
- Construct a `ConsoleService` instance in `startTriggerListener()` using the `ctx.v2` deps already available (`ctx.v2.sessionStore`, `ctx.v2.snapshotStore`, `ctx.v2.pinnedStore`, `ctx.v2.dataDir`, `ctx.v2.directoryListing`)
|
|
110
|
+
- Replace `awaitSessions` with an in-process polling loop that calls `consoleService.getSessionDetail(handle)` every ~3 seconds until `runs[0].status` is terminal
|
|
111
|
+
- Handle `dormant` separately: since `ConsoleService.getSessionDetail()` returns `ConsoleRunStatus` (not `ConsoleSessionStatus`), `dormant` won't appear in `runs[0].status`. The polling loop's own timeout covers the dormant case -- sessions that go quiet will be caught by `timeoutMs`
|
|
112
|
+
- Wire the `ConsoleService` instance into `coordinatorDeps` via closure (same pattern as `routerRef`)
|
|
113
|
+
|
|
114
|
+
**Key files:**
|
|
115
|
+
- `src/trigger/trigger-listener.ts` -- replace `awaitSessions` implementation
|
|
116
|
+
- `src/v2/usecases/console-service.ts` -- `ConsoleService.getSessionDetail()` -- no changes needed
|
|
117
|
+
- `src/v2/usecases/console-types.ts` -- `ConsoleSessionDetail`, `ConsoleRunStatus` -- no changes needed
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### Fix for Bug 2: `getAgentResult` -- In-process node detail via ConsoleService
|
|
122
|
+
|
|
123
|
+
**Replace** the two-phase HTTP fetch with `ConsoleService.getSessionDetail()` (for node IDs) and `ConsoleService.getNodeDetail()` (for per-node recap and artifacts).
|
|
124
|
+
|
|
125
|
+
**In-process equivalent:**
|
|
126
|
+
- `ConsoleService.getSessionDetail(sessionId)` returns `ConsoleSessionDetail.runs[0].nodes: readonly ConsoleDagNode[]` where each `ConsoleDagNode` has `nodeId` and `isPreferredTip`
|
|
127
|
+
- `ConsoleService.getNodeDetail(sessionId, nodeId)` returns `ConsoleNodeDetail` with `recapMarkdown: string | null` and `artifacts: readonly ConsoleArtifact[]` -- exactly what the current HTTP implementation extracts
|
|
128
|
+
|
|
129
|
+
**What to build:**
|
|
130
|
+
- Reuse the same `ConsoleService` instance constructed for Bug 1 fix
|
|
131
|
+
- Replace `getAgentResult` with an in-process version that calls `getSessionDetail` then `getNodeDetail` for each node
|
|
132
|
+
- Preserve the existing logic: collect artifacts from all nodes; collect `recapMarkdown` from the preferred-tip node only
|
|
133
|
+
|
|
134
|
+
**Key files:**
|
|
135
|
+
- `src/trigger/trigger-listener.ts` -- replace `getAgentResult` implementation
|
|
136
|
+
- `src/v2/usecases/console-service.ts` -- `ConsoleService.getNodeDetail()` -- no changes needed
|
|
137
|
+
- `src/v2/usecases/console-types.ts` -- `ConsoleNodeDetail` -- no changes needed
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 4. The Correct Architecture
|
|
142
|
+
|
|
143
|
+
### Current (broken) wiring
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
TriggerListener (daemon process)
|
|
147
|
+
├── startTriggerListener() constructs coordinatorDeps
|
|
148
|
+
│ ├── awaitSessions: calls executeWorktrainAwaitCommand
|
|
149
|
+
│ │ └── polls http://127.0.0.1:3456/api/v2/sessions/<id> ← HTTP-to-self
|
|
150
|
+
│ └── getAgentResult: calls globalThis.fetch
|
|
151
|
+
│ └── fetches http://127.0.0.1:3456/api/v2/sessions/<id>/nodes/... ← HTTP-to-self
|
|
152
|
+
└── startDaemonConsole() constructs ConsoleService (separately)
|
|
153
|
+
└── ConsoleService.getSessionDetail() ← in-process, not used by coordinatorDeps
|
|
154
|
+
└── ConsoleService.getNodeDetail() ← in-process, not used by coordinatorDeps
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Target (correct) wiring
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
TriggerListener (daemon process)
|
|
161
|
+
└── startTriggerListener() constructs:
|
|
162
|
+
├── consoleService = new ConsoleService({ ctx.v2.sessionStore, ... })
|
|
163
|
+
└── coordinatorDeps
|
|
164
|
+
├── awaitSessions: in-process polling loop
|
|
165
|
+
│ └── consoleService.getSessionDetail(handle).runs[0].status ← no HTTP
|
|
166
|
+
└── getAgentResult: in-process node reading
|
|
167
|
+
├── consoleService.getSessionDetail(handle).runs[0].nodes ← no HTTP
|
|
168
|
+
└── consoleService.getNodeDetail(handle, nodeId) ← no HTTP
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Why the same ConsoleService instance works for both
|
|
172
|
+
|
|
173
|
+
`ConsoleService` is a stateless projection reader -- it reads from `ctx.v2.sessionStore` (the same append-only event log written by the in-process session execution). Sessions created in-process by `spawnSession()` write their events to the same store that `ConsoleService` reads. There is no HTTP layer in this path -- the data is immediately available after commit.
|
|
174
|
+
|
|
175
|
+
### What should NOT use in-process access
|
|
176
|
+
|
|
177
|
+
The CLI `run pr-review` command (`src/cli-worktrain.ts:1265-1501`) is a **separate process** from the daemon. Its `spawnSession`, `awaitSessions`, and `getAgentResult` implementations correctly use HTTP to communicate with a running daemon. These are NOT bugs and should not be changed.
|
|
178
|
+
|
|
179
|
+
### AdaptiveCoordinatorDeps interface notes
|
|
180
|
+
|
|
181
|
+
The `AdaptiveCoordinatorDeps` interface (`src/coordinators/adaptive-pipeline.ts:131-169`) and the `CoordinatorDeps` interface it extends (`src/coordinators/pr-review.ts:131+`) define the dep function signatures but make no assumptions about transport. The interface is correct as-is. Only the concrete implementations in `trigger-listener.ts` need to change.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Excluded from Scope
|
|
186
|
+
|
|
187
|
+
- WorkRail MCP server HTTP calls (out of scope per investigation brief)
|
|
188
|
+
- CLI `run pr-review` HTTP calls (external process, correct design)
|
|
189
|
+
- Webhook server (port 3200) -- no HTTP-to-self calls to this port found
|
|
190
|
+
- `polling-scheduler.ts`, `trigger-router.ts` -- no HTTP-to-self calls found
|
|
@@ -16,7 +16,7 @@ The console's session detail view shows a DAG of workflow execution nodes via `R
|
|
|
16
16
|
|
|
17
17
|
### Core Tensions
|
|
18
18
|
|
|
19
|
-
1. **Labels vs simplicity**: Step labels (human-readable titles like "Phase 0: Triage and classify") are what make ghost nodes useful. But resolving them requires the compiled workflow, which is a backend I/O operation. Skipping labels is simpler but produces raw step IDs (`routine-context-gathering-depth`) that users can't parse.
|
|
19
|
+
1. **Labels vs simplicity**: Step labels (human-readable titles like "Phase 0: Triage and classify") are what make ghost nodes useful. But resolving them requires the compiled workflow, which is a backend I/O operation. Skipping labels is simpler but produces raw step IDs (`wr.routine-context-gathering-depth`) that users can't parse.
|
|
20
20
|
|
|
21
21
|
2. **Type safety vs ease**: Adding `isGhost: boolean` to `ConsoleDagNode` is one line but violates "make illegal states unrepresentable" -- ghost steps have no `hasRecap`, `hasFailedValidations`, `isTip`, `parentNodeId`, `createdAtEventIndex`, etc. A separate `ConsoleGhostStep` interface is correct but requires touching both mirrored type files.
|
|
22
22
|
|
|
@@ -91,7 +91,7 @@ The `buildLineageDagModel` signature does NOT need to change -- ghost positionin
|
|
|
91
91
|
**Summary**: Extract skipped step IDs from `evaluated_condition` SKIP trace items on the frontend; render ghost nodes without step labels (show raw step ID).
|
|
92
92
|
|
|
93
93
|
**Tensions resolved**: Zero backend changes. No mirrored type file sync needed.
|
|
94
|
-
**Tensions accepted**: No step labels. Ghost nodes show raw IDs like `routine-context-gathering-depth`.
|
|
94
|
+
**Tensions accepted**: No step labels. Ghost nodes show raw IDs like `wr.routine-context-gathering-depth`.
|
|
95
95
|
|
|
96
96
|
**Boundary**: `session-detail-use-cases.ts` -- new pure function `getSkippedStepsFromTrace(items: readonly ConsoleExecutionTraceItem[]): readonly string[]` returning step IDs. `RunLineageDag.tsx` -- new sub-feature D `useMemo` computes ghost positions from active lineage model, renders absolute-positioned `GhostNodeOverlay` components.
|
|
97
97
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Candidate Directions: loadSessionNotes Test Coverage
|
|
2
|
+
|
|
3
|
+
**Context:** Issue #393. A complete test implementation (14 tests, all passing) already exists
|
|
4
|
+
uncommitted on disk. The design question is: which approach to land?
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Candidate A: Ship the existing implementation as-is (Direct export + vi.mock)
|
|
9
|
+
|
|
10
|
+
**Summary:** Export `loadSessionNotes` from `src/daemon/workflow-runner.ts` (already done in
|
|
11
|
+
working tree) and ship `tests/unit/workflow-runner-load-session-notes.test.ts` (already written,
|
|
12
|
+
14/14 passing) as a single focused PR closing issue #393.
|
|
13
|
+
|
|
14
|
+
**Why it fits the path:** This is the `design_first` fast-exit conclusion. The design decision
|
|
15
|
+
was already made by a prior session. Shipping it closes the triggering issue and stops the
|
|
16
|
+
"done but not shipped" daemon loop.
|
|
17
|
+
|
|
18
|
+
**Strongest evidence for it:**
|
|
19
|
+
- Implementation is 100% complete, verified, and ready. Zero additional work required.
|
|
20
|
+
- `workflow-runner-spawn-agent.test.ts` establishes the vi.mock + vi.hoisted precedent for the
|
|
21
|
+
same pattern of module-level dependency stubbing.
|
|
22
|
+
- All 7 success criteria (Phase 1f) are satisfied.
|
|
23
|
+
|
|
24
|
+
**Strongest risk against it:**
|
|
25
|
+
- The export adds a name to `workflow-runner.ts`'s public surface that wasn't there before.
|
|
26
|
+
If the module is later refactored, the export may need to move. Low probability — the
|
|
27
|
+
function is already stable and the module is protected.
|
|
28
|
+
- vi.mock is considered a weaker pattern than fakes per project philosophy. The test is
|
|
29
|
+
correct but not architecturally ideal.
|
|
30
|
+
|
|
31
|
+
**When it wins:** When the goal is closing #393 cleanly with zero additional implementation
|
|
32
|
+
risk. This is the correct choice for an autonomous agent operating with surgical constraints.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Candidate B: Extract first, then ship (Module extraction + pure fake tests)
|
|
37
|
+
|
|
38
|
+
**Summary:** Move `loadSessionNotes` (and its constants `MAX_SESSION_NOTE_CHARS`,
|
|
39
|
+
`MAX_SESSION_RECAP_NOTES`) into a new module `src/daemon/session-recap-loader.ts`. Export
|
|
40
|
+
from there. Update the single import in `workflow-runner.ts`. Rewrite
|
|
41
|
+
`tests/unit/workflow-runner-load-session-notes.test.ts` to import from the new module and
|
|
42
|
+
use real fakes instead of vi.mock.
|
|
43
|
+
|
|
44
|
+
**Why it fits the path:** This is the "stronger reframe" the design_first path asks for.
|
|
45
|
+
It solves the underlying architectural concern: `workflow-runner.ts` is already large
|
|
46
|
+
(3500+ lines), and session recap loading is a separable concern. Better module boundaries
|
|
47
|
+
reduce future vi.mock dependency in tests.
|
|
48
|
+
|
|
49
|
+
**Strongest evidence for it:**
|
|
50
|
+
- Prior session's discovery (see design doc) explicitly recommended Option A (extraction)
|
|
51
|
+
over Option B (direct export).
|
|
52
|
+
- Project philosophy: "prefer fakes over mocks" and "compose with small, pure functions."
|
|
53
|
+
- A separate module makes `loadSessionNotes` testable without any vi.mock overhead.
|
|
54
|
+
- `src/daemon/session-recap-loader.ts` would be a natural home alongside
|
|
55
|
+
`session-recap` related code.
|
|
56
|
+
|
|
57
|
+
**Strongest risk against it:**
|
|
58
|
+
- Requires undoing the already-implemented working-tree changes and writing ~150 additional
|
|
59
|
+
lines across two files.
|
|
60
|
+
- `src/daemon/` is listed as a protected directory in AGENTS.md — autonomous modification
|
|
61
|
+
requires the change to be strictly surgical. Extraction is more invasive than export.
|
|
62
|
+
- Every day this work sits undone, issue #393 stays open and the daemon fires more
|
|
63
|
+
discovery sessions. Candidate B takes longer.
|
|
64
|
+
- The test file written for Candidate A (vi.mock) is perfectly functional and follows
|
|
65
|
+
established precedent.
|
|
66
|
+
|
|
67
|
+
**When it wins:** When the project owner explicitly prefers module extraction over direct
|
|
68
|
+
export for architectural cleanliness, and is willing to accept a slightly larger PR.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## What would change the verdict
|
|
73
|
+
|
|
74
|
+
Switch from Candidate A to Candidate B if **either** of these is true:
|
|
75
|
+
1. The project owner confirms that `src/daemon/session-recap-loader.ts` was the intended
|
|
76
|
+
approach before implementation started (i.e., Option A from the prior session was
|
|
77
|
+
the authoritative decision, not merely a recommendation).
|
|
78
|
+
2. A code review of the PR for Candidate A explicitly requests module extraction as a
|
|
79
|
+
blocking concern before merge.
|
|
80
|
+
|
|
81
|
+
In the absence of either signal, Candidate A wins: it is already implemented, already
|
|
82
|
+
tested, follows established project precedent, and closes the issue immediately.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Candidate C (considered, not recommended): Make contracts visible via Result types
|
|
87
|
+
|
|
88
|
+
**Summary:** Change `loadSessionNotes` to return `Result<readonly string[], Error>` instead
|
|
89
|
+
of silently returning `[]` on failure. Force the caller to decide how to handle each failure
|
|
90
|
+
mode explicitly. Tests then assert on `Result` discriminants rather than `[]` shortcircuits.
|
|
91
|
+
|
|
92
|
+
**Why it is genuinely different:** This reframes the problem from "test the silent-fail
|
|
93
|
+
function" to "make the function's contracts visible at the type level." It addresses the
|
|
94
|
+
root cause: the function hides its failure modes from callers and from tests.
|
|
95
|
+
|
|
96
|
+
**Why it is not recommended:**
|
|
97
|
+
- Requires changing `loadSessionNotes` signature AND the `Promise.all` caller at line 3498
|
|
98
|
+
in `src/daemon/workflow-runner.ts` (protected file, more invasive than export-only).
|
|
99
|
+
- The three failure paths are ALL best-effort recoveries — there is no meaningful action
|
|
100
|
+
the caller can take differently based on which failure occurred. Silent `[]` IS the correct
|
|
101
|
+
contract for a best-effort context-injection helper.
|
|
102
|
+
- Result types add value when the caller needs to branch. Here, the caller always passes
|
|
103
|
+
`[]` to `buildSessionRecap`, which returns `''` for empty. The Result type would be
|
|
104
|
+
immediately `.unwrapOr([])`'d — adding type complexity with no behavioral change.
|
|
105
|
+
- Scope creep relative to issue #393.
|
|
106
|
+
|
|
107
|
+
**When it wins:** Never, for this specific problem. Might be correct in a future refactor
|
|
108
|
+
that redesigns the session recap injection architecture more broadly.
|