granclaw 0.0.1-beta.88 → 0.0.1-beta.89
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/backend/agent/channel-broadcast.js +25 -0
- package/dist/backend/agent/process.js +3 -8
- package/dist/backend/agent/runner-pi.js +62 -3
- package/dist/backend/browser/session-manager.js +2 -2
- package/dist/backend/index.js +12 -0
- package/dist/backend/orchestrator/server.js +64 -13
- package/dist/backend/workflows-db.js +37 -0
- package/dist/frontend/assets/index-CA7f1mVG.js +158 -0
- package/dist/frontend/assets/index-DI4uAKWn.css +1 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/templates/skills/workflows/SKILL.md +57 -0
- package/dist/frontend/assets/index-C_L5wqFQ.css +0 -1
- package/dist/frontend/assets/index-CrjF9oii.js +0 -157
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WS_OPEN = void 0;
|
|
4
|
+
exports.broadcastToClients = broadcastToClients;
|
|
5
|
+
exports.getOrCreateChannelSet = getOrCreateChannelSet;
|
|
6
|
+
exports.WS_OPEN = 1;
|
|
7
|
+
function broadcastToClients(clients, data) {
|
|
8
|
+
const json = JSON.stringify(data);
|
|
9
|
+
let sent = 0;
|
|
10
|
+
for (const ws of clients) {
|
|
11
|
+
if (ws.readyState === exports.WS_OPEN) {
|
|
12
|
+
ws.send(json);
|
|
13
|
+
sent++;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return sent;
|
|
17
|
+
}
|
|
18
|
+
function getOrCreateChannelSet(map, channelId) {
|
|
19
|
+
let set = map.get(channelId);
|
|
20
|
+
if (!set) {
|
|
21
|
+
set = new Set();
|
|
22
|
+
map.set(channelId, set);
|
|
23
|
+
}
|
|
24
|
+
return set;
|
|
25
|
+
}
|
|
@@ -10,6 +10,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
10
10
|
const config_js_1 = require("../config.js");
|
|
11
11
|
const agent_db_js_1 = require("../agent-db.js");
|
|
12
12
|
const runner_pi_js_1 = require("./runner-pi.js");
|
|
13
|
+
const channel_broadcast_js_1 = require("./channel-broadcast.js");
|
|
13
14
|
const messages_db_js_1 = require("../messages-db.js");
|
|
14
15
|
const telegram_adapter_js_1 = require("./telegram-adapter.js");
|
|
15
16
|
const browser_sessions_js_1 = require("../browser-sessions.js");
|
|
@@ -36,9 +37,7 @@ function main() {
|
|
|
36
37
|
const wss = new ws_1.WebSocketServer({ port });
|
|
37
38
|
const channelClients = new Map();
|
|
38
39
|
function getChannelClients(channelId) {
|
|
39
|
-
|
|
40
|
-
channelClients.set(channelId, new Set());
|
|
41
|
-
return channelClients.get(channelId);
|
|
40
|
+
return (0, channel_broadcast_js_1.getOrCreateChannelSet)(channelClients, channelId);
|
|
42
41
|
}
|
|
43
42
|
wss.on('connection', (ws) => {
|
|
44
43
|
let clientChannelId = 'ui';
|
|
@@ -84,14 +83,10 @@ function main() {
|
|
|
84
83
|
telegramAdapter = new telegram_adapter_js_1.TelegramAdapter(agentId, telegramBotToken, workspaceDir);
|
|
85
84
|
}
|
|
86
85
|
function broadcastToChannel(channelId, data) {
|
|
87
|
-
const json = JSON.stringify(data);
|
|
88
86
|
const targets = channelClients.get(channelId);
|
|
89
87
|
if (!targets)
|
|
90
88
|
return;
|
|
91
|
-
|
|
92
|
-
if (ws.readyState === ws_1.WebSocket.OPEN)
|
|
93
|
-
ws.send(json);
|
|
94
|
-
}
|
|
89
|
+
(0, channel_broadcast_js_1.broadcastToClients)(targets, data);
|
|
95
90
|
}
|
|
96
91
|
const busyChannels = new Set();
|
|
97
92
|
function channelType(channelId) {
|
|
@@ -9,6 +9,7 @@ exports.bootstrapWorkspace = bootstrapWorkspace;
|
|
|
9
9
|
exports.translateSessionEvent = translateSessionEvent;
|
|
10
10
|
exports.stopAgent = stopAgent;
|
|
11
11
|
exports.compactAgentSession = compactAgentSession;
|
|
12
|
+
exports.classifyBrowserError = classifyBrowserError;
|
|
12
13
|
exports.runAgent = runAgent;
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
14
15
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -267,6 +268,31 @@ function extractAgentName(workspaceDir) {
|
|
|
267
268
|
}
|
|
268
269
|
return null;
|
|
269
270
|
}
|
|
271
|
+
function classifyBrowserError(err, signal) {
|
|
272
|
+
const e = err;
|
|
273
|
+
if (signal?.aborted)
|
|
274
|
+
return 'ABORTED';
|
|
275
|
+
if (e?.name === 'AbortError' || e?.code === 'ABORT_ERR')
|
|
276
|
+
return 'ABORTED';
|
|
277
|
+
if (e?.killed === true && (e?.signal === 'SIGTERM' || e?.code === null))
|
|
278
|
+
return 'TIMEOUT';
|
|
279
|
+
if (e?.code === 'ETIMEDOUT')
|
|
280
|
+
return 'TIMEOUT';
|
|
281
|
+
const raw = `${e?.stderr ?? ''}\n${e?.stdout ?? ''}\n${e?.message ?? ''}`.toLowerCase();
|
|
282
|
+
if (raw.includes('econnrefused') ||
|
|
283
|
+
raw.includes('target closed') ||
|
|
284
|
+
raw.includes('session closed') ||
|
|
285
|
+
raw.includes('protocol error') ||
|
|
286
|
+
raw.includes('browser has disconnected') ||
|
|
287
|
+
raw.includes('cdp connection closed') ||
|
|
288
|
+
raw.includes('daemon not running') ||
|
|
289
|
+
raw.includes('no session')) {
|
|
290
|
+
return 'BROWSER_DIED';
|
|
291
|
+
}
|
|
292
|
+
if (raw.includes('timeout') || raw.includes('timed out'))
|
|
293
|
+
return 'TIMEOUT';
|
|
294
|
+
return 'CMD_ERROR';
|
|
295
|
+
}
|
|
270
296
|
function getLanIp() {
|
|
271
297
|
const ifaces = (0, os_1.networkInterfaces)();
|
|
272
298
|
for (const list of Object.values(ifaces)) {
|
|
@@ -306,6 +332,13 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
306
332
|
catch { }
|
|
307
333
|
let envKey;
|
|
308
334
|
let prevValue;
|
|
335
|
+
let toolHeartbeat = null;
|
|
336
|
+
const stopHeartbeat = () => {
|
|
337
|
+
if (toolHeartbeat) {
|
|
338
|
+
clearInterval(toolHeartbeat);
|
|
339
|
+
toolHeartbeat = null;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
309
342
|
try {
|
|
310
343
|
const { getModel } = await (0, esm_import_js_1.esmImport)('@mariozechner/pi-ai');
|
|
311
344
|
const { createAgentSession, SessionManager, DefaultResourceLoader, getAgentDir } = await (0, esm_import_js_1.esmImport)('@mariozechner/pi-coding-agent');
|
|
@@ -698,6 +731,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
698
731
|
'You do not need to screenshot for audit — the whole session is recorded as video automatically.',
|
|
699
732
|
'Saved logins persist automatically when the user has set up a profile via the dashboard Browser view.',
|
|
700
733
|
'CAPTCHA & Cloudflare handling: the browser ships a stealth extension that handles Cloudflare JS interstitials ("Just a moment…") automatically (waits up to ~45s). If an actual CAPTCHA widget is detected (reCAPTCHA, hCaptcha, Turnstile, etc.), the runtime returns immediately with a warning — call request_human_browser_takeover right away.',
|
|
734
|
+
'Error recovery: if a result contains "CATEGORY=BROWSER_DIED" the runtime has ALREADY respawned the browser — just retry the exact same command; do NOT tell the user the browser crashed. "CATEGORY=TIMEOUT" means the page is hung — try a different URL or approach. "CATEGORY=ABORTED" means the user cancelled — stop; do not retry. "CATEGORY=CMD_ERROR" is a genuine command problem — inspect the message.',
|
|
701
735
|
'Examples: {"command":"open","args":["https://example.com"]}, {"command":"click","args":["--ref","e12"]}, {"command":"fill","args":["--ref","e5","Alice"]}',
|
|
702
736
|
],
|
|
703
737
|
parameters: {
|
|
@@ -715,7 +749,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
715
749
|
},
|
|
716
750
|
required: ['command'],
|
|
717
751
|
},
|
|
718
|
-
async execute(_toolCallId, params) {
|
|
752
|
+
async execute(_toolCallId, params, signal) {
|
|
719
753
|
const command = (params.command ?? '').trim();
|
|
720
754
|
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
721
755
|
if (!command) {
|
|
@@ -746,6 +780,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
746
780
|
timeout: 60_000,
|
|
747
781
|
maxBuffer: 10 * 1024 * 1024,
|
|
748
782
|
env: { ...process.env, ...browser.env },
|
|
783
|
+
signal,
|
|
749
784
|
});
|
|
750
785
|
(0, session_manager_js_1.appendCommand)(browserState.handle, `${command} ${args.join(' ')}`.trim());
|
|
751
786
|
const out = stdout.trim() || stderr.trim() || 'ok';
|
|
@@ -753,9 +788,20 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
753
788
|
}
|
|
754
789
|
catch (err) {
|
|
755
790
|
(0, session_manager_js_1.appendCommand)(browserState.handle, `${command} ${args.join(' ')}`.trim());
|
|
791
|
+
const category = classifyBrowserError(err, signal);
|
|
792
|
+
if (category === 'BROWSER_DIED') {
|
|
793
|
+
browserState.handle = null;
|
|
794
|
+
return { content: [{ type: 'text', text: `browser session expired and has been restarted (CATEGORY=BROWSER_DIED) — retry the ${command} you were attempting; the runtime already handled the reset.` }] };
|
|
795
|
+
}
|
|
796
|
+
if (category === 'ABORTED') {
|
|
797
|
+
return { content: [{ type: 'text', text: `browser ${command} was cancelled (CATEGORY=ABORTED)` }] };
|
|
798
|
+
}
|
|
756
799
|
const e = err;
|
|
757
|
-
const
|
|
758
|
-
|
|
800
|
+
const raw = (e.stderr || e.stdout || e.message || String(err)).trim();
|
|
801
|
+
if (category === 'TIMEOUT') {
|
|
802
|
+
return { content: [{ type: 'text', text: `browser ${command} timed out after 60s (CATEGORY=TIMEOUT). Page is likely hung — try a different URL or fall back to fetch_website.` }] };
|
|
803
|
+
}
|
|
804
|
+
return { content: [{ type: 'text', text: `browser ${command} failed (CATEGORY=CMD_ERROR): ${raw}` }] };
|
|
759
805
|
}
|
|
760
806
|
},
|
|
761
807
|
});
|
|
@@ -1027,6 +1073,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
1027
1073
|
console.error(`[runner-pi:${agent.id}] failed to configure auto-compaction:`, err);
|
|
1028
1074
|
}
|
|
1029
1075
|
let errorEmitted = false;
|
|
1076
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
1030
1077
|
session.subscribe((event) => {
|
|
1031
1078
|
try {
|
|
1032
1079
|
switch (event.type) {
|
|
@@ -1042,9 +1089,20 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
1042
1089
|
case 'tool_execution_start': {
|
|
1043
1090
|
onChunk({ type: 'tool_call', tool: event.toolName, input: event.args });
|
|
1044
1091
|
(0, logs_db_js_1.logAction)(agent.id, 'tool_call', { tool: event.toolName, input: event.args });
|
|
1092
|
+
stopHeartbeat();
|
|
1093
|
+
toolHeartbeat = setInterval(() => {
|
|
1094
|
+
try {
|
|
1095
|
+
onChunk({ type: 'heartbeat' });
|
|
1096
|
+
}
|
|
1097
|
+
catch { }
|
|
1098
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
1099
|
+
if (typeof toolHeartbeat?.unref === 'function') {
|
|
1100
|
+
toolHeartbeat.unref();
|
|
1101
|
+
}
|
|
1045
1102
|
break;
|
|
1046
1103
|
}
|
|
1047
1104
|
case 'tool_execution_end': {
|
|
1105
|
+
stopHeartbeat();
|
|
1048
1106
|
const output = event.result;
|
|
1049
1107
|
const outputStr = typeof output === 'string' ? output : JSON.stringify(output);
|
|
1050
1108
|
onChunk({ type: 'tool_result', tool: event.toolName, output });
|
|
@@ -1167,6 +1225,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
1167
1225
|
catch { }
|
|
1168
1226
|
}
|
|
1169
1227
|
finally {
|
|
1228
|
+
stopHeartbeat();
|
|
1170
1229
|
activeSessions.delete(agent.id);
|
|
1171
1230
|
if (browserState.handle) {
|
|
1172
1231
|
try {
|
|
@@ -119,7 +119,7 @@ async function startRecording(handle, res) {
|
|
|
119
119
|
const started = await tryStart();
|
|
120
120
|
if (!started)
|
|
121
121
|
return false;
|
|
122
|
-
const FILE_APPEAR_TIMEOUT_MS =
|
|
122
|
+
const FILE_APPEAR_TIMEOUT_MS = 15_000;
|
|
123
123
|
const FILE_APPEAR_POLL_MS = 100;
|
|
124
124
|
const deadline = Date.now() + FILE_APPEAR_TIMEOUT_MS;
|
|
125
125
|
let fileOk = false;
|
|
@@ -135,7 +135,7 @@ async function startRecording(handle, res) {
|
|
|
135
135
|
await new Promise((r) => setTimeout(r, FILE_APPEAR_POLL_MS));
|
|
136
136
|
}
|
|
137
137
|
if (!fileOk) {
|
|
138
|
-
console.warn(`[browser/session-manager] record start reported success but ${recordingPath} did not materialize within ${FILE_APPEAR_TIMEOUT_MS}ms —
|
|
138
|
+
console.warn(`[browser/session-manager] record start reported success but ${recordingPath} did not materialize within ${FILE_APPEAR_TIMEOUT_MS}ms — session will proceed without WebM recording`);
|
|
139
139
|
try {
|
|
140
140
|
await execFileAsync(bin, stopArgv, { cwd: handle.workspaceDir, timeout: 5000 });
|
|
141
141
|
}
|
package/dist/backend/index.js
CHANGED
|
@@ -5,12 +5,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
require("dotenv/config");
|
|
7
7
|
const net_1 = __importDefault(require("net"));
|
|
8
|
+
const node_events_1 = require("node:events");
|
|
8
9
|
const server_js_1 = require("./orchestrator/server.js");
|
|
10
|
+
(0, node_events_1.setMaxListeners)(50);
|
|
9
11
|
const agent_manager_js_1 = require("./orchestrator/agent-manager.js");
|
|
10
12
|
const scheduler_js_1 = require("./scheduler.js");
|
|
11
13
|
const telemetry_js_1 = require("./telemetry.js");
|
|
12
14
|
const config_js_1 = require("./config.js");
|
|
13
15
|
const browser_sessions_js_1 = require("./browser-sessions.js");
|
|
16
|
+
const workflows_db_js_1 = require("./workflows-db.js");
|
|
14
17
|
const PORT = Number(process.env.PORT ?? 3001);
|
|
15
18
|
function checkPortAvailable(port, label) {
|
|
16
19
|
return new Promise((resolve, reject) => {
|
|
@@ -50,6 +53,15 @@ preflight()
|
|
|
50
53
|
catch (err) {
|
|
51
54
|
console.error(`[orchestrator] finalizeAllActiveSessions failed for ${agent.id}:`, err);
|
|
52
55
|
}
|
|
56
|
+
try {
|
|
57
|
+
const flipped = (0, workflows_db_js_1.finalizeRunningRuns)(agent.id);
|
|
58
|
+
if (flipped > 0) {
|
|
59
|
+
console.log(`[orchestrator] finalized ${flipped} leftover running workflow run(s) for ${agent.id}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(`[orchestrator] finalizeRunningRuns failed for ${agent.id}:`, err);
|
|
64
|
+
}
|
|
53
65
|
}
|
|
54
66
|
(0, agent_manager_js_1.startAllAgents)();
|
|
55
67
|
(0, scheduler_js_1.startScheduler)();
|
|
@@ -624,6 +624,26 @@ function createServer() {
|
|
|
624
624
|
(0, agent_manager_js_1.restartAgent)(req.params.id);
|
|
625
625
|
res.json({ ok: true });
|
|
626
626
|
});
|
|
627
|
+
function resolveSkillsDir(workspaceDir) {
|
|
628
|
+
for (const candidate of ['.pi/skills', '.agent/skills', '.claude/skills']) {
|
|
629
|
+
const p = path_1.default.join(workspaceDir, candidate);
|
|
630
|
+
if (fs_1.default.existsSync(p))
|
|
631
|
+
return p;
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
function parseFrontmatter(content) {
|
|
636
|
+
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
637
|
+
if (!fm)
|
|
638
|
+
return {};
|
|
639
|
+
const out = {};
|
|
640
|
+
for (const line of fm[1].split('\n')) {
|
|
641
|
+
const m = line.match(/^([A-Za-z_-]+):\s*(.+)$/);
|
|
642
|
+
if (m)
|
|
643
|
+
out[m[1]] = m[2].trim();
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
627
647
|
app.get('/agents/:id/skills', (req, res) => {
|
|
628
648
|
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
629
649
|
if (!managed) {
|
|
@@ -631,11 +651,8 @@ function createServer() {
|
|
|
631
651
|
return;
|
|
632
652
|
}
|
|
633
653
|
const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, managed.config.workspaceDir);
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
? agentSkillsDir
|
|
637
|
-
: path_1.default.join(workspaceDir, '.claude', 'skills');
|
|
638
|
-
if (!fs_1.default.existsSync(skillsDir)) {
|
|
654
|
+
const skillsDir = resolveSkillsDir(workspaceDir);
|
|
655
|
+
if (!skillsDir) {
|
|
639
656
|
res.json({ skills: [] });
|
|
640
657
|
return;
|
|
641
658
|
}
|
|
@@ -646,19 +663,49 @@ function createServer() {
|
|
|
646
663
|
const skillMd = path_1.default.join(skillsDir, entry.name, 'SKILL.md');
|
|
647
664
|
if (!fs_1.default.existsSync(skillMd))
|
|
648
665
|
continue;
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
if (!fmMatch)
|
|
666
|
+
const fm = parseFrontmatter(fs_1.default.readFileSync(skillMd, 'utf8'));
|
|
667
|
+
if (!fm.name && !fm.description)
|
|
652
668
|
continue;
|
|
653
|
-
const nameMatch = fmMatch[1].match(/^name:\s*(.+)/m);
|
|
654
|
-
const descMatch = fmMatch[1].match(/^description:\s*(.+)/m);
|
|
655
669
|
skills.push({
|
|
656
|
-
name:
|
|
657
|
-
description:
|
|
670
|
+
name: fm.name ?? entry.name,
|
|
671
|
+
description: fm.description ?? '',
|
|
672
|
+
userInvocable: fm['user-invocable'] === 'true',
|
|
658
673
|
});
|
|
659
674
|
}
|
|
675
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
660
676
|
res.json({ skills });
|
|
661
677
|
});
|
|
678
|
+
app.get('/agents/:id/skills/:name', (req, res) => {
|
|
679
|
+
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
680
|
+
if (!managed) {
|
|
681
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, managed.config.workspaceDir);
|
|
685
|
+
const skillsDir = resolveSkillsDir(workspaceDir);
|
|
686
|
+
if (!skillsDir) {
|
|
687
|
+
res.status(404).json({ error: 'No skills directory' });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const requested = path_1.default.resolve(skillsDir, req.params.name, 'SKILL.md');
|
|
691
|
+
if (!requested.startsWith(skillsDir + path_1.default.sep)) {
|
|
692
|
+
res.status(400).json({ error: 'Invalid skill name' });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (!fs_1.default.existsSync(requested)) {
|
|
696
|
+
res.status(404).json({ error: 'Skill not found' });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const content = fs_1.default.readFileSync(requested, 'utf8');
|
|
700
|
+
const fm = parseFrontmatter(content);
|
|
701
|
+
res.json({
|
|
702
|
+
name: fm.name ?? req.params.name,
|
|
703
|
+
description: fm.description ?? '',
|
|
704
|
+
userInvocable: fm['user-invocable'] === 'true',
|
|
705
|
+
allowedTools: fm['allowed-tools'] ?? '',
|
|
706
|
+
content,
|
|
707
|
+
});
|
|
708
|
+
});
|
|
662
709
|
app.get('/agents/:id/tasks', (req, res) => {
|
|
663
710
|
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
664
711
|
if (!managed) {
|
|
@@ -1002,7 +1049,11 @@ function createServer() {
|
|
|
1002
1049
|
res.status(404).json({ error: 'Agent not found' });
|
|
1003
1050
|
return;
|
|
1004
1051
|
}
|
|
1005
|
-
|
|
1052
|
+
const workflows = (0, workflows_db_js_1.listWorkflows)(req.params.id).map(w => ({
|
|
1053
|
+
...w,
|
|
1054
|
+
lastRun: (0, workflows_db_js_1.getLatestRun)(req.params.id, w.id),
|
|
1055
|
+
}));
|
|
1056
|
+
res.json(workflows);
|
|
1006
1057
|
});
|
|
1007
1058
|
app.post('/agents/:id/workflows', (req, res) => {
|
|
1008
1059
|
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
@@ -18,6 +18,8 @@ exports.getRun = getRun;
|
|
|
18
18
|
exports.createRunStep = createRunStep;
|
|
19
19
|
exports.updateRunStep = updateRunStep;
|
|
20
20
|
exports.getRunningRuns = getRunningRuns;
|
|
21
|
+
exports.finalizeRunningRuns = finalizeRunningRuns;
|
|
22
|
+
exports.getLatestRun = getLatestRun;
|
|
21
23
|
exports.closeWorkflowsDb = closeWorkflowsDb;
|
|
22
24
|
const path_1 = __importDefault(require("path"));
|
|
23
25
|
const crypto_1 = require("crypto");
|
|
@@ -213,6 +215,41 @@ function getRunningRuns(agentId) {
|
|
|
213
215
|
return [];
|
|
214
216
|
}
|
|
215
217
|
}
|
|
218
|
+
function finalizeRunningRuns(agentId) {
|
|
219
|
+
try {
|
|
220
|
+
const db = getDb(agentId);
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const err = 'Run interrupted — backend restarted while executing.';
|
|
223
|
+
db.prepare(`
|
|
224
|
+
UPDATE run_steps SET status = 'failed', error = COALESCE(error, ?), finished_at = COALESCE(finished_at, ?)
|
|
225
|
+
WHERE status = 'running'
|
|
226
|
+
`).run(err, now);
|
|
227
|
+
db.prepare(`
|
|
228
|
+
UPDATE run_steps SET status = 'skipped'
|
|
229
|
+
WHERE status = 'pending' AND run_id IN (SELECT id FROM runs WHERE status = 'running')
|
|
230
|
+
`).run();
|
|
231
|
+
const result = db.prepare(`
|
|
232
|
+
UPDATE runs SET status = 'failed', finished_at = COALESCE(finished_at, ?)
|
|
233
|
+
WHERE status = 'running'
|
|
234
|
+
`).run(now);
|
|
235
|
+
return result.changes ?? 0;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function getLatestRun(agentId, workflowId) {
|
|
242
|
+
try {
|
|
243
|
+
const db = getDb(agentId);
|
|
244
|
+
const row = db.prepare(`
|
|
245
|
+
SELECT * FROM runs WHERE workflow_id = ? ORDER BY started_at DESC LIMIT 1
|
|
246
|
+
`).get(workflowId);
|
|
247
|
+
return row ? rowToRun(row) : null;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
216
253
|
function closeWorkflowsDb(agentId) {
|
|
217
254
|
const agent = (0, config_js_1.getAgent)(agentId);
|
|
218
255
|
if (!agent)
|