@exaudeus/workrail 3.59.2 → 3.59.4
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/console-ui/assets/{index-BN-nUCJ2.js → index-BuMfiLrV.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/modes/full-pipeline.js +43 -11
- package/dist/coordinators/modes/implement-shared.js +84 -17
- package/dist/coordinators/modes/implement.js +18 -1
- package/dist/coordinators/pr-review.d.ts +1 -1
- package/dist/manifest.json +17 -17
- package/dist/trigger/trigger-listener.js +83 -72
- package/dist/v2/usecases/console-routes.js +4 -1
- package/dist/v2/usecases/worktree-service.js +6 -2
- package/docs/design/coordinator-in-process-await-candidates.md +128 -0
- package/docs/design/coordinator-in-process-await-design-review.md +93 -0
- package/docs/design/coordinator-io-error-handling-candidates.md +199 -0
- package/docs/design/coordinator-io-error-handling-design-review.md +120 -0
- package/docs/ideas/backlog.md +100 -0
- package/package.json +1 -1
|
@@ -38,7 +38,18 @@ async function runReviewAndVerdictCycle(deps, opts, prUrl, coordinatorStartMs, i
|
|
|
38
38
|
escalationReason: { phase: 'review', reason: `review session ${outcome}` },
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
let agentResult;
|
|
42
|
+
try {
|
|
43
|
+
agentResult = await deps.getAgentResult(reviewHandle);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
47
|
+
deps.stderr(`[coordinator] getAgentResult failed: ${msg}`);
|
|
48
|
+
return {
|
|
49
|
+
kind: 'escalated',
|
|
50
|
+
escalationReason: { phase: 'review', reason: `getAgentResult threw: ${msg}` },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
42
53
|
const verdictFromArtifact = (0, pr_review_js_1.readVerdictArtifact)(agentResult.artifacts, reviewHandle);
|
|
43
54
|
const findingsResult = verdictFromArtifact !== null
|
|
44
55
|
? { kind: 'ok', value: verdictFromArtifact }
|
|
@@ -59,7 +70,12 @@ async function runReviewAndVerdictCycle(deps, opts, prUrl, coordinatorStartMs, i
|
|
|
59
70
|
case 'minor': {
|
|
60
71
|
if (iteration >= exports.MAX_FIX_ITERATIONS) {
|
|
61
72
|
deps.stderr(`[review-cycle] ${exports.MAX_FIX_ITERATIONS} fix iterations exhausted -- escalating`);
|
|
62
|
-
|
|
73
|
+
try {
|
|
74
|
+
await deps.postToOutbox(`Adaptive pipeline escalated: fix loop exhausted after ${exports.MAX_FIX_ITERATIONS} iterations`, { prUrl, phase: 'fix-loop', reason: 'max iterations reached', findingSummaries: findings.findingSummaries });
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
deps.stderr(`[WARN coordinator] postToOutbox failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
78
|
+
}
|
|
63
79
|
return {
|
|
64
80
|
kind: 'escalated',
|
|
65
81
|
escalationReason: { phase: 'fix-loop', reason: `${exports.MAX_FIX_ITERATIONS} fix iterations exhausted` },
|
|
@@ -112,7 +128,12 @@ async function runAuditChain(deps, opts, prUrl, coordinatorStartMs, severity, fi
|
|
|
112
128
|
: 'production-readiness-audit';
|
|
113
129
|
const auditSpawnResult = await deps.spawnSession(auditWorkflow, `Audit PR before merge: ${prUrl}`, opts.workspace, { prUrl, severity });
|
|
114
130
|
if (auditSpawnResult.kind === 'err') {
|
|
115
|
-
|
|
131
|
+
try {
|
|
132
|
+
await deps.postToOutbox(`Adaptive pipeline escalated: audit workflow failed to spawn`, { prUrl, phase: 'audit', reason: auditSpawnResult.error, severity });
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
deps.stderr(`[WARN coordinator] postToOutbox failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
136
|
+
}
|
|
116
137
|
return {
|
|
117
138
|
kind: 'escalated',
|
|
118
139
|
escalationReason: { phase: 'audit', reason: `audit spawn failed: ${auditSpawnResult.error}` },
|
|
@@ -120,7 +141,12 @@ async function runAuditChain(deps, opts, prUrl, coordinatorStartMs, severity, fi
|
|
|
120
141
|
}
|
|
121
142
|
const auditHandle = auditSpawnResult.value;
|
|
122
143
|
if (!auditHandle) {
|
|
123
|
-
|
|
144
|
+
try {
|
|
145
|
+
await deps.postToOutbox(`Adaptive pipeline escalated: audit returned empty handle`, { prUrl, phase: 'audit', severity });
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
deps.stderr(`[WARN coordinator] postToOutbox failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
149
|
+
}
|
|
124
150
|
return {
|
|
125
151
|
kind: 'escalated',
|
|
126
152
|
escalationReason: { phase: 'audit', reason: 'audit returned empty handle' },
|
|
@@ -130,7 +156,12 @@ async function runAuditChain(deps, opts, prUrl, coordinatorStartMs, severity, fi
|
|
|
130
156
|
const auditResult = auditAwait.results[0];
|
|
131
157
|
if (!auditResult || auditResult.outcome !== 'success') {
|
|
132
158
|
const outcome = auditResult?.outcome ?? 'not_found';
|
|
133
|
-
|
|
159
|
+
try {
|
|
160
|
+
await deps.postToOutbox(`Adaptive pipeline escalated: audit session ${outcome}`, { prUrl, phase: 'audit', auditOutcome: outcome, severity });
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
deps.stderr(`[WARN coordinator] postToOutbox failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
164
|
+
}
|
|
134
165
|
return {
|
|
135
166
|
kind: 'escalated',
|
|
136
167
|
escalationReason: { phase: 'audit', reason: `audit session ${outcome}` },
|
|
@@ -142,7 +173,12 @@ async function runAuditChain(deps, opts, prUrl, coordinatorStartMs, severity, fi
|
|
|
142
173
|
return reReviewCutoff;
|
|
143
174
|
const reReviewSpawnResult = await deps.spawnSession('mr-review-workflow-agentic', `Re-review after audit: ${prUrl}`, opts.workspace, { prUrl, auditComplete: true });
|
|
144
175
|
if (reReviewSpawnResult.kind === 'err') {
|
|
145
|
-
|
|
176
|
+
try {
|
|
177
|
+
await deps.postToOutbox(`Adaptive pipeline escalated: re-review after audit failed to spawn`, { prUrl, phase: 're-review-after-audit', reason: reReviewSpawnResult.error });
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
deps.stderr(`[WARN coordinator] postToOutbox failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
181
|
+
}
|
|
146
182
|
return {
|
|
147
183
|
kind: 'escalated',
|
|
148
184
|
escalationReason: {
|
|
@@ -153,7 +189,12 @@ async function runAuditChain(deps, opts, prUrl, coordinatorStartMs, severity, fi
|
|
|
153
189
|
}
|
|
154
190
|
const reReviewHandle = reReviewSpawnResult.value;
|
|
155
191
|
if (!reReviewHandle) {
|
|
156
|
-
|
|
192
|
+
try {
|
|
193
|
+
await deps.postToOutbox(`Adaptive pipeline escalated: re-review after audit returned empty handle`, { prUrl, phase: 're-review-after-audit' });
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
deps.stderr(`[WARN coordinator] postToOutbox failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
197
|
+
}
|
|
157
198
|
return {
|
|
158
199
|
kind: 'escalated',
|
|
159
200
|
escalationReason: { phase: 're-review-after-audit', reason: 're-review returned empty handle' },
|
|
@@ -163,19 +204,40 @@ async function runAuditChain(deps, opts, prUrl, coordinatorStartMs, severity, fi
|
|
|
163
204
|
const reReviewResult = reReviewAwait.results[0];
|
|
164
205
|
if (!reReviewResult || reReviewResult.outcome !== 'success') {
|
|
165
206
|
const outcome = reReviewResult?.outcome ?? 'not_found';
|
|
166
|
-
|
|
207
|
+
try {
|
|
208
|
+
await deps.postToOutbox(`Adaptive pipeline escalated: re-review after audit session ${outcome}`, { prUrl, phase: 're-review-after-audit', reReviewOutcome: outcome });
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
deps.stderr(`[WARN coordinator] postToOutbox failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
212
|
+
}
|
|
167
213
|
return {
|
|
168
214
|
kind: 'escalated',
|
|
169
215
|
escalationReason: { phase: 're-review-after-audit', reason: `re-review session ${outcome}` },
|
|
170
216
|
};
|
|
171
217
|
}
|
|
172
|
-
|
|
218
|
+
let reAgentResult;
|
|
219
|
+
try {
|
|
220
|
+
reAgentResult = await deps.getAgentResult(reReviewHandle);
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
224
|
+
deps.stderr(`[coordinator] getAgentResult failed: ${msg}`);
|
|
225
|
+
return {
|
|
226
|
+
kind: 'escalated',
|
|
227
|
+
escalationReason: { phase: 'review', reason: `getAgentResult threw: ${msg}` },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
173
230
|
const reVerdictFromArtifact = (0, pr_review_js_1.readVerdictArtifact)(reAgentResult.artifacts, reReviewHandle);
|
|
174
231
|
const reFindingsResult = reVerdictFromArtifact !== null
|
|
175
232
|
? { kind: 'ok', value: reVerdictFromArtifact }
|
|
176
233
|
: (0, pr_review_js_1.parseFindingsFromNotes)(reAgentResult.recapMarkdown);
|
|
177
234
|
if (reFindingsResult.kind === 'err') {
|
|
178
|
-
|
|
235
|
+
try {
|
|
236
|
+
await deps.postToOutbox(`Adaptive pipeline escalated: re-review verdict unparseable after audit`, { prUrl, phase: 're-review-after-audit' });
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
deps.stderr(`[WARN coordinator] postToOutbox failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
240
|
+
}
|
|
179
241
|
return {
|
|
180
242
|
kind: 'escalated',
|
|
181
243
|
escalationReason: { phase: 're-review-after-audit', reason: `re-review verdict parse failed` },
|
|
@@ -188,13 +250,18 @@ async function runAuditChain(deps, opts, prUrl, coordinatorStartMs, severity, fi
|
|
|
188
250
|
return { kind: 'merged', prUrl };
|
|
189
251
|
}
|
|
190
252
|
deps.stderr(`[audit-chain] Post-audit verdict still ${reFindings.severity} -- escalating to Human Outbox`);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
253
|
+
try {
|
|
254
|
+
await deps.postToOutbox(`PR requires human review: still ${reFindings.severity} after production-readiness audit`, {
|
|
255
|
+
prUrl,
|
|
256
|
+
phase: 'audit-chain-complete',
|
|
257
|
+
severity: reFindings.severity,
|
|
258
|
+
findingSummaries: reFindings.findingSummaries,
|
|
259
|
+
note: 'Do NOT auto-merge. Human review required.',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
deps.stderr(`[WARN coordinator] postToOutbox failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
264
|
+
}
|
|
198
265
|
return {
|
|
199
266
|
kind: 'escalated',
|
|
200
267
|
escalationReason: {
|
|
@@ -50,6 +50,12 @@ async function runImplementCore(deps, opts, pitchPath, coordinatorStartMs) {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
const uxHandle = uxSpawnResult.value;
|
|
53
|
+
if (!uxHandle || uxHandle.trim() === '') {
|
|
54
|
+
return {
|
|
55
|
+
kind: 'escalated',
|
|
56
|
+
escalationReason: { phase: 'ux-gate', reason: 'UX design session returned empty handle' },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
53
59
|
const uxAwait = await deps.awaitSessions([uxHandle], adaptive_pipeline_js_1.REVIEW_TIMEOUT_MS);
|
|
54
60
|
const uxResult = uxAwait.results[0];
|
|
55
61
|
if (!uxResult || uxResult.outcome !== 'success') {
|
|
@@ -93,7 +99,18 @@ async function runImplementCore(deps, opts, pitchPath, coordinatorStartMs) {
|
|
|
93
99
|
deps.stderr(`[implement] Coding session completed (${Math.round((codingResult.durationMs ?? 0) / 1000)}s)`);
|
|
94
100
|
const branchPattern = `worktrain/${codingHandle.slice(0, 16)}`;
|
|
95
101
|
deps.stderr(`[implement] Polling for PR on branch pattern: ${branchPattern}`);
|
|
96
|
-
|
|
102
|
+
let prUrl;
|
|
103
|
+
try {
|
|
104
|
+
prUrl = await deps.pollForPR(branchPattern, PR_POLL_TIMEOUT_MS);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
108
|
+
deps.stderr(`[coordinator] pollForPR threw: ${msg}`);
|
|
109
|
+
return {
|
|
110
|
+
kind: 'escalated',
|
|
111
|
+
escalationReason: { phase: 'pr-detection', reason: `pollForPR threw: ${msg}` },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
97
114
|
if (!prUrl) {
|
|
98
115
|
return {
|
|
99
116
|
kind: 'escalated',
|
|
@@ -49,7 +49,7 @@ export interface CoordinatorDeps {
|
|
|
49
49
|
readonly writeFile: (path: string, content: string) => Promise<void>;
|
|
50
50
|
readonly stderr: (line: string) => void;
|
|
51
51
|
readonly now: () => number;
|
|
52
|
-
readonly port
|
|
52
|
+
readonly port?: number;
|
|
53
53
|
readonly readFile: (path: string) => Promise<string>;
|
|
54
54
|
readonly appendFile: (path: string, content: string) => Promise<void>;
|
|
55
55
|
readonly mkdir: (path: string, options: {
|
package/dist/manifest.json
CHANGED
|
@@ -481,8 +481,8 @@
|
|
|
481
481
|
"sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
|
|
482
482
|
"bytes": 8011
|
|
483
483
|
},
|
|
484
|
-
"console-ui/assets/index-
|
|
485
|
-
"sha256": "
|
|
484
|
+
"console-ui/assets/index-BuMfiLrV.js": {
|
|
485
|
+
"sha256": "e7d0e3f4ded8e370e8c34846ffce1404d28d0eb2613bf89b10cfc22a678ea6cf",
|
|
486
486
|
"bytes": 760528
|
|
487
487
|
},
|
|
488
488
|
"console-ui/assets/index-DGj8EsFR.css": {
|
|
@@ -490,7 +490,7 @@
|
|
|
490
490
|
"bytes": 60631
|
|
491
491
|
},
|
|
492
492
|
"console-ui/index.html": {
|
|
493
|
-
"sha256": "
|
|
493
|
+
"sha256": "314e6af46d83e7e0daa7a3efb2a998ac981f7f2ff58bae090ddcbd26b477c855",
|
|
494
494
|
"bytes": 417
|
|
495
495
|
},
|
|
496
496
|
"console/standalone-console.d.ts": {
|
|
@@ -546,24 +546,24 @@
|
|
|
546
546
|
"bytes": 462
|
|
547
547
|
},
|
|
548
548
|
"coordinators/modes/full-pipeline.js": {
|
|
549
|
-
"sha256": "
|
|
550
|
-
"bytes":
|
|
549
|
+
"sha256": "a03cf485201d23b0ddf75ca36ea10741bb9d0373479e7df3350401653229ef8b",
|
|
550
|
+
"bytes": 12850
|
|
551
551
|
},
|
|
552
552
|
"coordinators/modes/implement-shared.d.ts": {
|
|
553
553
|
"sha256": "fbad9d91d84d2112b273175618686489a7f106385e0e62d6cab80804d6d0f2d7",
|
|
554
554
|
"bytes": 708
|
|
555
555
|
},
|
|
556
556
|
"coordinators/modes/implement-shared.js": {
|
|
557
|
-
"sha256": "
|
|
558
|
-
"bytes":
|
|
557
|
+
"sha256": "117eec98c38826e62150e4ca1ece6ac1cad91b2d0d7128dc34acb609151e4619",
|
|
558
|
+
"bytes": 13672
|
|
559
559
|
},
|
|
560
560
|
"coordinators/modes/implement.d.ts": {
|
|
561
561
|
"sha256": "23919c24d62a0bf15296a52fbc594cca8b1b34e6f8d98dcf7dede8d97ad4cabb",
|
|
562
562
|
"bytes": 347
|
|
563
563
|
},
|
|
564
564
|
"coordinators/modes/implement.js": {
|
|
565
|
-
"sha256": "
|
|
566
|
-
"bytes":
|
|
565
|
+
"sha256": "e05ad7330c00db7b2ad8baf27e224616eae1b5cce460a0394a80633fca8827bf",
|
|
566
|
+
"bytes": 5522
|
|
567
567
|
},
|
|
568
568
|
"coordinators/modes/quick-review.d.ts": {
|
|
569
569
|
"sha256": "03a4f29a07047b0bf788d84f8e0ebab63d64c8eb98aa57087943a8fb84563998",
|
|
@@ -582,8 +582,8 @@
|
|
|
582
582
|
"bytes": 1198
|
|
583
583
|
},
|
|
584
584
|
"coordinators/pr-review.d.ts": {
|
|
585
|
-
"sha256": "
|
|
586
|
-
"bytes":
|
|
585
|
+
"sha256": "a8886a3c83a31e869522812d1342a301e9bfae92d8e5e694594c3c50912035d9",
|
|
586
|
+
"bytes": 3833
|
|
587
587
|
},
|
|
588
588
|
"coordinators/pr-review.js": {
|
|
589
589
|
"sha256": "84b51f931eb55d908de8c60f90b4d4b66540054791a28ce2f07426a841fed386",
|
|
@@ -1730,8 +1730,8 @@
|
|
|
1730
1730
|
"bytes": 1740
|
|
1731
1731
|
},
|
|
1732
1732
|
"trigger/trigger-listener.js": {
|
|
1733
|
-
"sha256": "
|
|
1734
|
-
"bytes":
|
|
1733
|
+
"sha256": "09b8bbcda1825a9314dc29ac7435ef703fb0cdad13fa54ffe45f68767f22fbc7",
|
|
1734
|
+
"bytes": 25095
|
|
1735
1735
|
},
|
|
1736
1736
|
"trigger/trigger-router.d.ts": {
|
|
1737
1737
|
"sha256": "b916f33cab64d491ab04bd13dd37599d33e687f7aea1e69e50f5fcea4b3b4624",
|
|
@@ -3074,8 +3074,8 @@
|
|
|
3074
3074
|
"bytes": 880
|
|
3075
3075
|
},
|
|
3076
3076
|
"v2/usecases/console-routes.js": {
|
|
3077
|
-
"sha256": "
|
|
3078
|
-
"bytes":
|
|
3077
|
+
"sha256": "76e34345d329bfc8a998ebc50ef8bd6ead1b7914873f6350ba479909c9c097be",
|
|
3078
|
+
"bytes": 31648
|
|
3079
3079
|
},
|
|
3080
3080
|
"v2/usecases/console-service.d.ts": {
|
|
3081
3081
|
"sha256": "fc8fe65427fa9f4f3535344b385b36f66ca06b7e3bfaea708931817a3edcad2b",
|
|
@@ -3138,8 +3138,8 @@
|
|
|
3138
3138
|
"bytes": 660
|
|
3139
3139
|
},
|
|
3140
3140
|
"v2/usecases/worktree-service.js": {
|
|
3141
|
-
"sha256": "
|
|
3142
|
-
"bytes":
|
|
3141
|
+
"sha256": "a489b696720653761c6d969ce160e2c4f5e92573de962bfefc84b1ce7900270b",
|
|
3142
|
+
"bytes": 12592
|
|
3143
3143
|
}
|
|
3144
3144
|
}
|
|
3145
3145
|
}
|
|
@@ -201,8 +201,21 @@ async function startTriggerListener(ctx, options) {
|
|
|
201
201
|
? new notification_service_js_1.NotificationService({ macOs: notifyMacOs, webhookUrl: notifyWebhook })
|
|
202
202
|
: undefined;
|
|
203
203
|
const steerRegistry = new Map();
|
|
204
|
-
const DAEMON_CONSOLE_PORT = 3456;
|
|
205
204
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
205
|
+
const { ConsoleService } = await Promise.resolve().then(() => __importStar(require('../v2/usecases/console-service.js')));
|
|
206
|
+
let consoleService = null;
|
|
207
|
+
if (!ctx.v2?.dataDir || !ctx.v2?.directoryListing) {
|
|
208
|
+
process.stderr.write('[CRITICAL trigger-listener:reason=consoleService_unavailable] ctx.v2.dataDir or ctx.v2.directoryListing not available -- awaitSessions and getAgentResult will degrade to all-failed / empty results\n');
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
consoleService = new ConsoleService({
|
|
212
|
+
directoryListing: ctx.v2.directoryListing,
|
|
213
|
+
dataDir: ctx.v2.dataDir,
|
|
214
|
+
sessionStore: ctx.v2.sessionStore,
|
|
215
|
+
snapshotStore: ctx.v2.snapshotStore,
|
|
216
|
+
pinnedWorkflowStore: ctx.v2.pinnedStore,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
206
219
|
let routerRef;
|
|
207
220
|
const coordinatorDeps = {
|
|
208
221
|
spawnSession: async (workflowId, goal, workspace, context) => {
|
|
@@ -256,98 +269,97 @@ async function startTriggerListener(ctx, options) {
|
|
|
256
269
|
nowIso: () => new Date().toISOString(),
|
|
257
270
|
}),
|
|
258
271
|
awaitSessions: async (handles, timeoutMs) => {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
272
|
+
const POLL_INTERVAL_MS = 3000;
|
|
273
|
+
if (consoleService === null) {
|
|
274
|
+
process.stderr.write(`[WARN coord:reason=await_degraded] awaitSessions: ConsoleService unavailable -- returning all ${handles.length} session(s) as failed.\n`);
|
|
275
|
+
return {
|
|
276
|
+
results: [...handles].map((h) => ({
|
|
277
|
+
handle: h,
|
|
278
|
+
outcome: 'failed',
|
|
279
|
+
status: null,
|
|
280
|
+
durationMs: 0,
|
|
281
|
+
})),
|
|
282
|
+
allSucceeded: false,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const startMs = Date.now();
|
|
286
|
+
const pending = new Set(handles);
|
|
287
|
+
const results = new Map();
|
|
288
|
+
while (pending.size > 0) {
|
|
289
|
+
const elapsed = Date.now() - startMs;
|
|
290
|
+
if (elapsed >= timeoutMs) {
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
for (const handle of [...pending]) {
|
|
265
294
|
try {
|
|
266
|
-
|
|
295
|
+
const detail = await consoleService.getSessionDetail(handle);
|
|
296
|
+
if (detail.isErr()) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const run = detail.value.runs[0];
|
|
300
|
+
if (!run)
|
|
301
|
+
continue;
|
|
302
|
+
const status = run.status;
|
|
303
|
+
if (status === 'complete' || status === 'complete_with_gaps') {
|
|
304
|
+
results.set(handle, { handle, outcome: 'success', status, durationMs: Date.now() - startMs });
|
|
305
|
+
pending.delete(handle);
|
|
306
|
+
}
|
|
307
|
+
else if (status === 'blocked') {
|
|
308
|
+
results.set(handle, { handle, outcome: 'failed', status, durationMs: Date.now() - startMs });
|
|
309
|
+
pending.delete(handle);
|
|
310
|
+
}
|
|
267
311
|
}
|
|
268
|
-
catch {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
sessions: [...handles].join(','),
|
|
277
|
-
mode: 'all',
|
|
278
|
-
timeout: `${Math.round(timeoutMs / 1000)}s`,
|
|
279
|
-
port: DAEMON_CONSOLE_PORT,
|
|
280
|
-
});
|
|
281
|
-
if (resolvedResult === null) {
|
|
282
|
-
process.stderr.write(`[WARN coord:reason=await_failed] awaitSessions: could not get session results -- daemon may be unreachable or timed out. Returning all ${handles.length} session(s) as failed.\n`);
|
|
312
|
+
catch {
|
|
313
|
+
results.set(handle, { handle, outcome: 'failed', status: null, durationMs: Date.now() - startMs });
|
|
314
|
+
pending.delete(handle);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (pending.size > 0) {
|
|
318
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
319
|
+
}
|
|
283
320
|
}
|
|
284
|
-
|
|
285
|
-
results
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
allSucceeded: false,
|
|
321
|
+
for (const handle of pending) {
|
|
322
|
+
results.set(handle, { handle, outcome: 'timeout', status: null, durationMs: timeoutMs });
|
|
323
|
+
}
|
|
324
|
+
const resultsArray = [...results.values()];
|
|
325
|
+
return {
|
|
326
|
+
results: resultsArray,
|
|
327
|
+
allSucceeded: resultsArray.every((r) => r.outcome === 'success'),
|
|
292
328
|
};
|
|
293
329
|
},
|
|
294
330
|
getAgentResult: async (sessionHandle) => {
|
|
295
331
|
const emptyResult = { recapMarkdown: null, artifacts: [] };
|
|
332
|
+
if (consoleService === null) {
|
|
333
|
+
return emptyResult;
|
|
334
|
+
}
|
|
296
335
|
try {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
if (!sessionRes.ok) {
|
|
300
|
-
process.stderr.write(`[WARN coord:reason=http_error status=${sessionRes.status} handle=${sessionHandle.slice(0, 16)}] getAgentResult: session fetch returned HTTP ${sessionRes.status}\n`);
|
|
336
|
+
const detailResult = await consoleService.getSessionDetail(sessionHandle);
|
|
337
|
+
if (detailResult.isErr())
|
|
301
338
|
return emptyResult;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (sessionBody['success'] !== true) {
|
|
305
|
-
return emptyResult;
|
|
306
|
-
}
|
|
307
|
-
const data = sessionBody['data'];
|
|
308
|
-
if (!data)
|
|
339
|
+
const run = detailResult.value.runs[0];
|
|
340
|
+
if (!run)
|
|
309
341
|
return emptyResult;
|
|
310
|
-
const
|
|
311
|
-
if (!Array.isArray(runs) || runs.length === 0)
|
|
312
|
-
return emptyResult;
|
|
313
|
-
const firstRun = runs[0];
|
|
314
|
-
const tipNodeId = typeof firstRun['preferredTipNodeId'] === 'string'
|
|
315
|
-
? firstRun['preferredTipNodeId']
|
|
316
|
-
: null;
|
|
342
|
+
const tipNodeId = run.preferredTipNodeId;
|
|
317
343
|
if (!tipNodeId)
|
|
318
344
|
return emptyResult;
|
|
319
|
-
const
|
|
320
|
-
? firstRun['nodes']
|
|
321
|
-
: [];
|
|
322
|
-
const allNodeIds = allNodes
|
|
323
|
-
.map((n) => (typeof n['nodeId'] === 'string' ? n['nodeId'] : null))
|
|
324
|
-
.filter((id) => id !== null);
|
|
345
|
+
const allNodeIds = run.nodes.map((n) => n.nodeId).filter((id) => typeof id === 'string' && id !== '');
|
|
325
346
|
const nodeIdsToFetch = allNodeIds.length > 0 ? allNodeIds : [tipNodeId];
|
|
326
|
-
const baseNodeUrl = `http://127.0.0.1:${DAEMON_CONSOLE_PORT}/api/v2/sessions/${encodeURIComponent(sessionHandle)}/nodes/`;
|
|
327
347
|
let recap = null;
|
|
328
348
|
const collectedArtifacts = [];
|
|
329
349
|
for (const nodeId of nodeIdsToFetch) {
|
|
330
350
|
try {
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
continue;
|
|
334
|
-
const nodeBody = await nodeRes.json();
|
|
335
|
-
if (nodeBody['success'] !== true)
|
|
336
|
-
continue;
|
|
337
|
-
const nodeData = nodeBody['data'];
|
|
338
|
-
if (!nodeData)
|
|
351
|
+
const nodeResult = await consoleService.getNodeDetail(sessionHandle, nodeId);
|
|
352
|
+
if (nodeResult.isErr())
|
|
339
353
|
continue;
|
|
340
354
|
if (nodeId === tipNodeId) {
|
|
341
|
-
recap =
|
|
355
|
+
recap = nodeResult.value.recapMarkdown;
|
|
342
356
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
collectedArtifacts.push(...nodeArtifacts);
|
|
357
|
+
if (nodeResult.value.artifacts.length > 0) {
|
|
358
|
+
collectedArtifacts.push(...nodeResult.value.artifacts);
|
|
346
359
|
}
|
|
347
360
|
}
|
|
348
|
-
catch
|
|
349
|
-
|
|
350
|
-
process.stderr.write(`[WARN coord:reason=node_exception handle=${sessionHandle.slice(0, 16)} node=${nodeId.slice(0, 16)}] getAgentResult: ${msg}\n`);
|
|
361
|
+
catch {
|
|
362
|
+
continue;
|
|
351
363
|
}
|
|
352
364
|
}
|
|
353
365
|
return { recapMarkdown: recap, artifacts: collectedArtifacts };
|
|
@@ -396,7 +408,6 @@ async function startTriggerListener(ctx, options) {
|
|
|
396
408
|
generateId: () => (0, node_crypto_1.randomUUID)(),
|
|
397
409
|
stderr: (line) => process.stderr.write(line + '\n'),
|
|
398
410
|
now: () => Date.now(),
|
|
399
|
-
port: DAEMON_CONSOLE_PORT,
|
|
400
411
|
fileExists: (p) => fs.existsSync(p),
|
|
401
412
|
archiveFile: (src, dest) => fs.promises.rename(src, dest),
|
|
402
413
|
pollForPR: async (branchPattern, timeoutMs) => {
|
|
@@ -430,7 +430,10 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
430
430
|
}
|
|
431
431
|
const repoRoots = cachedRepoRoots;
|
|
432
432
|
const data = await Promise.race([
|
|
433
|
-
(0, worktree_service_js_1.getWorktreeList)(repoRoots, activeSessions)
|
|
433
|
+
(0, worktree_service_js_1.getWorktreeList)(repoRoots, activeSessions).finally(() => {
|
|
434
|
+
if (timeoutId !== null)
|
|
435
|
+
clearTimeout(timeoutId);
|
|
436
|
+
}),
|
|
434
437
|
timeoutPromise,
|
|
435
438
|
]);
|
|
436
439
|
if (timeoutId !== null)
|
|
@@ -275,9 +275,13 @@ async function scanRepos(repoRoots) {
|
|
|
275
275
|
}
|
|
276
276
|
async function runBackgroundEnrichment(repoRoots, repoRootsKey) {
|
|
277
277
|
try {
|
|
278
|
+
let bgTimeoutId;
|
|
279
|
+
const bgTimeoutPromise = new Promise((_, reject) => {
|
|
280
|
+
bgTimeoutId = setTimeout(() => reject(new Error('background enrichment timeout')), BACKGROUND_ENRICHMENT_TIMEOUT_MS);
|
|
281
|
+
});
|
|
278
282
|
const enriched = await Promise.race([
|
|
279
|
-
scanRepos(repoRoots),
|
|
280
|
-
|
|
283
|
+
scanRepos(repoRoots).finally(() => clearTimeout(bgTimeoutId)),
|
|
284
|
+
bgTimeoutPromise,
|
|
281
285
|
]);
|
|
282
286
|
if (worktreeCache?.repoRootsKey === repoRootsKey) {
|
|
283
287
|
worktreeCache = { ...worktreeCache, enrichedRepos: enriched };
|