palmier 0.9.26 → 0.9.28
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/command-runners.d.ts +20 -0
- package/dist/command-runners.js +119 -0
- package/dist/commands/run.js +24 -185
- package/dist/commands/serve.js +4 -0
- package/dist/pwa/assets/{index-CsXEu9PI.js → index-B405SEvB.js} +30 -30
- package/dist/pwa/assets/{index-D8gfaj_Y.css → index-BpsVmTp9.css} +1 -1
- package/dist/pwa/assets/{web-CfKyecWX.js → web-BtNEakaJ.js} +1 -1
- package/dist/pwa/assets/{web-BNVl5cy2.js → web-DqJat-O8.js} +1 -1
- package/dist/pwa/assets/{web-D9PV07-0.js → web-SpprOq9I.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/rpc-handler.js +90 -56
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-owned supervisors for command-triggered tasks. A command task's shell
|
|
3
|
+
* command is a long-running trigger source — the daemon spawns it while the task
|
|
4
|
+
* is enabled, reads its stdout, and feeds each line into the shared per-task
|
|
5
|
+
* event queue (the same one the NATS notification/SMS subscriptions populate).
|
|
6
|
+
* The idle→active edge launches a short-lived `palmier run` that drains the
|
|
7
|
+
* queue, so command tasks share the exact lifecycle as on_new_* event tasks:
|
|
8
|
+
* one run per burst, "running" only while the agent is actually invoked.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle parity:
|
|
11
|
+
* - Enabled command task → command process running (= being monitored).
|
|
12
|
+
* - Disable / delete → command process killed; no further triggers.
|
|
13
|
+
* - Abort → kills only the in-flight run; the command process is untouched.
|
|
14
|
+
*/
|
|
15
|
+
import type { HostConfig, ParsedTask } from "./types.js";
|
|
16
|
+
/** Start, stop, or restart a task's command process to match its current state. */
|
|
17
|
+
export declare function reconcileCommandRunner(config: HostConfig, task: ParsedTask): void;
|
|
18
|
+
export declare function stopCommandRunner(taskId: string): void;
|
|
19
|
+
/** Recover command runners for all enabled command tasks (daemon startup). */
|
|
20
|
+
export declare function startEnabledCommandRunners(config: HostConfig): void;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-owned supervisors for command-triggered tasks. A command task's shell
|
|
3
|
+
* command is a long-running trigger source — the daemon spawns it while the task
|
|
4
|
+
* is enabled, reads its stdout, and feeds each line into the shared per-task
|
|
5
|
+
* event queue (the same one the NATS notification/SMS subscriptions populate).
|
|
6
|
+
* The idle→active edge launches a short-lived `palmier run` that drains the
|
|
7
|
+
* queue, so command tasks share the exact lifecycle as on_new_* event tasks:
|
|
8
|
+
* one run per burst, "running" only while the agent is actually invoked.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle parity:
|
|
11
|
+
* - Enabled command task → command process running (= being monitored).
|
|
12
|
+
* - Disable / delete → command process killed; no further triggers.
|
|
13
|
+
* - Abort → kills only the in-flight run; the command process is untouched.
|
|
14
|
+
*/
|
|
15
|
+
import * as readline from "readline";
|
|
16
|
+
import { execFileSync } from "child_process";
|
|
17
|
+
import { spawnStreamingCommand } from "./spawn-command.js";
|
|
18
|
+
import { getTaskDir, listTasks, parseTaskFile } from "./task.js";
|
|
19
|
+
import { enqueueEvent } from "./event-queues.js";
|
|
20
|
+
import { getPlatform } from "./platform/index.js";
|
|
21
|
+
const runners = new Map();
|
|
22
|
+
const stopping = new Set();
|
|
23
|
+
function shouldRun(task) {
|
|
24
|
+
return !!task.frontmatter.command
|
|
25
|
+
&& !!task.frontmatter.schedule_enabled
|
|
26
|
+
&& !task.frontmatter.one_off;
|
|
27
|
+
}
|
|
28
|
+
function killChild(child) {
|
|
29
|
+
if (process.platform === "win32" && child.pid) {
|
|
30
|
+
try {
|
|
31
|
+
execFileSync("taskkill", ["/pid", String(child.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
catch { /* may have already exited */ }
|
|
35
|
+
}
|
|
36
|
+
child.kill();
|
|
37
|
+
}
|
|
38
|
+
function spawnRunner(config, taskId, command) {
|
|
39
|
+
stopping.delete(taskId);
|
|
40
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
41
|
+
const platform = getPlatform();
|
|
42
|
+
const child = spawnStreamingCommand(command, {
|
|
43
|
+
cwd: taskDir,
|
|
44
|
+
env: { ...platform.getGuiEnv(), PALMIER_HTTP_PORT: String(config.httpPort ?? 7256) },
|
|
45
|
+
});
|
|
46
|
+
runners.set(taskId, { child, command });
|
|
47
|
+
console.log(`[command-runner] ${taskId} spawned: ${command}`);
|
|
48
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
49
|
+
rl.on("line", (line) => {
|
|
50
|
+
if (!line.trim())
|
|
51
|
+
return;
|
|
52
|
+
const { shouldStart } = enqueueEvent(taskId, line);
|
|
53
|
+
if (shouldStart) {
|
|
54
|
+
platform.startTask(taskId).catch((err) => {
|
|
55
|
+
console.error(`[command-runner] failed to start run for ${taskId}:`, err);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
child.stderr?.on("data", (d) => process.stderr.write(d));
|
|
60
|
+
const handleExit = () => {
|
|
61
|
+
rl.close();
|
|
62
|
+
if (runners.get(taskId)?.child === child)
|
|
63
|
+
runners.delete(taskId);
|
|
64
|
+
if (stopping.has(taskId)) {
|
|
65
|
+
stopping.delete(taskId);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Exited on its own while still enabled — relaunch so monitoring stays live.
|
|
69
|
+
console.log(`[command-runner] ${taskId} command exited; relaunching in 1s`);
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
if (stopping.has(taskId) || runners.has(taskId))
|
|
72
|
+
return;
|
|
73
|
+
let task;
|
|
74
|
+
try {
|
|
75
|
+
task = parseTaskFile(taskDir);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (shouldRun(task))
|
|
81
|
+
spawnRunner(config, taskId, task.frontmatter.command);
|
|
82
|
+
}, 1000);
|
|
83
|
+
};
|
|
84
|
+
child.on("close", handleExit);
|
|
85
|
+
child.on("error", (err) => {
|
|
86
|
+
console.error(`[command-runner] ${taskId} error:`, err);
|
|
87
|
+
handleExit();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Start, stop, or restart a task's command process to match its current state. */
|
|
91
|
+
export function reconcileCommandRunner(config, task) {
|
|
92
|
+
const taskId = task.frontmatter.id;
|
|
93
|
+
if (!shouldRun(task)) {
|
|
94
|
+
stopCommandRunner(taskId);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const existing = runners.get(taskId);
|
|
98
|
+
if (existing) {
|
|
99
|
+
if (existing.command === task.frontmatter.command)
|
|
100
|
+
return;
|
|
101
|
+
stopCommandRunner(taskId);
|
|
102
|
+
}
|
|
103
|
+
spawnRunner(config, taskId, task.frontmatter.command);
|
|
104
|
+
}
|
|
105
|
+
export function stopCommandRunner(taskId) {
|
|
106
|
+
const existing = runners.get(taskId);
|
|
107
|
+
if (!existing)
|
|
108
|
+
return;
|
|
109
|
+
stopping.add(taskId);
|
|
110
|
+
runners.delete(taskId);
|
|
111
|
+
killChild(existing.child);
|
|
112
|
+
}
|
|
113
|
+
/** Recover command runners for all enabled command tasks (daemon startup). */
|
|
114
|
+
export function startEnabledCommandRunners(config) {
|
|
115
|
+
for (const task of listTasks(config.projectRoot)) {
|
|
116
|
+
if (shouldRun(task))
|
|
117
|
+
reconcileCommandRunner(config, task);
|
|
118
|
+
}
|
|
119
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import
|
|
4
|
-
import { StringCodec } from "nats";
|
|
5
|
-
import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
|
|
3
|
+
import { spawnCommand } from "../spawn-command.js";
|
|
6
4
|
import { loadConfig } from "../config.js";
|
|
7
5
|
import { connectNats } from "../nats-client.js";
|
|
8
6
|
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, appendRunMessage, readRunMessages, getRunDir, beginStreamingMessage } from "../task.js";
|
|
@@ -10,22 +8,10 @@ import { getAgent } from "../agents/agent.js";
|
|
|
10
8
|
import { getPlatform } from "../platform/index.js";
|
|
11
9
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
12
10
|
import { publishHostEvent } from "../events.js";
|
|
13
|
-
async function sendPushNotification(nc, hostId, title, body) {
|
|
14
|
-
if (!nc)
|
|
15
|
-
return;
|
|
16
|
-
try {
|
|
17
|
-
const sc = StringCodec();
|
|
18
|
-
const subject = `host.${hostId}.push.send`;
|
|
19
|
-
await nc.request(subject, sc.encode(JSON.stringify({ hostId, title, body })), { timeout: 15_000 });
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
console.warn(`[push] failed to send notification:`, err);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
11
|
/**
|
|
26
12
|
* Invoke the agent CLI in a continuation loop to handle permission requests.
|
|
27
|
-
* `invokeTask` is the ParsedTask whose prompt is passed to the agent (
|
|
28
|
-
*
|
|
13
|
+
* `invokeTask` is the ParsedTask whose prompt is passed to the agent (for
|
|
14
|
+
* triggered tasks this is the per-trigger augmented task).
|
|
29
15
|
*/
|
|
30
16
|
async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
31
17
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -226,33 +212,18 @@ export async function runCommand(taskId) {
|
|
|
226
212
|
agent, task, taskDir, runId, guiEnv, nc, config, taskId,
|
|
227
213
|
transientPermissions: [],
|
|
228
214
|
};
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
outcome = resolveOutcome(taskDir, result.outcome);
|
|
236
|
-
if (outcome === "aborted")
|
|
237
|
-
break;
|
|
238
|
-
console.log(`Task ${taskId} command exited (${outcome}); auto-restarting.`);
|
|
239
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
240
|
-
if (resolveOutcome(taskDir, "finished") === "aborted") {
|
|
241
|
-
outcome = "aborted";
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
246
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
247
|
-
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
248
|
-
}
|
|
249
|
-
else if (task.frontmatter.schedule_type === "on_new_notification"
|
|
215
|
+
// Command-triggered and on_new_* tasks share the same trigger machinery: the
|
|
216
|
+
// daemon owns the trigger source (the shell command's stdout / a NATS
|
|
217
|
+
// subscription) and feeds the shared per-task queue, while this run drains
|
|
218
|
+
// that queue one invocation at a time.
|
|
219
|
+
if (task.frontmatter.command
|
|
220
|
+
|| task.frontmatter.schedule_type === "on_new_notification"
|
|
250
221
|
|| task.frontmatter.schedule_type === "on_new_sms") {
|
|
251
222
|
const result = await runEventTriggeredMode(ctx);
|
|
252
223
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
253
224
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
254
225
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
255
|
-
console.log(`Task ${taskId} completed (
|
|
226
|
+
console.log(`Task ${taskId} completed (triggered).`);
|
|
256
227
|
}
|
|
257
228
|
else {
|
|
258
229
|
await appendAndNotify(ctx, {
|
|
@@ -284,155 +255,21 @@ export async function runCommand(taskId) {
|
|
|
284
255
|
await cleanup();
|
|
285
256
|
}
|
|
286
257
|
}
|
|
287
|
-
const MAX_QUEUE_SIZE = 100;
|
|
288
|
-
const MAX_LOG_ENTRIES = 1000;
|
|
289
|
-
/** Max input line length (chars). Long emails can take up to 200k chars. */
|
|
290
|
-
const MAX_LINE_LENGTH = 200_000;
|
|
291
|
-
/**
|
|
292
|
-
* Spawn a long-running shell command and invoke the agent CLI once per stdout
|
|
293
|
-
* line, with the user's prompt augmented by that line. Sequential with a
|
|
294
|
-
* bounded queue.
|
|
295
|
-
*/
|
|
296
|
-
async function runCommandTriggeredMode(ctx) {
|
|
297
|
-
const commandStr = ctx.task.frontmatter.command;
|
|
298
|
-
console.log(`[command-triggered] Spawning: ${commandStr}`);
|
|
299
|
-
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
|
|
300
|
-
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
301
|
-
const child = spawnStreamingCommand(commandStr, {
|
|
302
|
-
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
303
|
-
env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
|
|
304
|
-
});
|
|
305
|
-
let linesProcessed = 0;
|
|
306
|
-
let invocationsSucceeded = 0;
|
|
307
|
-
let invocationsFailed = 0;
|
|
308
|
-
const lineQueue = [];
|
|
309
|
-
let processing = false;
|
|
310
|
-
let commandExited = false;
|
|
311
|
-
let resolveWhenDone;
|
|
312
|
-
const logPath = path.join(getRunDir(ctx.taskDir, ctx.runId), "command-output.log");
|
|
313
|
-
function appendLog(line, agentOutput, outcome) {
|
|
314
|
-
const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
|
|
315
|
-
fs.appendFileSync(logPath, entry, "utf-8");
|
|
316
|
-
try {
|
|
317
|
-
const content = fs.readFileSync(logPath, "utf-8");
|
|
318
|
-
const entries = content.split("\n---\n").filter(Boolean);
|
|
319
|
-
if (entries.length > MAX_LOG_ENTRIES) {
|
|
320
|
-
const trimmed = entries.slice(-MAX_LOG_ENTRIES).join("\n---\n") + "\n---\n";
|
|
321
|
-
fs.writeFileSync(logPath, trimmed, "utf-8");
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
catch { /* ignore trim errors */ }
|
|
325
|
-
}
|
|
326
|
-
async function processLine(line) {
|
|
327
|
-
linesProcessed++;
|
|
328
|
-
if (line.length > MAX_LINE_LENGTH) {
|
|
329
|
-
console.warn(`[command-triggered] Skipping line #${linesProcessed}: ${line.length} chars exceeds limit`);
|
|
330
|
-
invocationsFailed++;
|
|
331
|
-
appendLog(line.slice(0, 200) + "...(truncated)", "", "skipped");
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
console.log(`[command-triggered] Processing line #${linesProcessed}: ${line}`);
|
|
335
|
-
const perLinePrompt = `${ctx.task.frontmatter.user_prompt}\n\nProcess this input:\n${line}`;
|
|
336
|
-
const perLineTask = {
|
|
337
|
-
frontmatter: { ...ctx.task.frontmatter, user_prompt: perLinePrompt },
|
|
338
|
-
};
|
|
339
|
-
const result = await invokeAgentWithRetries(ctx, perLineTask);
|
|
340
|
-
if (result.outcome === "finished") {
|
|
341
|
-
invocationsSucceeded++;
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
invocationsFailed++;
|
|
345
|
-
const taskLabel = ctx.task.frontmatter.name || ctx.task.frontmatter.user_prompt;
|
|
346
|
-
await sendPushNotification(ctx.nc, ctx.config.hostId, `Task "${taskLabel}" invocation failed`, line.length > 200 ? line.slice(0, 200) + "…" : line);
|
|
347
|
-
}
|
|
348
|
-
appendLog(line, "", result.outcome);
|
|
349
|
-
// Signal "waiting for more input" in the UI.
|
|
350
|
-
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
|
|
351
|
-
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
352
|
-
}
|
|
353
|
-
async function drainQueue() {
|
|
354
|
-
if (processing)
|
|
355
|
-
return;
|
|
356
|
-
processing = true;
|
|
357
|
-
try {
|
|
358
|
-
while (lineQueue.length > 0) {
|
|
359
|
-
const line = lineQueue.shift();
|
|
360
|
-
await processLine(line);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
finally {
|
|
364
|
-
processing = false;
|
|
365
|
-
if (commandExited && lineQueue.length === 0 && resolveWhenDone) {
|
|
366
|
-
resolveWhenDone();
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
const rl = readline.createInterface({ input: child.stdout });
|
|
371
|
-
rl.on("line", (line) => {
|
|
372
|
-
if (!line.trim())
|
|
373
|
-
return;
|
|
374
|
-
if (lineQueue.length >= MAX_QUEUE_SIZE) {
|
|
375
|
-
console.warn(`[command-triggered] Queue full, dropping oldest line.`);
|
|
376
|
-
lineQueue.shift();
|
|
377
|
-
}
|
|
378
|
-
lineQueue.push(line);
|
|
379
|
-
drainQueue().catch((err) => {
|
|
380
|
-
console.error(`[command-triggered] Error processing line:`, err);
|
|
381
|
-
invocationsFailed++;
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
let stderrBuf = "";
|
|
385
|
-
child.stderr?.on("data", (d) => {
|
|
386
|
-
const chunk = d.toString();
|
|
387
|
-
stderrBuf += chunk;
|
|
388
|
-
process.stderr.write(d);
|
|
389
|
-
});
|
|
390
|
-
const exitCode = await new Promise((resolve) => {
|
|
391
|
-
child.on("close", (code) => {
|
|
392
|
-
commandExited = true;
|
|
393
|
-
rl.close();
|
|
394
|
-
resolve(code);
|
|
395
|
-
});
|
|
396
|
-
child.on("error", (err) => {
|
|
397
|
-
console.error(`[command-triggered] Command error:`, err);
|
|
398
|
-
stderrBuf += err.message;
|
|
399
|
-
commandExited = true;
|
|
400
|
-
rl.close();
|
|
401
|
-
resolve(1);
|
|
402
|
-
});
|
|
403
|
-
});
|
|
404
|
-
if (lineQueue.length > 0 || processing) {
|
|
405
|
-
await new Promise((resolve) => {
|
|
406
|
-
resolveWhenDone = resolve;
|
|
407
|
-
drainQueue();
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
const endTime = Date.now();
|
|
411
|
-
if (exitCode !== 0) {
|
|
412
|
-
const errorDetail = stderrBuf.trim() || `Command exited with code ${exitCode}`;
|
|
413
|
-
appendRunMessage(ctx.taskDir, ctx.runId, {
|
|
414
|
-
role: "status",
|
|
415
|
-
time: endTime,
|
|
416
|
-
content: errorDetail,
|
|
417
|
-
type: "error",
|
|
418
|
-
});
|
|
419
|
-
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
420
|
-
return { outcome: "failed", endTime };
|
|
421
|
-
}
|
|
422
|
-
return { outcome: "finished", endTime };
|
|
423
|
-
}
|
|
424
258
|
/**
|
|
425
|
-
* Drain the daemon-owned per-task event queue via /task-event/pop, invoking
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
259
|
+
* Drain the daemon-owned per-task event queue via /task-event/pop, invoking the
|
|
260
|
+
* agent once per queued trigger. The run process holds no subscription of its
|
|
261
|
+
* own — the daemon owns the trigger source (a NATS subscription for on_new_*
|
|
262
|
+
* tasks, the command's stdout for command tasks) and atomically clears the
|
|
263
|
+
* active flag on empty pop so it can fire a fresh run on the next trigger.
|
|
429
264
|
*/
|
|
430
265
|
async function runEventTriggeredMode(ctx) {
|
|
431
|
-
const
|
|
432
|
-
const label =
|
|
266
|
+
const isCommand = !!ctx.task.frontmatter.command;
|
|
267
|
+
const label = isCommand
|
|
268
|
+
? "input"
|
|
269
|
+
: ctx.task.frontmatter.schedule_type === "on_new_notification" ? "notification" : "SMS";
|
|
433
270
|
const port = ctx.config.httpPort ?? 7256;
|
|
434
271
|
const popUrl = `http://localhost:${port}/task-event/pop?taskId=${encodeURIComponent(ctx.taskId)}`;
|
|
435
|
-
console.log(`[
|
|
272
|
+
console.log(`[triggered] Draining ${label} queue`);
|
|
436
273
|
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
|
|
437
274
|
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
438
275
|
let eventsProcessed = 0;
|
|
@@ -447,8 +284,10 @@ async function runEventTriggeredMode(ctx) {
|
|
|
447
284
|
if (body.empty || !body.event)
|
|
448
285
|
break;
|
|
449
286
|
eventsProcessed++;
|
|
450
|
-
console.log(`[
|
|
451
|
-
const perEventPrompt =
|
|
287
|
+
console.log(`[triggered] Processing ${label} #${eventsProcessed}`);
|
|
288
|
+
const perEventPrompt = isCommand
|
|
289
|
+
? `${ctx.task.frontmatter.user_prompt}\n\nProcess this input:\n${body.event}`
|
|
290
|
+
: `${ctx.task.frontmatter.user_prompt}\n\nProcess this new ${label}:\n${body.event}`;
|
|
452
291
|
const perEventTask = {
|
|
453
292
|
frontmatter: { ...ctx.task.frontmatter, user_prompt: perEventPrompt },
|
|
454
293
|
};
|
package/dist/commands/serve.js
CHANGED
|
@@ -15,6 +15,7 @@ import { StringCodec } from "nats";
|
|
|
15
15
|
import { addNotification } from "../notification-store.js";
|
|
16
16
|
import { addSmsMessage } from "../sms-store.js";
|
|
17
17
|
import { enqueueEvent } from "../event-queues.js";
|
|
18
|
+
import { startEnabledCommandRunners } from "../command-runners.js";
|
|
18
19
|
const POLL_INTERVAL_MS = 30_000;
|
|
19
20
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
20
21
|
/**
|
|
@@ -122,6 +123,9 @@ export async function serveCommand() {
|
|
|
122
123
|
console.error(`Warning: failed to install timer for task ${task.frontmatter.id}: ${err}`);
|
|
123
124
|
}
|
|
124
125
|
}
|
|
126
|
+
// Spawn the long-running command process for every enabled command task —
|
|
127
|
+
// the daemon owns these the same way it owns the device-event subscriptions.
|
|
128
|
+
startEnabledCommandRunners(config);
|
|
125
129
|
setInterval(() => {
|
|
126
130
|
checkStaleTasks(config, nc).catch((err) => {
|
|
127
131
|
console.error("[monitor] Error checking stale tasks:", err);
|