@exaudeus/workrail 3.59.5 → 3.59.7
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/cli-worktrain.js +2 -26
- package/dist/console-ui/assets/{index-Ctoxo1z6.js → index-RXJXvJ8T.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/modes/full-pipeline.js +4 -4
- package/dist/coordinators/pr-review.d.ts +4 -1
- package/dist/daemon/workflow-runner.js +1 -1
- package/dist/manifest.json +23 -31
- package/dist/trigger/adapters/github-queue-poller.js +25 -1
- package/dist/trigger/polling-scheduler.d.ts +1 -0
- package/dist/trigger/polling-scheduler.js +49 -6
- package/dist/trigger/trigger-listener.js +2 -1
- package/dist/v2/usecases/console-routes.js +6 -8
- package/docs/ideas/backlog.md +226 -0
- package/package.json +1 -1
- package/dist/trigger/daemon-console.d.ts +0 -28
- package/dist/trigger/daemon-console.js +0 -120
|
@@ -142,7 +142,7 @@ async function readAllDaemonSessions(sessionsDir = exports.DAEMON_SESSIONS_DIR)
|
|
|
142
142
|
}
|
|
143
143
|
const sessions = [];
|
|
144
144
|
for (const entry of entries) {
|
|
145
|
-
if (!entry.endsWith('.json'))
|
|
145
|
+
if (!entry.endsWith('.json') || entry.startsWith('queue-issue-'))
|
|
146
146
|
continue;
|
|
147
147
|
const sessionId = entry.slice(0, -5);
|
|
148
148
|
const filePath = path.join(sessionsDir, entry);
|
package/dist/manifest.json
CHANGED
|
@@ -238,8 +238,8 @@
|
|
|
238
238
|
"bytes": 31
|
|
239
239
|
},
|
|
240
240
|
"cli-worktrain.js": {
|
|
241
|
-
"sha256": "
|
|
242
|
-
"bytes":
|
|
241
|
+
"sha256": "cc970333db8fec7bcccec57e901bcde86f1671d1efa039d7abc9119593870bef",
|
|
242
|
+
"bytes": 59518
|
|
243
243
|
},
|
|
244
244
|
"cli.d.ts": {
|
|
245
245
|
"sha256": "43e818adf60173644896298637f47b01d5819b17eda46eaa32d0c7d64724d012",
|
|
@@ -481,16 +481,16 @@
|
|
|
481
481
|
"sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
|
|
482
482
|
"bytes": 8011
|
|
483
483
|
},
|
|
484
|
-
"console-ui/assets/index-Ctoxo1z6.js": {
|
|
485
|
-
"sha256": "fc11e753539cbb514c0c3c37189f2fbe2abca848fa005c5bcae09e5c88b78bd9",
|
|
486
|
-
"bytes": 760528
|
|
487
|
-
},
|
|
488
484
|
"console-ui/assets/index-DGj8EsFR.css": {
|
|
489
485
|
"sha256": "3bdb55ec0957928e0ebbb86a7d6b36d28f7ba7d5c0f3e236fd8f2e2aacee2fa4",
|
|
490
486
|
"bytes": 60631
|
|
491
487
|
},
|
|
488
|
+
"console-ui/assets/index-RXJXvJ8T.js": {
|
|
489
|
+
"sha256": "2d32ed2f503a4eaf9d9c4868d8bd7906df0ab8635c6d24c1f8eb333a50ac442d",
|
|
490
|
+
"bytes": 760528
|
|
491
|
+
},
|
|
492
492
|
"console-ui/index.html": {
|
|
493
|
-
"sha256": "
|
|
493
|
+
"sha256": "546a59bc2b573a0d127fd10eb6ef2b39ed98d4bb38ea432610d831d5dd288bfb",
|
|
494
494
|
"bytes": 417
|
|
495
495
|
},
|
|
496
496
|
"console/standalone-console.d.ts": {
|
|
@@ -546,8 +546,8 @@
|
|
|
546
546
|
"bytes": 462
|
|
547
547
|
},
|
|
548
548
|
"coordinators/modes/full-pipeline.js": {
|
|
549
|
-
"sha256": "
|
|
550
|
-
"bytes":
|
|
549
|
+
"sha256": "945d726d728235f8f31f03f33fbde8f6614472b38921a44fba42da959875f37d",
|
|
550
|
+
"bytes": 13201
|
|
551
551
|
},
|
|
552
552
|
"coordinators/modes/implement-shared.d.ts": {
|
|
553
553
|
"sha256": "fbad9d91d84d2112b273175618686489a7f106385e0e62d6cab80804d6d0f2d7",
|
|
@@ -582,8 +582,8 @@
|
|
|
582
582
|
"bytes": 1198
|
|
583
583
|
},
|
|
584
584
|
"coordinators/pr-review.d.ts": {
|
|
585
|
-
"sha256": "
|
|
586
|
-
"bytes":
|
|
585
|
+
"sha256": "d46e4923995a0b43aefee25da298b86235fae0ad105e548b3174c0eea9c1f8d0",
|
|
586
|
+
"bytes": 3947
|
|
587
587
|
},
|
|
588
588
|
"coordinators/pr-review.js": {
|
|
589
589
|
"sha256": "84b51f931eb55d908de8c60f90b4d4b66540054791a28ce2f07426a841fed386",
|
|
@@ -650,8 +650,8 @@
|
|
|
650
650
|
"bytes": 7307
|
|
651
651
|
},
|
|
652
652
|
"daemon/workflow-runner.js": {
|
|
653
|
-
"sha256": "
|
|
654
|
-
"bytes":
|
|
653
|
+
"sha256": "0d4991c3589e75679d4035d506d84ebe595df8328c6e632d352597c9e23ad741",
|
|
654
|
+
"bytes": 95348
|
|
655
655
|
},
|
|
656
656
|
"di/container.d.ts": {
|
|
657
657
|
"sha256": "003bb7fb7478d627524b9b1e76bd0a963a243794a687ff233b96dc0e33a06d9f",
|
|
@@ -1650,8 +1650,8 @@
|
|
|
1650
1650
|
"bytes": 1363
|
|
1651
1651
|
},
|
|
1652
1652
|
"trigger/adapters/github-queue-poller.js": {
|
|
1653
|
-
"sha256": "
|
|
1654
|
-
"bytes":
|
|
1653
|
+
"sha256": "b15f56cf0782a1eceb66ef6d58bb75b17e14f701f1e95d072fe7a71b5aa6a4f5",
|
|
1654
|
+
"bytes": 8824
|
|
1655
1655
|
},
|
|
1656
1656
|
"trigger/adapters/gitlab-poller.d.ts": {
|
|
1657
1657
|
"sha256": "f685490fafad77194fdd0f0bbaf80dbc56730aeb344853da365199a120fbe399",
|
|
@@ -1661,14 +1661,6 @@
|
|
|
1661
1661
|
"sha256": "6728a2169f4007b9ea0414fade6b21500500d9c79d0b09296d92ef8bcabb9c79",
|
|
1662
1662
|
"bytes": 2763
|
|
1663
1663
|
},
|
|
1664
|
-
"trigger/daemon-console.d.ts": {
|
|
1665
|
-
"sha256": "a3b9a9f58482c6ea379c0e02c30f55a5820c7c37fa3fae55fc336cd518f35462",
|
|
1666
|
-
"bytes": 1162
|
|
1667
|
-
},
|
|
1668
|
-
"trigger/daemon-console.js": {
|
|
1669
|
-
"sha256": "f2f09c05e48b42ebf1c7be137fc6eced46673048471b7114434710b5691fe6f2",
|
|
1670
|
-
"bytes": 5497
|
|
1671
|
-
},
|
|
1672
1664
|
"trigger/delivery-action.d.ts": {
|
|
1673
1665
|
"sha256": "559e2b2645aa60528f73de351cd35ebf45c5b82f47797aa15ddd681319315d39",
|
|
1674
1666
|
"bytes": 1759
|
|
@@ -1718,20 +1710,20 @@
|
|
|
1718
1710
|
"bytes": 6968
|
|
1719
1711
|
},
|
|
1720
1712
|
"trigger/polling-scheduler.d.ts": {
|
|
1721
|
-
"sha256": "
|
|
1722
|
-
"bytes":
|
|
1713
|
+
"sha256": "3c0865f9d21819c364575062745741405bc80006f4a0754d26ed4302253371c6",
|
|
1714
|
+
"bytes": 1126
|
|
1723
1715
|
},
|
|
1724
1716
|
"trigger/polling-scheduler.js": {
|
|
1725
|
-
"sha256": "
|
|
1726
|
-
"bytes":
|
|
1717
|
+
"sha256": "61b94e35aae2e9578a9e9cc32548791166b9ec98abb8f2cff58135fc6b3e5593",
|
|
1718
|
+
"bytes": 23945
|
|
1727
1719
|
},
|
|
1728
1720
|
"trigger/trigger-listener.d.ts": {
|
|
1729
1721
|
"sha256": "1eebb3d4829030b264c3798b0b0d55d7357d313ab83e3f344ad455eaafcedb44",
|
|
1730
1722
|
"bytes": 1740
|
|
1731
1723
|
},
|
|
1732
1724
|
"trigger/trigger-listener.js": {
|
|
1733
|
-
"sha256": "
|
|
1734
|
-
"bytes":
|
|
1725
|
+
"sha256": "4aa62601aac5d3c7d1750ef839ee71a911dacbab346fb6dfdb3d7151e9e7d359",
|
|
1726
|
+
"bytes": 25179
|
|
1735
1727
|
},
|
|
1736
1728
|
"trigger/trigger-router.d.ts": {
|
|
1737
1729
|
"sha256": "b916f33cab64d491ab04bd13dd37599d33e687f7aea1e69e50f5fcea4b3b4624",
|
|
@@ -3074,8 +3066,8 @@
|
|
|
3074
3066
|
"bytes": 880
|
|
3075
3067
|
},
|
|
3076
3068
|
"v2/usecases/console-routes.js": {
|
|
3077
|
-
"sha256": "
|
|
3078
|
-
"bytes":
|
|
3069
|
+
"sha256": "6e22d5ef4fce3d9bf2b2709d3fcfc35089b8c558d82372071ac7d22251c68ddb",
|
|
3070
|
+
"bytes": 31748
|
|
3079
3071
|
},
|
|
3080
3072
|
"v2/usecases/console-service.d.ts": {
|
|
3081
3073
|
"sha256": "fc8fe65427fa9f4f3535344b385b36f66ca06b7e3bfaea708931817a3edcad2b",
|
|
@@ -148,6 +148,30 @@ function inferMaturity(body) {
|
|
|
148
148
|
return 'idea';
|
|
149
149
|
}
|
|
150
150
|
async function checkIdempotency(issueNumber, sessionsDir = exports.DEFAULT_SESSIONS_DIR) {
|
|
151
|
+
const sidecarFilename = `queue-issue-${issueNumber}.json`;
|
|
152
|
+
const sidecarFilePath = path.join(sessionsDir, sidecarFilename);
|
|
153
|
+
try {
|
|
154
|
+
const sidecarContent = await fs.readFile(sidecarFilePath, 'utf8');
|
|
155
|
+
const sidecarParsed = JSON.parse(sidecarContent);
|
|
156
|
+
if (typeof sidecarParsed !== 'object' || sidecarParsed === null) {
|
|
157
|
+
return 'active';
|
|
158
|
+
}
|
|
159
|
+
const sidecar = sidecarParsed;
|
|
160
|
+
const dispatchedAt = sidecar['dispatchedAt'];
|
|
161
|
+
const ttlMs = sidecar['ttlMs'];
|
|
162
|
+
if (typeof dispatchedAt === 'number' && typeof ttlMs === 'number') {
|
|
163
|
+
if (dispatchedAt + ttlMs > Date.now()) {
|
|
164
|
+
return 'active';
|
|
165
|
+
}
|
|
166
|
+
return 'clear';
|
|
167
|
+
}
|
|
168
|
+
return 'active';
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
if (e.code !== 'ENOENT') {
|
|
172
|
+
return 'active';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
151
175
|
let files;
|
|
152
176
|
try {
|
|
153
177
|
files = await fs.readdir(sessionsDir);
|
|
@@ -155,7 +179,7 @@ async function checkIdempotency(issueNumber, sessionsDir = exports.DEFAULT_SESSI
|
|
|
155
179
|
catch {
|
|
156
180
|
return 'clear';
|
|
157
181
|
}
|
|
158
|
-
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
182
|
+
const jsonFiles = files.filter(f => f.endsWith('.json') && f !== sidecarFilename);
|
|
159
183
|
for (const filename of jsonFiles) {
|
|
160
184
|
try {
|
|
161
185
|
const content = await fs.readFile(path.join(sessionsDir, filename), 'utf8');
|
|
@@ -38,6 +38,7 @@ const gitlab_poller_js_1 = require("./adapters/gitlab-poller.js");
|
|
|
38
38
|
const github_poller_js_1 = require("./adapters/github-poller.js");
|
|
39
39
|
const github_queue_poller_js_1 = require("./adapters/github-queue-poller.js");
|
|
40
40
|
const github_queue_config_js_1 = require("./github-queue-config.js");
|
|
41
|
+
const adaptive_pipeline_js_1 = require("../coordinators/adaptive-pipeline.js");
|
|
41
42
|
const fs = __importStar(require("node:fs/promises"));
|
|
42
43
|
const os = __importStar(require("node:os"));
|
|
43
44
|
const path = __importStar(require("node:path"));
|
|
@@ -312,15 +313,31 @@ class PollingScheduler {
|
|
|
312
313
|
}
|
|
313
314
|
this.dispatchingIssues.add(top.issue.number);
|
|
314
315
|
console.log(`[QueuePoll] in-flight-add #${top.issue.number}`);
|
|
316
|
+
const sidecarPath = path.join(sessionsDir, `queue-issue-${top.issue.number}.json`);
|
|
317
|
+
const sidecarContent = JSON.stringify({
|
|
318
|
+
issueNumber: top.issue.number,
|
|
319
|
+
triggerId,
|
|
320
|
+
dispatchedAt: Date.now(),
|
|
321
|
+
ttlMs: adaptive_pipeline_js_1.DISCOVERY_TIMEOUT_MS + 60000,
|
|
322
|
+
}, null, 2);
|
|
323
|
+
void fs.writeFile(sidecarPath, sidecarContent, 'utf8').catch((e) => {
|
|
324
|
+
console.warn(`[QueuePoll] Failed to write sidecar for issue #${top.issue.number}: ${e instanceof Error ? e.message : String(e)}`);
|
|
325
|
+
});
|
|
315
326
|
const dispatchP = this.router.dispatchAdaptivePipeline(workflowTrigger.goal, workflowTrigger.workspacePath, workflowTrigger.context);
|
|
327
|
+
const issueNumber = top.issue.number;
|
|
316
328
|
void dispatchP
|
|
317
|
-
.then(() => {
|
|
318
|
-
this.dispatchingIssues.delete(
|
|
319
|
-
console.log(`[QueuePoll] in-flight-clear #${
|
|
329
|
+
.then((outcome) => {
|
|
330
|
+
this.dispatchingIssues.delete(issueNumber);
|
|
331
|
+
console.log(`[QueuePoll] in-flight-clear #${issueNumber} reason=completed`);
|
|
332
|
+
if (outcome.kind === 'escalated' || outcome.kind === 'dry_run') {
|
|
333
|
+
void this.applyGitHubLabel(issueNumber, 'worktrain:in-progress', queueConfig.token, source.repo);
|
|
334
|
+
}
|
|
335
|
+
void fs.unlink(sidecarPath).catch(() => { });
|
|
320
336
|
})
|
|
321
337
|
.catch(() => {
|
|
322
|
-
this.dispatchingIssues.delete(
|
|
323
|
-
console.log(`[QueuePoll] in-flight-clear #${
|
|
338
|
+
this.dispatchingIssues.delete(issueNumber);
|
|
339
|
+
console.log(`[QueuePoll] in-flight-clear #${issueNumber} reason=error`);
|
|
340
|
+
void fs.unlink(sidecarPath).catch(() => { });
|
|
324
341
|
});
|
|
325
342
|
console.log(`[QueuePoll] dispatched via adaptivePipeline goal="${workflowTrigger.goal.slice(0, 80)}"`);
|
|
326
343
|
for (let i = 1; i < candidates.length; i++) {
|
|
@@ -332,6 +349,32 @@ class PollingScheduler {
|
|
|
332
349
|
console.log(`[QueuePoll] cycle complete selected=1 skipped=${skipped.length + candidates.length - 1} elapsed=${elapsed}ms`);
|
|
333
350
|
await appendQueuePollLog({ event: 'poll_cycle_complete', selected: 1, skipped: skipped.length + candidates.length - 1, elapsed, ts: new Date().toISOString() });
|
|
334
351
|
}
|
|
352
|
+
async applyGitHubLabel(issueNumber, label, token, repo) {
|
|
353
|
+
const fetchFn = this.fetchFn ?? globalThis.fetch;
|
|
354
|
+
const url = `https://api.github.com/repos/${repo}/issues/${issueNumber}/labels`;
|
|
355
|
+
try {
|
|
356
|
+
const response = await fetchFn(url, {
|
|
357
|
+
method: 'POST',
|
|
358
|
+
headers: {
|
|
359
|
+
'Authorization': `Bearer ${token}`,
|
|
360
|
+
'Accept': 'application/vnd.github+json',
|
|
361
|
+
'Content-Type': 'application/json',
|
|
362
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
363
|
+
},
|
|
364
|
+
body: JSON.stringify({ labels: [label] }),
|
|
365
|
+
});
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
const text = await response.text().catch(() => '');
|
|
368
|
+
console.warn(`[QueuePoll] Failed to apply label '${label}' to issue #${issueNumber}: HTTP ${response.status} ${text.slice(0, 200)}`);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.log(`[QueuePoll] Applied label '${label}' to issue #${issueNumber}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
console.warn(`[QueuePoll] Failed to apply label '${label}' to issue #${issueNumber}: ${e instanceof Error ? e.message : String(e)}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
335
378
|
}
|
|
336
379
|
exports.PollingScheduler = PollingScheduler;
|
|
337
380
|
function buildGitLabWorkflowTrigger(trigger, mr) {
|
|
@@ -432,7 +475,7 @@ function extractDotPath(obj, rawPath) {
|
|
|
432
475
|
async function countActiveSessions(sessionsDir) {
|
|
433
476
|
try {
|
|
434
477
|
const files = await fs.readdir(sessionsDir);
|
|
435
|
-
return files.filter((f) => f.endsWith('.json')).length;
|
|
478
|
+
return files.filter((f) => f.endsWith('.json') && !f.startsWith('queue-issue-')).length;
|
|
436
479
|
}
|
|
437
480
|
catch {
|
|
438
481
|
return 0;
|
|
@@ -218,7 +218,7 @@ async function startTriggerListener(ctx, options) {
|
|
|
218
218
|
}
|
|
219
219
|
let routerRef;
|
|
220
220
|
const coordinatorDeps = {
|
|
221
|
-
spawnSession: async (workflowId, goal, workspace, context) => {
|
|
221
|
+
spawnSession: async (workflowId, goal, workspace, context, agentConfig) => {
|
|
222
222
|
if (routerRef === undefined) {
|
|
223
223
|
return { kind: 'err', error: 'in-process router not initialized -- coordinator deps not ready' };
|
|
224
224
|
}
|
|
@@ -242,6 +242,7 @@ async function startTriggerListener(ctx, options) {
|
|
|
242
242
|
goal,
|
|
243
243
|
workspacePath: workspace,
|
|
244
244
|
context,
|
|
245
|
+
...(agentConfig !== undefined ? { agentConfig } : {}),
|
|
245
246
|
_preAllocatedStartResponse: startResult.value.response,
|
|
246
247
|
});
|
|
247
248
|
return { kind: 'ok', value: sessionHandle };
|
|
@@ -429,13 +429,11 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
429
429
|
repoRootsExpiresAt = Date.now() + REPO_ROOTS_TTL_MS;
|
|
430
430
|
}
|
|
431
431
|
const repoRoots = cachedRepoRoots;
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
timeoutPromise,
|
|
438
|
-
]);
|
|
432
|
+
const worktreeWork = (0, worktree_service_js_1.getWorktreeList)(repoRoots, activeSessions)
|
|
433
|
+
.finally(() => { if (timeoutId !== null)
|
|
434
|
+
clearTimeout(timeoutId); })
|
|
435
|
+
.catch(() => ({ repos: [] }));
|
|
436
|
+
const data = await Promise.race([worktreeWork, timeoutPromise]);
|
|
439
437
|
if (timeoutId !== null)
|
|
440
438
|
clearTimeout(timeoutId);
|
|
441
439
|
res.json({ success: true, data });
|
|
@@ -532,7 +530,7 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
532
530
|
}
|
|
533
531
|
app.post('/api/v2/auto/dispatch', express_1.default.json(), async (req, res) => {
|
|
534
532
|
if (!v2ToolContext) {
|
|
535
|
-
res.status(503).json({ success: false, error: 'Autonomous dispatch requires
|
|
533
|
+
res.status(503).json({ success: false, error: 'Autonomous dispatch requires the WorkTrain daemon. Run worktrain console alongside worktrain daemon to enable browser dispatch.' });
|
|
536
534
|
return;
|
|
537
535
|
}
|
|
538
536
|
const body = req.body;
|
package/docs/ideas/backlog.md
CHANGED
|
@@ -7351,3 +7351,229 @@ An agent can die from: stream watchdog timeout (600s no progress), OOM kill, or
|
|
|
7351
7351
|
### Priority
|
|
7352
7352
|
|
|
7353
7353
|
High. Agent crash recovery makes the overnight-autonomous bar achievable. Without it, any hung LLM call or tool timeout fails the entire pipeline silently. With it, transient failures are automatically retried and the pipeline continues.
|
|
7354
|
+
|
|
7355
|
+
---
|
|
7356
|
+
|
|
7357
|
+
## Workflow execution time tracking and prediction (Apr 21, 2026)
|
|
7358
|
+
|
|
7359
|
+
**The problem:** WorkTrain has no data on how long workflows actually take. Timeouts are set by intuition (55 min for discovery, 35 for shaping, 65 for coding). We just discovered that discovery on a real workrail task takes ~16 minutes. The 55-minute timeout is 3x the actual time -- but we didn't know that until we ran a benchmark manually.
|
|
7360
|
+
|
|
7361
|
+
### What to track
|
|
7362
|
+
|
|
7363
|
+
For every completed session, record:
|
|
7364
|
+
- Workflow ID
|
|
7365
|
+
- Total wall-clock duration
|
|
7366
|
+
- Number of turns
|
|
7367
|
+
- Number of step advances
|
|
7368
|
+
- Outcome (success / timeout / stuck / error)
|
|
7369
|
+
- Task complexity signals (codebase size, number of files read, task type from context)
|
|
7370
|
+
|
|
7371
|
+
Store in `~/.workrail/data/execution-stats.jsonl` -- one line per completed session, append-only.
|
|
7372
|
+
|
|
7373
|
+
### What to do with it
|
|
7374
|
+
|
|
7375
|
+
**Immediate use: calibrate timeouts automatically**
|
|
7376
|
+
|
|
7377
|
+
Instead of hardcoded `DISCOVERY_TIMEOUT_MS = 55 * 60 * 1000`, read the p95 completion time from execution stats and set the timeout to `p95 * 1.5`. Start with the hardcoded values as seeds; refine after 10+ samples.
|
|
7378
|
+
|
|
7379
|
+
**Medium-term use: predict duration before dispatch**
|
|
7380
|
+
|
|
7381
|
+
Given: task description + workflow ID + codebase characteristics → predicted duration range.
|
|
7382
|
+
|
|
7383
|
+
The coordinator could use this to:
|
|
7384
|
+
- Warn when a task is likely to exceed session limits before starting
|
|
7385
|
+
- Adjust timeout budgets per-dispatch based on predicted complexity
|
|
7386
|
+
- Surface "this type of task usually takes 45 minutes" in `worktrain trigger test` output
|
|
7387
|
+
|
|
7388
|
+
**Longer-term use: quality/efficiency metrics**
|
|
7389
|
+
|
|
7390
|
+
Track step-advance rate (steps per turn) as a proxy for workflow efficiency. A session with 50 turns and 2 step advances is spending too many turns between steps. This feeds into the workflow improvement loop.
|
|
7391
|
+
|
|
7392
|
+
### Implementation notes
|
|
7393
|
+
|
|
7394
|
+
- Append to `execution-stats.jsonl` in `runWorkflow()`'s finally block, same pattern as the daemon event log
|
|
7395
|
+
- Keep it simple: flat JSONL, no database, no schema migration
|
|
7396
|
+
- `worktrain status` can show recent timing stats: "Last 10 wr.discovery sessions: avg 18min, p95 31min"
|
|
7397
|
+
- `worktrain trigger validate` can warn if configured timeouts are well below historical p95
|
|
7398
|
+
|
|
7399
|
+
### Priority
|
|
7400
|
+
|
|
7401
|
+
Medium. The data collection is small (~5 lines in `runWorkflow()`). The prediction and calibration are more involved. Ship collection first, calibration second.
|
|
7402
|
+
|
|
7403
|
+
---
|
|
7404
|
+
|
|
7405
|
+
## WorkRail MCP server self-cleanup (Apr 21, 2026)
|
|
7406
|
+
|
|
7407
|
+
**The problem:** The WorkRail MCP server accumulates stale state that never cleans itself up: old workflow copies in `~/.workrail/workflows/`, dead managed sources, git repo caches that can't pull, 500+ sessions in the store, stale remembered roots. None of it has a TTL or cleanup mechanism. Every server startup loads everything and logs validation errors for stale state.
|
|
7408
|
+
|
|
7409
|
+
### Sources of stale state
|
|
7410
|
+
|
|
7411
|
+
1. **`~/.workrail/workflows/`** -- manually copied or `worktrain init`-placed workflows that go stale when the repo updates. MCP server loads both repo copy and user copy; older one fails validation silently or noisily.
|
|
7412
|
+
|
|
7413
|
+
2. **Managed sources** (`~/.workrail/data/managed-sources/`) -- paths that no longer exist stay registered. Server tries to load them on every startup.
|
|
7414
|
+
|
|
7415
|
+
3. **Git workflow cache** (`~/.workrail/cache/git-*`) -- cloned repos whose remotes have changed, been deleted, or whose auth has expired. `git pull` fails; errors logged on every startup.
|
|
7416
|
+
|
|
7417
|
+
4. **Session store** (`~/.workrail/data/sessions/`) -- sessions accumulate forever. No TTL, no archival. Console loads all 500+ on every `/api/v2/sessions` request (partially mitigated by mtime cache).
|
|
7418
|
+
|
|
7419
|
+
5. **Remembered roots** (`~/.workrail/data/managed-sources/remembered-roots.json`) -- workspace paths from past sessions that no longer exist.
|
|
7420
|
+
|
|
7421
|
+
### Fix: two layers
|
|
7422
|
+
|
|
7423
|
+
**Layer 1: Defensive loading (mostly already done)**
|
|
7424
|
+
Every loader should already handle missing/broken sources gracefully. Audit: are all managed source failures caught and logged as warnings rather than errors? Are git cache failures non-fatal?
|
|
7425
|
+
|
|
7426
|
+
**Layer 2: `workrail cleanup` command**
|
|
7427
|
+
```
|
|
7428
|
+
workrail cleanup [--yes] [--sessions --older-than <age>] [--sources] [--cache] [--roots]
|
|
7429
|
+
```
|
|
7430
|
+
- `--sources`: remove managed sources where path doesn't exist on disk
|
|
7431
|
+
- `--cache`: remove git caches where `git pull` fails (remote gone or auth expired)
|
|
7432
|
+
- `--sessions --older-than 30d`: archive or delete sessions older than N days
|
|
7433
|
+
- `--roots`: remove remembered roots where path doesn't exist
|
|
7434
|
+
- Without `--yes`: show what would be removed and ask for confirmation
|
|
7435
|
+
- With `--yes`: remove without prompting (for CI / worktrain init)
|
|
7436
|
+
|
|
7437
|
+
**Layer 3: Automatic startup cleanup (light)**
|
|
7438
|
+
On MCP server startup, silently remove managed sources where the filesystem path doesn't exist (non-destructive -- the path is already gone). Log a single "removed N stale sources" line. Do not auto-remove sessions or caches -- those require explicit user intent.
|
|
7439
|
+
|
|
7440
|
+
**Layer 4: User workflow directory sync**
|
|
7441
|
+
`~/.workrail/workflows/` should not be a place users copy workflows to manually. It should either:
|
|
7442
|
+
- Be deprecated entirely (use managed sources / workspace roots instead)
|
|
7443
|
+
- Have a `workrail sync` command that updates it from the canonical sources
|
|
7444
|
+
- Auto-detect when a user workflow is an older version of a bundled workflow and skip loading it
|
|
7445
|
+
|
|
7446
|
+
### Priority
|
|
7447
|
+
|
|
7448
|
+
Medium for the cleanup command (quality of life, stops log noise). High for startup auto-cleanup of dead managed sources (prevents the `Invalid workflow` errors that have been confusing throughout this session). Low for session TTL/archival (the mtime cache handles the performance concern).
|
|
7449
|
+
|
|
7450
|
+
---
|
|
7451
|
+
|
|
7452
|
+
## Worktree orphan leak on delivery failure (Apr 21, 2026, from Audit 4)
|
|
7453
|
+
|
|
7454
|
+
**The bug:** In `src/trigger/trigger-router.ts`, `maybeRunDelivery()` on the success path deletes the session sidecar file before attempting worktree removal. If worktree removal fails (network error, git command failure), the sidecar is already gone. `runStartupRecovery()` scans sidecar files to find orphan worktrees -- so the orphaned worktree is invisible and accumulates indefinitely.
|
|
7455
|
+
|
|
7456
|
+
**Fix:** In the success path cleanup, delete the sidecar AFTER worktree removal, not before. Or better: always attempt worktree removal in a `try/finally` that deletes the sidecar regardless of whether removal succeeded.
|
|
7457
|
+
|
|
7458
|
+
**File:** `src/trigger/trigger-router.ts`, `maybeRunDelivery()` success path.
|
|
7459
|
+
|
|
7460
|
+
**Priority:** Medium. Worktrees are small, but the leak is permanent across daemon restarts.
|
|
7461
|
+
|
|
7462
|
+
---
|
|
7463
|
+
|
|
7464
|
+
## queue-poll.jsonl never rotated (Apr 21, 2026, from Audit 5)
|
|
7465
|
+
|
|
7466
|
+
**The bug:** `~/.workrail/queue-poll.jsonl` grows indefinitely. `appendFile`-only, no rotation. At 5-minute poll intervals with 2-3 events per cycle, this is ~8-87 MB/month depending on activity. Disk exhaustion risk on long-running daemons.
|
|
7467
|
+
|
|
7468
|
+
**Fix:** Add a size check before appending in `appendQueuePollLog()`. If file exceeds 10 MB, rotate it: rename to `queue-poll.jsonl.1`, start fresh. Keep at most 2 rotated files.
|
|
7469
|
+
|
|
7470
|
+
**File:** `src/trigger/polling-scheduler.ts`, `appendQueuePollLog()` function.
|
|
7471
|
+
|
|
7472
|
+
**Priority:** Medium. Not urgent but a production correctness issue.
|
|
7473
|
+
|
|
7474
|
+
---
|
|
7475
|
+
|
|
7476
|
+
## ReviewSeverity missing assertNever + stderr bypassing injected dep (Apr 21, 2026, from Audit 2)
|
|
7477
|
+
|
|
7478
|
+
**Bug 1 (Major):** In `src/coordinators/modes/implement-shared.ts`, the `switch(findings.severity)` over `ReviewSeverity` has no `default: assertNever(findings.severity)`. Widening `ReviewSeverity` with a new variant compiles cleanly and falls through silently.
|
|
7479
|
+
|
|
7480
|
+
**Fix:** Add `default: assertNever(findings.severity)` to the switch.
|
|
7481
|
+
|
|
7482
|
+
**Bug 2 (Major):** In `src/coordinators/pr-review.ts`, `readVerdictArtifact()` calls `process.stderr.write(...)` directly instead of using the injected `deps.stderr`. Tests that inject a fake dep will miss this log.
|
|
7483
|
+
|
|
7484
|
+
**Fix:** Replace `process.stderr.write(...)` with `deps.stderr(...)`.
|
|
7485
|
+
|
|
7486
|
+
**Files:** `src/coordinators/modes/implement-shared.ts`, `src/coordinators/pr-review.ts`.
|
|
7487
|
+
|
|
7488
|
+
**Priority:** Medium. Correctness issues that won't crash in production but make future refactors unsafe.
|
|
7489
|
+
|
|
7490
|
+
---
|
|
7491
|
+
|
|
7492
|
+
## Current state update (Apr 21, 2026)
|
|
7493
|
+
|
|
7494
|
+
**npm version: v3.59.6** | Daemon PID: 54113 | Status: Running, pipeline active
|
|
7495
|
+
|
|
7496
|
+
### What shipped in this session (Apr 19-21, 2026)
|
|
7497
|
+
|
|
7498
|
+
**All five autonomous pipeline items (previously recorded) plus:**
|
|
7499
|
+
|
|
7500
|
+
- ✅ **Discovery loop fix** (#748) -- three coupled fixes: thread `maxSessionMinutes` through `spawnSession` (sessions now get 55/35/65 min instead of 30 min default), inspect `PipelineOutcome` in polling-scheduler and apply `worktrain:in-progress` label on escalation, write issue-ownership sidecar for cross-restart idempotency
|
|
7501
|
+
- ✅ **In-process `awaitSessions` and `getAgentResult`** (#741) -- replaced HTTP calls to the daemon's own console with direct `ConsoleService` access
|
|
7502
|
+
- ✅ **Try/catch on all coordinator I/O** (#740) -- `getAgentResult`, `pollForPR`, `postToOutbox` all wrapped; coordinator no longer crashes on I/O failure
|
|
7503
|
+
- ✅ **Dispatch dedup prealloc bypass** (#744) -- `dispatch()` now bypasses dedup for pre-allocated sessions, fixing the zombie session bug that prevented discovery from starting
|
|
7504
|
+
- ✅ **Promise.race crash fix** (#733) -- worktrees scan timeout no longer crashes the daemon via unhandled rejection
|
|
7505
|
+
- ✅ **Trigger validator** (#690) -- `worktrain trigger validate` command, `validateTriggerStrict()` pure function
|
|
7506
|
+
- ✅ **`worktrain trigger poll`** (#697) -- force immediate poll cycle on any queue trigger
|
|
7507
|
+
- ✅ **`worktrain trigger test`** (#656) -- dry-run showing what would dispatch
|
|
7508
|
+
- ✅ **Auto-load ~/.workrail/.env** (#673) -- daemon reads secrets from .env automatically
|
|
7509
|
+
- ✅ **Daemon lifecycle events** (#674) -- `session_aborted` on SIGTERM, `daemon_heartbeat` every 30s
|
|
7510
|
+
- ✅ **Attribution signals** (#658) -- `[WT]` PR title prefix, `Co-authored-by: WorkTrain` commit trailers, `worktrain:generated` label
|
|
7511
|
+
- ✅ **Secret scan before push** (#660) -- pattern-based scan blocks commits with leaked credentials
|
|
7512
|
+
- ✅ **Unified logs stream** (#680) -- `worktrain logs` now merges daemon events, queue-poll.jsonl, and filtered stderr
|
|
7513
|
+
- ✅ **Stale lock file handling** (#705) -- validates lock file PID before trusting port discovery
|
|
7514
|
+
- ✅ **5 architectural audits** (docs/design/) -- coordinator access, error handling, testability, type bloat, memory management
|
|
7515
|
+
- ✅ **Stale user workflow cleanup** -- removed old copies from `~/.workrail/workflows/` that were causing ValidationError noise
|
|
7516
|
+
|
|
7517
|
+
### Current pipeline state (live)
|
|
7518
|
+
|
|
7519
|
+
Discovery session `ecf359d7` running: 77 turns, 11 step advances (active, making real progress on issue #393). Session `b7df0c8b` also running (just started). First clean run after all pipeline fixes landed.
|
|
7520
|
+
|
|
7521
|
+
### Accurate limitations (v3.59.6)
|
|
7522
|
+
|
|
7523
|
+
1. **Ghost sessions in event log** -- sessions killed by daemon crashes don't get `session_aborted` events from old daemon instances. New daemons emit it on shutdown, but historical sessions show as RUNNING.
|
|
7524
|
+
2. **Worktree orphan leak** -- if `maybeRunDelivery()` worktree removal fails after sidecar deletion, orphan is invisible to `runStartupRecovery`. See backlog.
|
|
7525
|
+
3. **`queue-poll.jsonl` never rotated** -- disk exhaustion risk on long-running daemons. See backlog.
|
|
7526
|
+
4. **`ReviewSeverity` missing `assertNever`** -- future variants silently fall through. See backlog.
|
|
7527
|
+
5. **`process.stderr.write` in `readVerdictArtifact`** -- bypasses injected dep, invisible to test fakes. See backlog.
|
|
7528
|
+
6. **WorkRail MCP stale state** -- `workrail cleanup` command doesn't exist yet. Manual cleanup needed for dead managed sources, old session accumulation.
|
|
7529
|
+
7. **Trigger validation static/runtime gap** -- some runtime checks not in static validator. See trigger-validation-gap-audit.md.
|
|
7530
|
+
8. **WorkflowTrigger type bloat** -- mixes trigger config, session runtime state, delivery config. See workflow-trigger-lifecycle-audit.md.
|
|
7531
|
+
9. **Conversation history not persisted** -- LLM conversation history is in-memory only. On crash, context is lost. See backlog.
|
|
7532
|
+
|
|
7533
|
+
### Next priorities (groomed Apr 21)
|
|
7534
|
+
|
|
7535
|
+
1. **Watch the current pipeline run** -- discovery `ecf359d7` is active at 77 turns/11 steps. If it completes, shaping and coding should fire automatically. First end-to-end validation.
|
|
7536
|
+
2. **Execution time tracking** -- add session timing to `execution-stats.jsonl` for timeout calibration. Small change in `runWorkflow()` finally block.
|
|
7537
|
+
3. **Three audit findings from above** -- worktree orphan leak, queue-poll rotation, assertNever fixes. All small, targeted.
|
|
7538
|
+
4. **`workrail cleanup` command** -- removes dead managed sources, rotates old session files, clears stale git caches. Stops ValidationError noise in MCP server logs.
|
|
7539
|
+
5. **Conversation history persistence** -- `conversation.jsonl` per session, append-only. Prerequisite for true crash recovery.
|
|
7540
|
+
6. **Autonomous crash recovery and interrupted-session resume** -- see full entry below (Apr 21).
|
|
7541
|
+
|
|
7542
|
+
---
|
|
7543
|
+
|
|
7544
|
+
## Autonomous crash recovery and interrupted-session resume (Apr 21, 2026)
|
|
7545
|
+
|
|
7546
|
+
**The problem we hit today:** A daemon crash loop (console `worktrees scan` unhandled rejection) killed all in-flight sessions. The queue correctly detected the sidecar and skipped re-dispatch for 56 min (TTL), but when the sidecar expired the session was re-dispatched from scratch with zero context from the previous attempt. The agent had already spent ~10 min in Phase 0, read codebase files, and formed a plan -- all of that work was lost.
|
|
7547
|
+
|
|
7548
|
+
**What we want:** WorkTrain should be able to detect orphaned sessions on startup and make an autonomous decision: resume if the session had meaningful progress, discard and re-dispatch from scratch if it was too early to be worth resuming.
|
|
7549
|
+
|
|
7550
|
+
**Resumability decision criteria (heuristics):**
|
|
7551
|
+
- Session had >= 1 `continue_workflow` call (at least one step advance): worth resuming -- the agent made real progress.
|
|
7552
|
+
- Session is at step 0 with 0 advances but > 5 LLM turns: borderline -- context was accumulated but no checkpoint written. Resume is risky (stale context), discard is safer. Could surface to console for human decision.
|
|
7553
|
+
- Session is at step 0, < 5 turns, < 2 min: discard -- nothing was lost.
|
|
7554
|
+
- Session's worktree is missing or corrupted: discard -- can't resume cleanly.
|
|
7555
|
+
- Session is on a coding workflow and has uncommitted changes in the worktree: pause for human review before discarding (could have partial work).
|
|
7556
|
+
|
|
7557
|
+
**Implementation sketch:**
|
|
7558
|
+
|
|
7559
|
+
1. **On daemon startup**, `runStartupRecovery()` already scans `daemon-sessions/` for orphaned token files. Extend it to also inspect the session event log for each orphan:
|
|
7560
|
+
- Count `continue_workflow` calls and LLM turns from `~/.workrail/events/<sessionId>.jsonl`
|
|
7561
|
+
- Apply decision criteria above
|
|
7562
|
+
- For resume candidates: call `continue_workflow` with the checkpoint token and a synthesized re-entry prompt: "You are resuming a session that was interrupted by a daemon crash. Your last known step was [stepLabel]. Continue from where you left off."
|
|
7563
|
+
- For discard candidates: emit `session_aborted` event, delete the sidecar, re-add the issue to the queue (or just let the TTL expire and the queue re-select naturally)
|
|
7564
|
+
|
|
7565
|
+
2. **Conversation history prerequisite**: Resume is only useful if the agent can reconstruct its context. Today, conversation history is in-memory only -- it is lost on crash. The `conversation.jsonl` per-session persistence (backlog item #5 above) is a prerequisite for high-quality resume. Without it, resume starts from the workflow system prompt plus the current step recap only. This is enough for mid-pipeline phases (shaping, coding) since they read artifacts from disk. It may be insufficient for early discovery phases.
|
|
7566
|
+
|
|
7567
|
+
3. **`worktrain session resume <sessionId>` CLI** -- manual override for human-initiated resume. Useful when the daemon's automatic heuristic chose to discard but the user sees partial work worth keeping.
|
|
7568
|
+
|
|
7569
|
+
4. **Queue sidecar TTL for resume vs. discard**: Today the sidecar TTL prevents re-dispatch during the entire pipeline window (56 min). With autonomous resume, the TTL for a discarded session should be much shorter (5 min) so the queue can quickly re-select. For a resumed session, keep the full TTL and extend it by the time already spent.
|
|
7570
|
+
|
|
7571
|
+
**Files to change:**
|
|
7572
|
+
- `src/daemon/workflow-runner.ts` -- `runStartupRecovery()`: add event log inspection and conditional resume
|
|
7573
|
+
- `src/trigger/polling-scheduler.ts` -- `doPollGitHubQueue()`: accept a `ttlOverride` param so discard path uses short TTL
|
|
7574
|
+
- `src/trigger/adapters/github-queue-poller.ts` -- `checkIdempotency()`: handle expired sidecars with `ttlOverride`
|
|
7575
|
+
- New: `src/daemon/session-recovery-policy.ts` -- pure function `evaluateRecovery(orphan, eventLog) -> 'resume' | 'discard' | 'human_review'`
|
|
7576
|
+
|
|
7577
|
+
**Priority:** High. Every daemon crash currently wastes all in-flight work and waits up to 56 min before retrying. With even basic resume (step > 0 → resume, step = 0 → discard + fast re-dispatch), we'd recover most of the lost work and reduce retry latency from 56 min to < 5 min.
|
|
7578
|
+
|
|
7579
|
+
**Depends on:** Conversation history persistence (for high-quality resume context).
|
package/package.json
CHANGED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import 'reflect-metadata';
|
|
2
|
-
import type { V2ToolContext } from '../mcp/types.js';
|
|
3
|
-
import type { TriggerRouter } from './trigger-router.js';
|
|
4
|
-
import type { PollingScheduler } from './polling-scheduler.js';
|
|
5
|
-
import type { WorkflowService } from '../application/services/workflow-service.js';
|
|
6
|
-
import type { SteerRegistry } from '../daemon/workflow-runner.js';
|
|
7
|
-
import type { Result } from '../runtime/result.js';
|
|
8
|
-
export interface DaemonConsoleHandle {
|
|
9
|
-
readonly port: number;
|
|
10
|
-
stop(): Promise<void>;
|
|
11
|
-
}
|
|
12
|
-
export type DaemonConsoleError = {
|
|
13
|
-
readonly kind: 'port_conflict';
|
|
14
|
-
readonly port: number;
|
|
15
|
-
} | {
|
|
16
|
-
readonly kind: 'io_error';
|
|
17
|
-
readonly message: string;
|
|
18
|
-
};
|
|
19
|
-
export interface StartDaemonConsoleOptions {
|
|
20
|
-
readonly port?: number;
|
|
21
|
-
readonly triggerRouter?: TriggerRouter;
|
|
22
|
-
readonly serverVersion?: string;
|
|
23
|
-
readonly workflowService?: WorkflowService;
|
|
24
|
-
readonly lockFilePath?: string;
|
|
25
|
-
readonly steerRegistry?: SteerRegistry;
|
|
26
|
-
readonly pollingScheduler?: PollingScheduler;
|
|
27
|
-
}
|
|
28
|
-
export declare function startDaemonConsole(ctx: V2ToolContext, options?: StartDaemonConsoleOptions): Promise<Result<DaemonConsoleHandle, DaemonConsoleError>>;
|