palmier 0.9.3 → 0.9.4
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/commands/run.js +6 -0
- package/dist/commands/serve.js +61 -36
- package/dist/pwa/assets/{index-CknFGshO.js → index-DX5qJgHZ.js} +4 -4
- package/dist/pwa/assets/{web-DdzXb-jW.js → web-Dcldtodb.js} +1 -1
- package/dist/pwa/assets/{web-Dl9aC-Qr.js → web-DdVpqhvX.js} +1 -1
- package/dist/pwa/assets/{web-a9jK1xeo.js → web-Eg0A6HEi.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/manifest.webmanifest +1 -1
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +41 -19
- package/dist/task.d.ts +7 -0
- package/dist/task.js +17 -0
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
- package/palmier-server/pwa/index.html +1 -1
- package/palmier-server/pwa/src/components/RunDetailView.tsx +1 -1
- package/palmier-server/pwa/src/components/TaskForm.tsx +5 -3
- package/palmier-server/pwa/vite.config.ts +1 -1
- package/src/commands/run.ts +3 -0
- package/src/commands/serve.ts +62 -37
- package/src/rpc-handler.ts +34 -18
- package/src/task.ts +21 -0
- package/src/types.ts +4 -0
package/src/rpc-handler.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { type ChildProcess } from "child_process";
|
|
5
5
|
import { type NatsConnection } from "nats";
|
|
6
|
-
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
|
|
6
|
+
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir, writeFollowupStatus, readFollowupStatus, deleteFollowupStatus } from "./task.js";
|
|
7
7
|
import { resolvePending, getPending, listPending } from "./pending-requests.js";
|
|
8
8
|
import { getPlatform } from "./platform/index.js";
|
|
9
9
|
import { spawnCommand } from "./spawn-command.js";
|
|
@@ -310,6 +310,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
310
310
|
agent: params.agent,
|
|
311
311
|
schedule_enabled: false,
|
|
312
312
|
requires_confirmation: params.requires_confirmation ?? false,
|
|
313
|
+
one_off: true,
|
|
313
314
|
...(params.yolo_mode ? { yolo_mode: true } : {}),
|
|
314
315
|
...(params.foreground_mode ? { foreground_mode: true } : {}),
|
|
315
316
|
...(params.command ? { command: params.command } : {}),
|
|
@@ -322,13 +323,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
322
323
|
const runId = createRunDir(taskDir, name, Date.now(), params.agent);
|
|
323
324
|
appendHistory(config.projectRoot, { task_id: id, run_id: runId });
|
|
324
325
|
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
stdio: "ignore",
|
|
329
|
-
windowsHide: true,
|
|
330
|
-
});
|
|
331
|
-
child.unref();
|
|
326
|
+
const platform = getPlatform();
|
|
327
|
+
platform.installTaskTimer(config, task);
|
|
328
|
+
await platform.startTask(id);
|
|
332
329
|
|
|
333
330
|
return { ok: true, task_id: id, run_id: runId };
|
|
334
331
|
}
|
|
@@ -397,6 +394,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
397
394
|
});
|
|
398
395
|
if (stdin != null) child.stdin!.end(stdin);
|
|
399
396
|
activeFollowups.set(followupKey, child);
|
|
397
|
+
if (child.pid) writeFollowupStatus(followupRunDir, { pid: child.pid, spawned_at: Date.now() });
|
|
400
398
|
|
|
401
399
|
const chunks: Buffer[] = [];
|
|
402
400
|
child.stdout?.on("data", (d: Buffer) => chunks.push(d));
|
|
@@ -404,6 +402,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
404
402
|
|
|
405
403
|
child.on("close", async (code: number | null) => {
|
|
406
404
|
activeFollowups.delete(followupKey);
|
|
405
|
+
deleteFollowupStatus(followupRunDir);
|
|
407
406
|
// stop_followup already wrote the stopped status.
|
|
408
407
|
if (child.killed) return;
|
|
409
408
|
|
|
@@ -428,6 +427,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
428
427
|
|
|
429
428
|
child.on("error", async (err: Error) => {
|
|
430
429
|
activeFollowups.delete(followupKey);
|
|
430
|
+
deleteFollowupStatus(followupRunDir);
|
|
431
431
|
console.error(`Follow-up failed for ${followupKey}:`, err);
|
|
432
432
|
appendRunMessage(followupTaskDir, params.run_id, {
|
|
433
433
|
role: "status",
|
|
@@ -447,22 +447,33 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
447
447
|
return { error: "run_id is required" };
|
|
448
448
|
}
|
|
449
449
|
const stopKey = `${params.id}:${params.run_id}`;
|
|
450
|
+
const stopTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
451
|
+
const stopRunDir = getRunDir(stopTaskDir, params.run_id);
|
|
450
452
|
const child = activeFollowups.get(stopKey);
|
|
453
|
+
|
|
454
|
+
let pidToKill: number | undefined = child?.pid;
|
|
451
455
|
if (!child) {
|
|
452
|
-
|
|
456
|
+
// Daemon restarted since spawn — the in-memory handle is gone but
|
|
457
|
+
// the child may still be running. Fall back to the persisted PID.
|
|
458
|
+
const persisted = readFollowupStatus(stopRunDir);
|
|
459
|
+
if (!persisted) return { error: "No active follow-up for this run" };
|
|
460
|
+
pidToKill = persisted.pid;
|
|
453
461
|
}
|
|
454
462
|
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
child
|
|
463
|
+
if (pidToKill !== undefined) {
|
|
464
|
+
if (process.platform === "win32") {
|
|
465
|
+
try {
|
|
466
|
+
const { execFileSync } = await import("child_process");
|
|
467
|
+
execFileSync("taskkill", ["/pid", String(pidToKill), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
468
|
+
} catch { /* may have already exited */ }
|
|
469
|
+
} else if (child) {
|
|
470
|
+
child.kill();
|
|
471
|
+
} else {
|
|
472
|
+
try { process.kill(pidToKill, "SIGTERM"); } catch { /* already dead */ }
|
|
473
|
+
}
|
|
462
474
|
}
|
|
463
475
|
|
|
464
476
|
// child.killed stops the close handler from double-writing the status.
|
|
465
|
-
const stopTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
466
477
|
appendRunMessage(stopTaskDir, params.run_id, {
|
|
467
478
|
role: "status",
|
|
468
479
|
time: Date.now(),
|
|
@@ -470,6 +481,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
470
481
|
type: "stopped",
|
|
471
482
|
});
|
|
472
483
|
activeFollowups.delete(stopKey);
|
|
484
|
+
deleteFollowupStatus(stopRunDir);
|
|
473
485
|
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
474
486
|
return { ok: true, task_id: params.id, run_id: params.run_id };
|
|
475
487
|
}
|
|
@@ -509,6 +521,10 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
509
521
|
console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
|
|
510
522
|
return { error: `Failed to abort task: ${e.stderr || e.message}` };
|
|
511
523
|
}
|
|
524
|
+
try {
|
|
525
|
+
const aborted = parseTaskFile(abortTaskDir);
|
|
526
|
+
if (aborted.frontmatter.one_off) getPlatform().removeTaskTimer(params.id);
|
|
527
|
+
} catch { /* best-effort cleanup */ }
|
|
512
528
|
const abortPayload: Record<string, unknown> = { event_type: "running-state", running_state: "aborted" };
|
|
513
529
|
await publishHostEvent(nc, config.hostId, params.id, abortPayload);
|
|
514
530
|
return { ok: true, task_id: params.id };
|
package/src/task.ts
CHANGED
|
@@ -116,6 +116,27 @@ export function readTaskStatus(taskDir: string): TaskStatus | undefined {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
export interface FollowupStatus {
|
|
120
|
+
pid: number;
|
|
121
|
+
spawned_at: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function writeFollowupStatus(runDir: string, status: FollowupStatus): void {
|
|
125
|
+
fs.writeFileSync(path.join(runDir, "followup.json"), JSON.stringify(status), "utf-8");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function readFollowupStatus(runDir: string): FollowupStatus | undefined {
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(fs.readFileSync(path.join(runDir, "followup.json"), "utf-8")) as FollowupStatus;
|
|
131
|
+
} catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function deleteFollowupStatus(runDir: string): void {
|
|
137
|
+
try { fs.unlinkSync(path.join(runDir, "followup.json")); } catch { /* ignore */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
/** Returns the run ID (timestamp string used as directory name). */
|
|
120
141
|
export function createRunDir(
|
|
121
142
|
taskDir: string,
|
package/src/types.ts
CHANGED
|
@@ -37,6 +37,10 @@ export interface TaskFrontmatter {
|
|
|
37
37
|
foreground_mode?: boolean;
|
|
38
38
|
permissions?: RequiredPermission[];
|
|
39
39
|
command?: string;
|
|
40
|
+
/** Set when the task was created via task.run_oneoff. Used so the run process
|
|
41
|
+
* can tear down its OS scheduler unit when it finishes — one-off tasks aren't
|
|
42
|
+
* in tasks.jsonl so the daemon's recovery/sweep logic doesn't cover them. */
|
|
43
|
+
one_off?: boolean;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
export interface ParsedTask {
|