@exaudeus/workrail 3.52.0 → 3.54.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/cli/commands/worktrain-overview.d.ts +1 -0
- package/dist/cli/commands/worktrain-overview.js +64 -0
- package/dist/cli-worktrain.js +20 -0
- package/dist/console-ui/assets/{index-Ce7Feod7.js → index-CoXPahi0.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/daemon-env.d.ts +5 -0
- package/dist/daemon/daemon-env.js +36 -0
- package/dist/daemon/daemon-events.d.ts +11 -1
- package/dist/manifest.json +29 -21
- package/dist/trigger/github-queue-config.js +1 -1
- package/dist/trigger/polling-scheduler.js +6 -7
- package/dist/trigger/trigger-router.js +10 -0
- package/dist/trigger/trigger-store.js +69 -2
- package/dist/trigger/types.d.ts +4 -0
- package/docs/design/dispatch-condition-and-adaptive-queue.md +97 -0
- package/docs/design/dispatch-condition-implementation-plan.md +168 -0
- package/docs/design/dispatch-condition-review-findings.md +61 -0
- package/docs/ideas/backlog.md +163 -0
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ export interface WorktrainOverviewCommandDeps {
|
|
|
21
21
|
readonly joinPath: (...parts: string[]) => string;
|
|
22
22
|
readonly print: (line: string) => void;
|
|
23
23
|
readonly getDataDirEnv: () => string | undefined;
|
|
24
|
+
readonly readEventLog: (filePath: string) => Promise<string>;
|
|
24
25
|
}
|
|
25
26
|
export interface WorktrainOverviewCommandOpts {
|
|
26
27
|
readonly json?: boolean;
|
|
@@ -45,6 +45,65 @@ function buildSessionTitle(s) {
|
|
|
45
45
|
function extractStepLabel(_s) {
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
|
+
function parseDaemonStatusLine(nowMs, eventLogContent) {
|
|
49
|
+
let latestHeartbeat = null;
|
|
50
|
+
let latestStopped = null;
|
|
51
|
+
for (const raw of eventLogContent.split('\n')) {
|
|
52
|
+
if (!raw.trim())
|
|
53
|
+
continue;
|
|
54
|
+
let obj;
|
|
55
|
+
try {
|
|
56
|
+
obj = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const kind = typeof obj['kind'] === 'string' ? obj['kind'] : '';
|
|
62
|
+
const ts = typeof obj['ts'] === 'number' ? obj['ts'] : null;
|
|
63
|
+
if (ts === null)
|
|
64
|
+
continue;
|
|
65
|
+
if (kind === 'daemon_heartbeat') {
|
|
66
|
+
if (latestHeartbeat === null || ts > latestHeartbeat.ts) {
|
|
67
|
+
const activeSessions = typeof obj['activeSessions'] === 'number' ? obj['activeSessions'] : 0;
|
|
68
|
+
latestHeartbeat = { ts, activeSessions };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (kind === 'daemon_stopped') {
|
|
72
|
+
if (latestStopped === null || ts > latestStopped.ts) {
|
|
73
|
+
const reason = typeof obj['reason'] === 'string' ? obj['reason'] : 'unknown';
|
|
74
|
+
latestStopped = { ts, reason };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (latestHeartbeat === null && latestStopped === null) {
|
|
79
|
+
return 'Daemon: no events today';
|
|
80
|
+
}
|
|
81
|
+
const heartbeatTs = latestHeartbeat?.ts ?? -Infinity;
|
|
82
|
+
const stoppedTs = latestStopped?.ts ?? -Infinity;
|
|
83
|
+
if (stoppedTs >= heartbeatTs && latestStopped !== null) {
|
|
84
|
+
const stoppedTime = new Date(latestStopped.ts).toLocaleTimeString('en-US', {
|
|
85
|
+
hour: '2-digit',
|
|
86
|
+
minute: '2-digit',
|
|
87
|
+
second: '2-digit',
|
|
88
|
+
hour12: false,
|
|
89
|
+
});
|
|
90
|
+
if (latestStopped.reason === 'graceful') {
|
|
91
|
+
return `Daemon: stopped gracefully (${stoppedTime})`;
|
|
92
|
+
}
|
|
93
|
+
return `Daemon: crashed (${stoppedTime})`;
|
|
94
|
+
}
|
|
95
|
+
if (latestHeartbeat !== null) {
|
|
96
|
+
const agoMs = nowMs - latestHeartbeat.ts;
|
|
97
|
+
if (agoMs < 90000) {
|
|
98
|
+
const agoSec = Math.round(agoMs / 1000);
|
|
99
|
+
const sessions = latestHeartbeat.activeSessions;
|
|
100
|
+
const sessionStr = sessions === 1 ? '1 active session' : `${sessions} active sessions`;
|
|
101
|
+
return `Daemon: running (last heartbeat ${agoSec}s ago, ${sessionStr})`;
|
|
102
|
+
}
|
|
103
|
+
return `Daemon: may have crashed (last heartbeat ${formatRelativeTime(agoMs)})`;
|
|
104
|
+
}
|
|
105
|
+
return 'Daemon: no events today';
|
|
106
|
+
}
|
|
48
107
|
function isCompleted(s) {
|
|
49
108
|
return s.status === 'complete' || s.status === 'complete_with_gaps';
|
|
50
109
|
}
|
|
@@ -100,6 +159,10 @@ async function executeWorktrainOverviewCommand(deps, opts = {}) {
|
|
|
100
159
|
deps.print(JSON.stringify(packet, null, 2));
|
|
101
160
|
return;
|
|
102
161
|
}
|
|
162
|
+
const todayDate = new Date(nowMs).toISOString().slice(0, 10);
|
|
163
|
+
const eventLogPath = deps.joinPath(deps.homedir(), '.workrail', 'events', 'daemon', `${todayDate}.jsonl`);
|
|
164
|
+
const eventLogContent = await deps.readEventLog(eventLogPath);
|
|
165
|
+
const daemonStatusLine = parseDaemonStatusLine(nowMs, eventLogContent);
|
|
103
166
|
const date = new Date(nowMs);
|
|
104
167
|
const dateStr = date.toLocaleDateString('en-US', {
|
|
105
168
|
weekday: 'short',
|
|
@@ -113,6 +176,7 @@ async function executeWorktrainOverviewCommand(deps, opts = {}) {
|
|
|
113
176
|
hour12: false,
|
|
114
177
|
});
|
|
115
178
|
deps.print(`WorkTrain [${dateStr} ${timeStr}]`);
|
|
179
|
+
deps.print(daemonStatusLine);
|
|
116
180
|
deps.print('Note: live session detection requires daemon (showing last-known state).');
|
|
117
181
|
deps.print('');
|
|
118
182
|
if (activeSessions.length === 0 && recentSessions.length === 0) {
|
package/dist/cli-worktrain.js
CHANGED
|
@@ -47,6 +47,7 @@ const child_process_1 = require("child_process");
|
|
|
47
47
|
const util_1 = require("util");
|
|
48
48
|
const crypto_1 = require("crypto");
|
|
49
49
|
const interpret_result_js_1 = require("./cli/interpret-result.js");
|
|
50
|
+
const daemon_env_js_1 = require("./daemon/daemon-env.js");
|
|
50
51
|
const index_js_1 = require("./context-assembly/index.js");
|
|
51
52
|
const infra_js_1 = require("./context-assembly/infra.js");
|
|
52
53
|
const index_js_2 = require("./cli/commands/index.js");
|
|
@@ -243,6 +244,7 @@ program
|
|
|
243
244
|
.option('--uninstall', 'Stop the daemon service and remove the launchd plist')
|
|
244
245
|
.option('--status', 'Show the current status of the daemon service')
|
|
245
246
|
.action(async (options) => {
|
|
247
|
+
await (0, daemon_env_js_1.loadDaemonEnv)();
|
|
246
248
|
const { execFile: execFileRaw } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
247
249
|
const execFilePromise = (0, util_1.promisify)(execFileRaw);
|
|
248
250
|
const result = await (0, index_js_2.executeWorktrainDaemonCommand)({
|
|
@@ -283,6 +285,7 @@ program
|
|
|
283
285
|
print: (line) => console.log(line),
|
|
284
286
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
285
287
|
startDaemon: async () => {
|
|
288
|
+
await (0, daemon_env_js_1.loadDaemonEnv)();
|
|
286
289
|
const { startTriggerListener } = await Promise.resolve().then(() => __importStar(require('./trigger/trigger-listener.js')));
|
|
287
290
|
const { startDaemonConsole } = await Promise.resolve().then(() => __importStar(require('./trigger/daemon-console.js')));
|
|
288
291
|
const { DaemonEventEmitter } = await Promise.resolve().then(() => __importStar(require('./daemon/daemon-events.js')));
|
|
@@ -346,8 +349,24 @@ program
|
|
|
346
349
|
console.warn(`[DaemonConsole] Could not start console: ${consoleResult.error.message}`);
|
|
347
350
|
}
|
|
348
351
|
await new Promise((resolve) => {
|
|
352
|
+
const heartbeatInterval = setInterval(() => {
|
|
353
|
+
const sessionsDir = path_1.default.join(os_1.default.homedir(), '.workrail', 'daemon-sessions');
|
|
354
|
+
fs_1.default.promises.readdir(sessionsDir)
|
|
355
|
+
.then((files) => files.filter((f) => f.endsWith('.json')).length)
|
|
356
|
+
.catch(() => 0)
|
|
357
|
+
.then((activeSessions) => {
|
|
358
|
+
emitter.emit({ kind: 'daemon_heartbeat', activeSessions, ts: Date.now() });
|
|
359
|
+
});
|
|
360
|
+
}, 30000);
|
|
361
|
+
process.on('uncaughtException', (err) => {
|
|
362
|
+
console.error('[WorkTrain] Uncaught exception -- daemon shutting down:', err);
|
|
363
|
+
emitter.emit({ kind: 'daemon_stopped', reason: 'crash', ts: Date.now() });
|
|
364
|
+
process.exit(1);
|
|
365
|
+
});
|
|
349
366
|
const shutdown = async () => {
|
|
350
367
|
console.log('\nShutting down daemon...');
|
|
368
|
+
clearInterval(heartbeatInterval);
|
|
369
|
+
emitter.emit({ kind: 'daemon_stopped', reason: 'graceful', ts: Date.now() });
|
|
351
370
|
if (consoleHandle) {
|
|
352
371
|
await consoleHandle.stop();
|
|
353
372
|
}
|
|
@@ -581,6 +600,7 @@ program
|
|
|
581
600
|
joinPath: path_1.default.join,
|
|
582
601
|
print: (line) => process.stdout.write(line + '\n'),
|
|
583
602
|
getDataDirEnv: () => process.env['WORKRAIL_DATA_DIR'],
|
|
603
|
+
readEventLog: (p) => fs_1.default.promises.readFile(p, 'utf-8').catch(() => ''),
|
|
584
604
|
}, {
|
|
585
605
|
json: options.json,
|
|
586
606
|
workspace: options.workspace,
|