palmier 0.4.3 → 0.4.5
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/README.md +7 -4
- package/dist/agents/agent.d.ts +2 -2
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +4 -4
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +4 -4
- package/dist/agents/copilot.d.ts +1 -1
- package/dist/agents/copilot.js +3 -3
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +4 -4
- package/dist/agents/openclaw.d.ts +1 -1
- package/dist/agents/openclaw.js +2 -2
- package/dist/commands/request-input.d.ts +1 -2
- package/dist/commands/request-input.js +7 -21
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +57 -64
- package/dist/commands/serve.js +31 -33
- package/dist/platform/linux.js +16 -6
- package/dist/platform/windows.js +37 -12
- package/dist/rpc-handler.js +177 -30
- package/dist/task.d.ts +13 -13
- package/dist/task.js +59 -51
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/agents/agent.ts +2 -2
- package/src/agents/claude.ts +3 -3
- package/src/agents/codex.ts +3 -3
- package/src/agents/copilot.ts +3 -3
- package/src/agents/gemini.ts +3 -3
- package/src/agents/openclaw.ts +2 -2
- package/src/commands/request-input.ts +7 -21
- package/src/commands/run.ts +57 -67
- package/src/commands/serve.ts +34 -41
- package/src/platform/linux.ts +17 -7
- package/src/platform/windows.ts +36 -13
- package/src/rpc-handler.ts +195 -34
- package/src/task.ts +60 -55
- package/src/types.ts +2 -2
- package/test/agent-output-parsing.test.ts +1 -14
package/dist/platform/linux.js
CHANGED
|
@@ -3,6 +3,8 @@ import * as path from "path";
|
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { execSync, exec } from "child_process";
|
|
5
5
|
import { promisify } from "util";
|
|
6
|
+
import { loadConfig } from "../config.js";
|
|
7
|
+
import { getTaskDir, readTaskStatus } from "../task.js";
|
|
6
8
|
const execAsync = promisify(exec);
|
|
7
9
|
const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
|
|
8
10
|
const PATH_FILE = path.join(homedir(), ".config", "palmier", "user-path");
|
|
@@ -205,18 +207,26 @@ WantedBy=timers.target
|
|
|
205
207
|
await execAsync(`systemctl --user stop ${serviceName}`);
|
|
206
208
|
}
|
|
207
209
|
isTaskRunning(taskId) {
|
|
210
|
+
// Check systemd first (for scheduled/on-demand runs)
|
|
208
211
|
const serviceName = getServiceName(taskId);
|
|
209
212
|
try {
|
|
210
|
-
// is-active exits 0 only for "active". For oneshot services (Type=oneshot),
|
|
211
|
-
// the state is "activating" while running, which exits non-zero.
|
|
212
|
-
// Use show -p ActiveState to reliably get the state without exit code issues.
|
|
213
213
|
const out = execSync(`systemctl --user show -p ActiveState --value ${serviceName}`, { encoding: "utf-8" });
|
|
214
214
|
const state = out.trim();
|
|
215
|
-
|
|
215
|
+
if (state === "active" || state === "activating")
|
|
216
|
+
return true;
|
|
216
217
|
}
|
|
217
|
-
catch {
|
|
218
|
-
|
|
218
|
+
catch { /* service may not exist */ }
|
|
219
|
+
// Fall back to PID check (for follow-up runs spawned directly)
|
|
220
|
+
try {
|
|
221
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
222
|
+
const status = readTaskStatus(taskDir);
|
|
223
|
+
if (status?.pid) {
|
|
224
|
+
process.kill(status.pid, 0); // signal 0 = check if process exists
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
219
227
|
}
|
|
228
|
+
catch { /* process not running or config unavailable */ }
|
|
229
|
+
return false;
|
|
220
230
|
}
|
|
221
231
|
getGuiEnv() {
|
|
222
232
|
const uid = process.getuid?.();
|
package/dist/platform/windows.js
CHANGED
|
@@ -8,14 +8,6 @@ const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
|
8
8
|
const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
9
9
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
10
10
|
const DAEMON_VBS_FILE = path.join(CONFIG_DIR, "daemon.vbs");
|
|
11
|
-
/**
|
|
12
|
-
* Build the /tr value for schtasks: a single string with quoted paths
|
|
13
|
-
* so Task Scheduler can invoke node with the palmier script + subcommand.
|
|
14
|
-
*/
|
|
15
|
-
function schtasksTr(...subcommand) {
|
|
16
|
-
const script = process.argv[1] || "palmier";
|
|
17
|
-
return `"${process.execPath}" "${script}" ${subcommand.join(" ")}`;
|
|
18
|
-
}
|
|
19
11
|
const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
20
12
|
/**
|
|
21
13
|
* Convert a cron expression or "once" trigger to Task Scheduler XML trigger elements.
|
|
@@ -144,7 +136,13 @@ export class WindowsPlatform {
|
|
|
144
136
|
installTaskTimer(config, task) {
|
|
145
137
|
const taskId = task.frontmatter.id;
|
|
146
138
|
const tn = schtasksTaskName(taskId);
|
|
147
|
-
const
|
|
139
|
+
const script = process.argv[1] || "palmier";
|
|
140
|
+
// Write a VBS launcher so the task runs without a visible console window
|
|
141
|
+
const vbsPath = path.join(CONFIG_DIR, `task-${taskId}.vbs`);
|
|
142
|
+
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" run ${taskId}", 0, True`;
|
|
143
|
+
fs.writeFileSync(vbsPath, vbs, "utf-8");
|
|
144
|
+
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
145
|
+
const tr = `"${wscript}" "${vbsPath}"`;
|
|
148
146
|
// Build trigger XML elements
|
|
149
147
|
const triggerElements = [];
|
|
150
148
|
if (task.frontmatter.triggers_enabled) {
|
|
@@ -192,6 +190,10 @@ export class WindowsPlatform {
|
|
|
192
190
|
catch {
|
|
193
191
|
// Task might not exist — that's fine
|
|
194
192
|
}
|
|
193
|
+
try {
|
|
194
|
+
fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`));
|
|
195
|
+
}
|
|
196
|
+
catch { /* ignore */ }
|
|
195
197
|
}
|
|
196
198
|
async startTask(taskId) {
|
|
197
199
|
const tn = schtasksTaskName(taskId);
|
|
@@ -228,17 +230,40 @@ export class WindowsPlatform {
|
|
|
228
230
|
}
|
|
229
231
|
}
|
|
230
232
|
isTaskRunning(taskId) {
|
|
233
|
+
// Check Task Scheduler first (for scheduled/on-demand runs)
|
|
231
234
|
const tn = schtasksTaskName(taskId);
|
|
232
235
|
try {
|
|
233
236
|
const out = execFileSync("schtasks", ["/query", "/tn", tn, "/fo", "CSV", "/nh"], {
|
|
234
237
|
encoding: "utf-8",
|
|
235
238
|
windowsHide: true,
|
|
236
239
|
});
|
|
237
|
-
|
|
240
|
+
if (out.includes('"Running"'))
|
|
241
|
+
return true;
|
|
238
242
|
}
|
|
239
|
-
catch {
|
|
240
|
-
|
|
243
|
+
catch { /* task may not exist in scheduler */ }
|
|
244
|
+
// Fall back to PID check (for follow-up runs spawned directly, not via schtasks)
|
|
245
|
+
try {
|
|
246
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
247
|
+
const status = readTaskStatus(taskDir);
|
|
248
|
+
if (status?.pid) {
|
|
249
|
+
// tasklist exits 0 if the PID is found
|
|
250
|
+
execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/nh"], {
|
|
251
|
+
encoding: "utf-8",
|
|
252
|
+
windowsHide: true,
|
|
253
|
+
stdio: "pipe",
|
|
254
|
+
});
|
|
255
|
+
// tasklist always exits 0; check if output contains the PID
|
|
256
|
+
const out = execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/fo", "CSV", "/nh"], {
|
|
257
|
+
encoding: "utf-8",
|
|
258
|
+
windowsHide: true,
|
|
259
|
+
stdio: "pipe",
|
|
260
|
+
});
|
|
261
|
+
if (out.includes(`"${status.pid}"`))
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
241
264
|
}
|
|
265
|
+
catch { /* ignore */ }
|
|
266
|
+
return false;
|
|
242
267
|
}
|
|
243
268
|
getGuiEnv() {
|
|
244
269
|
// Windows GUI is always available — no special env vars needed
|
package/dist/rpc-handler.js
CHANGED
|
@@ -4,13 +4,15 @@ import * as path from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
6
|
import { parse as parseYaml } from "yaml";
|
|
7
|
-
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory,
|
|
7
|
+
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
|
|
8
8
|
import { getPlatform } from "./platform/index.js";
|
|
9
9
|
import { spawnCommand } from "./spawn-command.js";
|
|
10
|
+
import crossSpawn from "cross-spawn";
|
|
10
11
|
import { getAgent } from "./agents/agent.js";
|
|
11
12
|
import { validateSession } from "./session-store.js";
|
|
12
13
|
import { publishHostEvent } from "./events.js";
|
|
13
14
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
15
|
+
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
14
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
17
|
const PLAN_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "commands", "plan-generation.md"), "utf-8");
|
|
16
18
|
/**
|
|
@@ -28,13 +30,26 @@ function parseResultFrontmatter(raw) {
|
|
|
28
30
|
meta[line.slice(0, sep).trim()] = line.slice(sep + 2).trim();
|
|
29
31
|
}
|
|
30
32
|
const messages = parseConversationMessages(fmMatch[2]);
|
|
33
|
+
// Derive state from status messages — just look at the last one
|
|
34
|
+
const statusMessages = messages.filter((m) => m.role === "status");
|
|
35
|
+
const lastStatus = statusMessages[statusMessages.length - 1];
|
|
36
|
+
const startedMsg = statusMessages.find((m) => m.type === "started");
|
|
37
|
+
const terminalStates = ["finished", "failed", "aborted"];
|
|
38
|
+
const terminalMsg = [...statusMessages].reverse().find((m) => terminalStates.includes(m.type ?? ""));
|
|
39
|
+
// If last status is "started", determine if it's a task run or follow-up
|
|
40
|
+
let runningState;
|
|
41
|
+
if (lastStatus?.type === "started") {
|
|
42
|
+
runningState = terminalMsg ? "followup" : "started";
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
runningState = lastStatus?.type;
|
|
46
|
+
}
|
|
31
47
|
return {
|
|
32
48
|
messages,
|
|
33
49
|
task_name: meta.task_name,
|
|
34
|
-
running_state:
|
|
35
|
-
start_time:
|
|
36
|
-
end_time:
|
|
37
|
-
task_file: meta.task_file,
|
|
50
|
+
running_state: runningState,
|
|
51
|
+
start_time: startedMsg?.time || undefined,
|
|
52
|
+
end_time: terminalMsg?.time || undefined,
|
|
38
53
|
};
|
|
39
54
|
}
|
|
40
55
|
/**
|
|
@@ -101,6 +116,8 @@ async function generatePlan(projectRoot, userPrompt, agentName) {
|
|
|
101
116
|
}
|
|
102
117
|
return { name, body };
|
|
103
118
|
}
|
|
119
|
+
/** Active follow-up child processes, keyed by "taskId:runId". */
|
|
120
|
+
const activeFollowups = new Map();
|
|
104
121
|
/**
|
|
105
122
|
* Create a transport-agnostic RPC handler bound to the given config.
|
|
106
123
|
*/
|
|
@@ -242,8 +259,8 @@ export function createRpcHandler(config, nc) {
|
|
|
242
259
|
writeTaskFile(taskDir, task);
|
|
243
260
|
// Do NOT append to tasks.jsonl — this is a one-off run
|
|
244
261
|
// Create initial result file so it appears in runs list immediately
|
|
245
|
-
const
|
|
246
|
-
appendHistory(config.projectRoot, { task_id: id,
|
|
262
|
+
const runId = createRunDir(taskDir, name, Date.now());
|
|
263
|
+
appendHistory(config.projectRoot, { task_id: id, run_id: runId });
|
|
247
264
|
// Spawn `palmier run <id>` directly as a detached process
|
|
248
265
|
const script = process.argv[1] || "palmier";
|
|
249
266
|
const child = spawn(process.execPath, [script, "run", id], {
|
|
@@ -252,7 +269,7 @@ export function createRpcHandler(config, nc) {
|
|
|
252
269
|
windowsHide: true,
|
|
253
270
|
});
|
|
254
271
|
child.unref();
|
|
255
|
-
return { ok: true, task_id: id,
|
|
272
|
+
return { ok: true, task_id: id, run_id: runId };
|
|
256
273
|
}
|
|
257
274
|
case "task.run": {
|
|
258
275
|
const params = request.params;
|
|
@@ -260,10 +277,10 @@ export function createRpcHandler(config, nc) {
|
|
|
260
277
|
// Create initial result file so it appears in runs list immediately
|
|
261
278
|
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
262
279
|
const runTask = parseTaskFile(runTaskDir);
|
|
263
|
-
const
|
|
264
|
-
appendHistory(config.projectRoot, { task_id: params.id,
|
|
280
|
+
const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now());
|
|
281
|
+
appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
|
|
265
282
|
await getPlatform().startTask(params.id);
|
|
266
|
-
return { ok: true, task_id: params.id,
|
|
283
|
+
return { ok: true, task_id: params.id, run_id: taskRunId };
|
|
267
284
|
}
|
|
268
285
|
catch (err) {
|
|
269
286
|
const e = err;
|
|
@@ -271,15 +288,145 @@ export function createRpcHandler(config, nc) {
|
|
|
271
288
|
return { error: `Failed to start task: ${e.stderr || e.message}` };
|
|
272
289
|
}
|
|
273
290
|
}
|
|
291
|
+
case "task.followup": {
|
|
292
|
+
const params = request.params;
|
|
293
|
+
if (!params.run_id || !params.message?.trim()) {
|
|
294
|
+
return { error: "run_id and message are required" };
|
|
295
|
+
}
|
|
296
|
+
const followupKey = `${params.id}:${params.run_id}`;
|
|
297
|
+
if (activeFollowups.has(followupKey)) {
|
|
298
|
+
return { error: "A follow-up is already running for this run" };
|
|
299
|
+
}
|
|
300
|
+
const followupTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
301
|
+
const followupTask = parseTaskFile(followupTaskDir);
|
|
302
|
+
const followupRunDir = getRunDir(followupTaskDir, params.run_id);
|
|
303
|
+
// Append user message + started status
|
|
304
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
305
|
+
role: "user",
|
|
306
|
+
time: Date.now(),
|
|
307
|
+
content: params.message,
|
|
308
|
+
});
|
|
309
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
310
|
+
role: "status",
|
|
311
|
+
time: Date.now(),
|
|
312
|
+
content: "",
|
|
313
|
+
type: "started",
|
|
314
|
+
});
|
|
315
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
316
|
+
// Fire-and-forget: invoke agent inline as a child of the serve process
|
|
317
|
+
const followupAgent = getAgent(followupTask.frontmatter.agent);
|
|
318
|
+
const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(followupTask, params.message, followupTask.frontmatter.permissions);
|
|
319
|
+
// Spawn directly via crossSpawn so we can track and kill the child
|
|
320
|
+
const child = crossSpawn(cmd, cmdArgs, {
|
|
321
|
+
cwd: followupRunDir,
|
|
322
|
+
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
323
|
+
env: { ...process.env, PALMIER_TASK_ID: params.id },
|
|
324
|
+
windowsHide: true,
|
|
325
|
+
});
|
|
326
|
+
if (stdin != null)
|
|
327
|
+
child.stdin.end(stdin);
|
|
328
|
+
activeFollowups.set(followupKey, child);
|
|
329
|
+
// Collect output
|
|
330
|
+
const chunks = [];
|
|
331
|
+
child.stdout?.on("data", (d) => chunks.push(d));
|
|
332
|
+
child.stderr?.on("data", (d) => process.stderr.write(d));
|
|
333
|
+
child.on("close", async (code) => {
|
|
334
|
+
activeFollowups.delete(followupKey);
|
|
335
|
+
// If killed by stop_followup, the stopped status is already written
|
|
336
|
+
if (child.killed)
|
|
337
|
+
return;
|
|
338
|
+
const output = Buffer.concat(chunks).toString("utf-8");
|
|
339
|
+
const outcome = code !== 0 ? "failed" : parseTaskOutcome(output);
|
|
340
|
+
const reportFiles = parseReportFiles(output);
|
|
341
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
342
|
+
role: "assistant",
|
|
343
|
+
time: Date.now(),
|
|
344
|
+
content: stripPalmierMarkers(output),
|
|
345
|
+
attachments: reportFiles.length > 0 ? reportFiles : undefined,
|
|
346
|
+
});
|
|
347
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
348
|
+
role: "status",
|
|
349
|
+
time: Date.now(),
|
|
350
|
+
content: "",
|
|
351
|
+
type: outcome,
|
|
352
|
+
});
|
|
353
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
354
|
+
});
|
|
355
|
+
child.on("error", async (err) => {
|
|
356
|
+
activeFollowups.delete(followupKey);
|
|
357
|
+
console.error(`Follow-up failed for ${followupKey}:`, err);
|
|
358
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
359
|
+
role: "status",
|
|
360
|
+
time: Date.now(),
|
|
361
|
+
content: "",
|
|
362
|
+
type: "failed",
|
|
363
|
+
});
|
|
364
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
365
|
+
});
|
|
366
|
+
return { ok: true, task_id: params.id, run_id: params.run_id };
|
|
367
|
+
}
|
|
368
|
+
case "task.stop_followup": {
|
|
369
|
+
const params = request.params;
|
|
370
|
+
if (!params.run_id) {
|
|
371
|
+
return { error: "run_id is required" };
|
|
372
|
+
}
|
|
373
|
+
const stopKey = `${params.id}:${params.run_id}`;
|
|
374
|
+
const child = activeFollowups.get(stopKey);
|
|
375
|
+
if (!child) {
|
|
376
|
+
return { error: "No active follow-up for this run" };
|
|
377
|
+
}
|
|
378
|
+
// Kill the child process tree
|
|
379
|
+
if (process.platform === "win32" && child.pid) {
|
|
380
|
+
try {
|
|
381
|
+
const { execFileSync } = await import("child_process");
|
|
382
|
+
execFileSync("taskkill", ["/pid", String(child.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
383
|
+
}
|
|
384
|
+
catch { /* may have already exited */ }
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
child.kill();
|
|
388
|
+
}
|
|
389
|
+
// Append stopped status (child.killed prevents the close handler from writing)
|
|
390
|
+
const stopTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
391
|
+
appendRunMessage(stopTaskDir, params.run_id, {
|
|
392
|
+
role: "status",
|
|
393
|
+
time: Date.now(),
|
|
394
|
+
content: "",
|
|
395
|
+
type: "stopped",
|
|
396
|
+
});
|
|
397
|
+
activeFollowups.delete(stopKey);
|
|
398
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
399
|
+
return { ok: true, task_id: params.id, run_id: params.run_id };
|
|
400
|
+
}
|
|
274
401
|
case "task.abort": {
|
|
275
402
|
const params = request.params;
|
|
403
|
+
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
404
|
+
// Read the PID before overwriting status — stopTask needs it to
|
|
405
|
+
// kill the entire process tree on Windows.
|
|
406
|
+
const abortPrevStatus = readTaskStatus(abortTaskDir);
|
|
276
407
|
// Write abort status BEFORE killing so the dying process's signal
|
|
277
408
|
// handler can detect this was RPC-initiated and skip publishing.
|
|
278
|
-
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
279
409
|
writeTaskStatus(abortTaskDir, {
|
|
280
410
|
running_state: "aborted",
|
|
281
411
|
time_stamp: Date.now(),
|
|
412
|
+
...(abortPrevStatus?.pid ? { pid: abortPrevStatus.pid } : {}),
|
|
282
413
|
});
|
|
414
|
+
// Append aborted status to the latest run
|
|
415
|
+
try {
|
|
416
|
+
const runDirs = fs.readdirSync(abortTaskDir)
|
|
417
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(abortTaskDir, f, "TASKRUN.md")))
|
|
418
|
+
.sort();
|
|
419
|
+
const latestRunId = runDirs[runDirs.length - 1];
|
|
420
|
+
if (latestRunId) {
|
|
421
|
+
appendRunMessage(abortTaskDir, latestRunId, {
|
|
422
|
+
role: "status",
|
|
423
|
+
time: Date.now(),
|
|
424
|
+
content: "",
|
|
425
|
+
type: "aborted",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch { /* best-effort */ }
|
|
283
430
|
try {
|
|
284
431
|
await getPlatform().stopTask(params.id);
|
|
285
432
|
}
|
|
@@ -304,25 +451,26 @@ export function createRpcHandler(config, nc) {
|
|
|
304
451
|
}
|
|
305
452
|
case "task.result": {
|
|
306
453
|
const params = request.params;
|
|
307
|
-
if (!params.
|
|
308
|
-
return { error: "
|
|
454
|
+
if (!params.run_id) {
|
|
455
|
+
return { error: "run_id is required" };
|
|
309
456
|
}
|
|
310
|
-
const
|
|
457
|
+
const taskrunPath = path.join(config.projectRoot, "tasks", params.id, params.run_id, "TASKRUN.md");
|
|
311
458
|
try {
|
|
312
|
-
const raw = fs.readFileSync(
|
|
459
|
+
const raw = fs.readFileSync(taskrunPath, "utf-8");
|
|
313
460
|
const meta = parseResultFrontmatter(raw);
|
|
314
461
|
return { task_id: params.id, ...meta };
|
|
315
462
|
}
|
|
316
463
|
catch {
|
|
317
|
-
return { task_id: params.id, error: "
|
|
464
|
+
return { task_id: params.id, error: "Run not found" };
|
|
318
465
|
}
|
|
319
466
|
}
|
|
320
467
|
case "task.reports": {
|
|
321
468
|
const params = request.params;
|
|
322
|
-
if (!Array.isArray(params.report_files) || params.report_files.length === 0) {
|
|
323
|
-
return { error: "
|
|
469
|
+
if (!params.run_id || !Array.isArray(params.report_files) || params.report_files.length === 0) {
|
|
470
|
+
return { error: "run_id and report_files are required" };
|
|
324
471
|
}
|
|
325
472
|
const reports = [];
|
|
473
|
+
const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
|
|
326
474
|
for (const file of params.report_files) {
|
|
327
475
|
if (!file.endsWith(".md")) {
|
|
328
476
|
reports.push({ file, error: "must end with .md" });
|
|
@@ -333,7 +481,7 @@ export function createRpcHandler(config, nc) {
|
|
|
333
481
|
reports.push({ file, error: "must be a plain filename" });
|
|
334
482
|
continue;
|
|
335
483
|
}
|
|
336
|
-
const reportPath = path.join(
|
|
484
|
+
const reportPath = path.join(runDir, basename);
|
|
337
485
|
try {
|
|
338
486
|
const content = fs.readFileSync(reportPath, "utf-8");
|
|
339
487
|
reports.push({ file, content });
|
|
@@ -355,7 +503,7 @@ export function createRpcHandler(config, nc) {
|
|
|
355
503
|
console.log(`[task.user_input] ${params.id} → ${params.value}`);
|
|
356
504
|
return { ok: true };
|
|
357
505
|
}
|
|
358
|
-
case "
|
|
506
|
+
case "taskrun.list": {
|
|
359
507
|
const params = request.params;
|
|
360
508
|
const { entries, total } = readHistory(config.projectRoot, {
|
|
361
509
|
offset: params.offset ?? 0,
|
|
@@ -363,30 +511,29 @@ export function createRpcHandler(config, nc) {
|
|
|
363
511
|
task_id: params.task_id,
|
|
364
512
|
});
|
|
365
513
|
const enriched = entries.map((entry) => {
|
|
366
|
-
const
|
|
514
|
+
const taskrunPath = path.join(config.projectRoot, "tasks", entry.task_id, entry.run_id, "TASKRUN.md");
|
|
367
515
|
try {
|
|
368
|
-
const raw = fs.readFileSync(
|
|
516
|
+
const raw = fs.readFileSync(taskrunPath, "utf-8");
|
|
369
517
|
const meta = parseResultFrontmatter(raw);
|
|
370
|
-
// Exclude messages from list response
|
|
371
518
|
const { messages: _, ...rest } = meta;
|
|
372
519
|
return { ...entry, ...rest };
|
|
373
520
|
}
|
|
374
521
|
catch {
|
|
375
|
-
return { ...entry, error: "
|
|
522
|
+
return { ...entry, error: "Run not found" };
|
|
376
523
|
}
|
|
377
524
|
});
|
|
378
525
|
return { entries: enriched, total };
|
|
379
526
|
}
|
|
380
|
-
case "
|
|
527
|
+
case "taskrun.delete": {
|
|
381
528
|
const params = request.params;
|
|
382
|
-
if (!params.task_id || !params.
|
|
383
|
-
return { error: "task_id and
|
|
529
|
+
if (!params.task_id || !params.run_id) {
|
|
530
|
+
return { error: "task_id and run_id are required" };
|
|
384
531
|
}
|
|
385
|
-
const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.
|
|
532
|
+
const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.run_id);
|
|
386
533
|
if (!deleted) {
|
|
387
534
|
return { error: "History entry not found" };
|
|
388
535
|
}
|
|
389
|
-
return { ok: true, task_id: params.task_id,
|
|
536
|
+
return { ok: true, task_id: params.task_id, run_id: params.run_id };
|
|
390
537
|
}
|
|
391
538
|
case "host.update": {
|
|
392
539
|
const error = await performUpdate();
|
package/dist/task.d.ts
CHANGED
|
@@ -39,31 +39,31 @@ export declare function writeTaskStatus(taskDir: string, status: TaskStatus): vo
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function readTaskStatus(taskDir: string): TaskStatus | undefined;
|
|
41
41
|
/**
|
|
42
|
-
* Create
|
|
43
|
-
*
|
|
44
|
-
* Returns the result file name.
|
|
42
|
+
* Create a run directory with an initial TASKRUN.md file.
|
|
43
|
+
* Returns the run ID (timestamp string used as directory name).
|
|
45
44
|
*/
|
|
46
|
-
export declare function
|
|
45
|
+
export declare function createRunDir(taskDir: string, taskName: string, startTime: number): string;
|
|
47
46
|
/**
|
|
48
|
-
*
|
|
47
|
+
* Get the path to a run directory.
|
|
49
48
|
*/
|
|
50
|
-
export declare function
|
|
49
|
+
export declare function getRunDir(taskDir: string, runId: string): string;
|
|
51
50
|
/**
|
|
52
|
-
*
|
|
51
|
+
* Append a conversation message to a run's TASKRUN.md file.
|
|
53
52
|
*/
|
|
54
|
-
export declare function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
export declare function appendRunMessage(taskDir: string, runId: string, msg: ConversationMessage): void;
|
|
54
|
+
/**
|
|
55
|
+
* Read conversation messages from a run's TASKRUN.md file.
|
|
56
|
+
*/
|
|
57
|
+
export declare function readRunMessages(taskDir: string, runId: string): ConversationMessage[];
|
|
58
58
|
/**
|
|
59
59
|
* Append a history entry to the project-level history.jsonl file.
|
|
60
60
|
*/
|
|
61
61
|
export declare function appendHistory(projectRoot: string, entry: HistoryEntry): void;
|
|
62
62
|
/**
|
|
63
|
-
* Delete a history entry and its associated
|
|
63
|
+
* Delete a history entry and its associated run directory.
|
|
64
64
|
* Returns true if the entry was found and removed.
|
|
65
65
|
*/
|
|
66
|
-
export declare function deleteHistoryEntry(projectRoot: string, taskId: string,
|
|
66
|
+
export declare function deleteHistoryEntry(projectRoot: string, taskId: string, runId: string): boolean;
|
|
67
67
|
/**
|
|
68
68
|
* Read history entries from history.jsonl with pagination.
|
|
69
69
|
* Returns entries sorted most-recent-first.
|
package/dist/task.js
CHANGED
|
@@ -130,21 +130,27 @@ export function readTaskStatus(taskDir) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
|
-
* Create
|
|
134
|
-
*
|
|
135
|
-
* Returns the result file name.
|
|
133
|
+
* Create a run directory with an initial TASKRUN.md file.
|
|
134
|
+
* Returns the run ID (timestamp string used as directory name).
|
|
136
135
|
*/
|
|
137
|
-
export function
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
export function createRunDir(taskDir, taskName, startTime) {
|
|
137
|
+
const runId = String(startTime);
|
|
138
|
+
const runDir = path.join(taskDir, runId);
|
|
139
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
140
|
+
const content = `---\ntask_name: ${taskName}\n---\n\n`;
|
|
141
|
+
fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
|
|
142
|
+
return runId;
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
|
-
*
|
|
145
|
+
* Get the path to a run directory.
|
|
146
146
|
*/
|
|
147
|
-
export function
|
|
147
|
+
export function getRunDir(taskDir, runId) {
|
|
148
|
+
return path.join(taskDir, runId);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Append a conversation message to a run's TASKRUN.md file.
|
|
152
|
+
*/
|
|
153
|
+
export function appendRunMessage(taskDir, runId, msg) {
|
|
148
154
|
const attrs = [`role="${msg.role}"`, `time="${msg.time}"`];
|
|
149
155
|
if (msg.type)
|
|
150
156
|
attrs.push(`type="${msg.type}"`);
|
|
@@ -152,29 +158,41 @@ export function appendResultMessage(taskDir, resultFile, msg) {
|
|
|
152
158
|
attrs.push(`attachments="${msg.attachments.join(",")}"`);
|
|
153
159
|
const delimiter = `<!-- palmier:message ${attrs.join(" ")} -->`;
|
|
154
160
|
const entry = `${delimiter}\n\n${msg.content}\n\n`;
|
|
155
|
-
fs.appendFileSync(path.join(taskDir,
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
*
|
|
159
|
-
*/
|
|
160
|
-
export function
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
fs.appendFileSync(path.join(taskDir, runId, "TASKRUN.md"), entry, "utf-8");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read conversation messages from a run's TASKRUN.md file.
|
|
165
|
+
*/
|
|
166
|
+
export function readRunMessages(taskDir, runId) {
|
|
167
|
+
const raw = fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
|
|
168
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
169
|
+
if (!fmMatch)
|
|
170
|
+
return [];
|
|
171
|
+
const body = fmMatch[1];
|
|
172
|
+
const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
|
|
173
|
+
const matches = [...body.matchAll(delimiterRegex)];
|
|
174
|
+
if (matches.length === 0)
|
|
175
|
+
return [];
|
|
176
|
+
const messages = [];
|
|
177
|
+
for (let i = 0; i < matches.length; i++) {
|
|
178
|
+
const match = matches[i];
|
|
179
|
+
const attrs = match[1];
|
|
180
|
+
const start = match.index + match[0].length;
|
|
181
|
+
const end = i + 1 < matches.length ? matches[i + 1].index : body.length;
|
|
182
|
+
const content = body.slice(start, end).trim();
|
|
183
|
+
const roleAttr = attrs.match(/role="([^"]*)"/)?.[1] ?? "assistant";
|
|
184
|
+
const timeAttr = attrs.match(/time="([^"]*)"/)?.[1] ?? "0";
|
|
185
|
+
const typeAttr = attrs.match(/type="([^"]*)"/)?.[1];
|
|
186
|
+
const attachmentsAttr = attrs.match(/attachments="([^"]*)"/)?.[1];
|
|
187
|
+
messages.push({
|
|
188
|
+
role: roleAttr,
|
|
189
|
+
time: Number(timeAttr),
|
|
190
|
+
content,
|
|
191
|
+
...(typeAttr ? { type: typeAttr } : {}),
|
|
192
|
+
...(attachmentsAttr ? { attachments: attachmentsAttr.split(",").map((f) => f.trim()).filter(Boolean) } : {}),
|
|
193
|
+
});
|
|
176
194
|
}
|
|
177
|
-
|
|
195
|
+
return messages;
|
|
178
196
|
}
|
|
179
197
|
/**
|
|
180
198
|
* Append a history entry to the project-level history.jsonl file.
|
|
@@ -184,10 +202,10 @@ export function appendHistory(projectRoot, entry) {
|
|
|
184
202
|
fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
185
203
|
}
|
|
186
204
|
/**
|
|
187
|
-
* Delete a history entry and its associated
|
|
205
|
+
* Delete a history entry and its associated run directory.
|
|
188
206
|
* Returns true if the entry was found and removed.
|
|
189
207
|
*/
|
|
190
|
-
export function deleteHistoryEntry(projectRoot, taskId,
|
|
208
|
+
export function deleteHistoryEntry(projectRoot, taskId, runId) {
|
|
191
209
|
const historyPath = path.join(projectRoot, "history.jsonl");
|
|
192
210
|
if (!fs.existsSync(historyPath))
|
|
193
211
|
return false;
|
|
@@ -197,9 +215,9 @@ export function deleteHistoryEntry(projectRoot, taskId, resultFile) {
|
|
|
197
215
|
for (const line of lines) {
|
|
198
216
|
try {
|
|
199
217
|
const entry = JSON.parse(line);
|
|
200
|
-
if (entry.task_id === taskId && entry.
|
|
218
|
+
if (entry.task_id === taskId && entry.run_id === runId) {
|
|
201
219
|
found = true;
|
|
202
|
-
continue;
|
|
220
|
+
continue;
|
|
203
221
|
}
|
|
204
222
|
}
|
|
205
223
|
catch { /* keep malformed lines */ }
|
|
@@ -207,21 +225,11 @@ export function deleteHistoryEntry(projectRoot, taskId, resultFile) {
|
|
|
207
225
|
}
|
|
208
226
|
if (!found)
|
|
209
227
|
return false;
|
|
210
|
-
// Rewrite history.jsonl without the deleted entry
|
|
211
228
|
fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
|
|
212
|
-
// Delete the
|
|
213
|
-
const
|
|
214
|
-
if (fs.existsSync(
|
|
215
|
-
fs.
|
|
216
|
-
}
|
|
217
|
-
// Delete the corresponding task snapshot (TASK-<timestamp>.md)
|
|
218
|
-
const tsMatch = resultFile.match(/^RESULT-(\d+)\.md$/);
|
|
219
|
-
if (tsMatch) {
|
|
220
|
-
const snapshotFile = `TASK-${tsMatch[1]}.md`;
|
|
221
|
-
const snapshotPath = path.join(projectRoot, "tasks", taskId, snapshotFile);
|
|
222
|
-
if (fs.existsSync(snapshotPath)) {
|
|
223
|
-
fs.unlinkSync(snapshotPath);
|
|
224
|
-
}
|
|
229
|
+
// Delete the run directory
|
|
230
|
+
const runDir = path.join(projectRoot, "tasks", taskId, runId);
|
|
231
|
+
if (fs.existsSync(runDir)) {
|
|
232
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
225
233
|
}
|
|
226
234
|
return true;
|
|
227
235
|
}
|