palmier 0.9.3 → 0.9.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.
Files changed (33) hide show
  1. package/dist/commands/run.js +6 -0
  2. package/dist/commands/serve.js +61 -36
  3. package/dist/pwa/assets/index-Cvffaohh.js +120 -0
  4. package/dist/pwa/assets/{index-BsB1tIsn.css → index-DBgOYBrB.css} +1 -1
  5. package/dist/pwa/assets/{web-Dl9aC-Qr.js → web-ChtbM4nv.js} +1 -1
  6. package/dist/pwa/assets/{web-DdzXb-jW.js → web-hExASsqW.js} +1 -1
  7. package/dist/pwa/assets/{web-a9jK1xeo.js → web-qdLcAD7T.js} +1 -1
  8. package/dist/pwa/index.html +3 -3
  9. package/dist/pwa/manifest.webmanifest +1 -1
  10. package/dist/pwa/service-worker.js +1 -1
  11. package/dist/rpc-handler.js +61 -19
  12. package/dist/task.d.ts +7 -0
  13. package/dist/task.js +17 -0
  14. package/dist/types.d.ts +4 -0
  15. package/package.json +1 -1
  16. package/palmier-server/pwa/index.html +1 -1
  17. package/palmier-server/pwa/src/App.css +6 -0
  18. package/palmier-server/pwa/src/components/HostMenu.tsx +27 -0
  19. package/palmier-server/pwa/src/components/RunDetailView.tsx +2 -1
  20. package/palmier-server/pwa/src/components/SessionsView.tsx +2 -1
  21. package/palmier-server/pwa/src/components/TaskCard.tsx +14 -6
  22. package/palmier-server/pwa/src/components/TaskForm.tsx +14 -8
  23. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +12 -0
  24. package/palmier-server/pwa/src/formatTime.ts +38 -4
  25. package/palmier-server/pwa/src/pages/Dashboard.tsx +4 -2
  26. package/palmier-server/pwa/src/types.ts +2 -0
  27. package/palmier-server/pwa/vite.config.ts +1 -1
  28. package/src/commands/run.ts +3 -0
  29. package/src/commands/serve.ts +62 -37
  30. package/src/rpc-handler.ts +48 -18
  31. package/src/task.ts +21 -0
  32. package/src/types.ts +4 -0
  33. package/dist/pwa/assets/index-CknFGshO.js +0 -120
@@ -150,6 +150,12 @@ export async function runCommand(taskId) {
150
150
  if (nc && !nc.isClosed()) {
151
151
  await nc.drain();
152
152
  }
153
+ if (task.frontmatter.one_off) {
154
+ try {
155
+ getPlatform().removeTaskTimer(taskId);
156
+ }
157
+ catch { /* best-effort */ }
158
+ }
153
159
  };
154
160
  try {
155
161
  nc = await connectNats(config);
@@ -5,7 +5,7 @@ import { connectNats } from "../nats-client.js";
5
5
  import { createRpcHandler } from "../rpc-handler.js";
6
6
  import { startNatsTransport } from "../transports/nats-transport.js";
7
7
  import { startHttpTransport } from "../transports/http-transport.js";
8
- import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage, listTasks } from "../task.js";
8
+ import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage, listTasks, readFollowupStatus, deleteFollowupStatus } from "../task.js";
9
9
  import { publishHostEvent } from "../events.js";
10
10
  import { getPlatform } from "../platform/index.js";
11
11
  import { detectAgents } from "../agents/agent.js";
@@ -18,54 +18,79 @@ import { enqueueEvent } from "../event-queues.js";
18
18
  const POLL_INTERVAL_MS = 30_000;
19
19
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
20
20
  /**
21
- * Reconcile tasks stuck in "started" whose process is no longer alive.
21
+ * Reconcile tasks stuck in "started" whose process is no longer alive, and
22
+ * clean up OS scheduler units for one-off tasks that have already terminated.
22
23
  * The system scheduler (Task Scheduler / systemd) is the authoritative source.
23
24
  */
24
25
  async function checkStaleTasks(config, nc) {
25
- const tasksJsonl = path.join(config.projectRoot, "tasks.jsonl");
26
- if (!fs.existsSync(tasksJsonl))
26
+ const tasksRoot = path.join(config.projectRoot, "tasks");
27
+ if (!fs.existsSync(tasksRoot))
27
28
  return;
28
29
  const platform = getPlatform();
29
- const lines = fs.readFileSync(tasksJsonl, "utf-8").split("\n").filter(Boolean);
30
- for (const line of lines) {
31
- let taskId;
30
+ const taskIds = fs.readdirSync(tasksRoot).filter((f) => fs.statSync(path.join(tasksRoot, f)).isDirectory());
31
+ for (const taskId of taskIds) {
32
+ const taskDir = getTaskDir(config.projectRoot, taskId);
33
+ const status = readTaskStatus(taskDir);
34
+ if (!status)
35
+ continue;
36
+ let task;
32
37
  try {
33
- taskId = JSON.parse(line).task_id;
38
+ task = parseTaskFile(taskDir);
34
39
  }
35
40
  catch {
36
41
  continue;
37
42
  }
38
- const taskDir = getTaskDir(config.projectRoot, taskId);
39
- const status = readTaskStatus(taskDir);
40
- if (!status || status.running_state !== "started")
41
- continue;
42
- if (platform.isTaskRunning(taskId))
43
- continue;
44
- console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
45
- const endTime = Date.now();
46
- writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
47
- const runId = fs.readdirSync(taskDir)
48
- .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
49
- .sort()
50
- .pop();
51
- if (runId) {
52
- appendRunMessage(taskDir, runId, {
53
- role: "status",
54
- time: endTime,
55
- content: "",
56
- type: "failed",
43
+ if (status.running_state === "started" && !platform.isTaskRunning(taskId)) {
44
+ console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
45
+ const endTime = Date.now();
46
+ writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
47
+ const runId = fs.readdirSync(taskDir)
48
+ .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
49
+ .sort()
50
+ .pop();
51
+ if (runId) {
52
+ appendRunMessage(taskDir, runId, {
53
+ role: "status",
54
+ time: endTime,
55
+ content: "",
56
+ type: "failed",
57
+ });
58
+ }
59
+ await publishHostEvent(nc, config.hostId, taskId, {
60
+ event_type: "running-state",
61
+ running_state: "failed",
62
+ name: task.frontmatter.name || taskId,
57
63
  });
58
64
  }
59
- let taskName = taskId;
60
- try {
61
- taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
65
+ if (task.frontmatter.one_off && status.running_state !== "started") {
66
+ try {
67
+ platform.removeTaskTimer(taskId);
68
+ }
69
+ catch { /* best-effort */ }
70
+ }
71
+ // Reconcile orphaned follow-ups: if a run has a persisted follow-up PID
72
+ // but that process is no longer alive, clear the file and mark the run
73
+ // as failed so the UI doesn't claim it's still running.
74
+ const runIds = fs.readdirSync(taskDir).filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")));
75
+ for (const runId of runIds) {
76
+ const runDir = path.join(taskDir, runId);
77
+ const followup = readFollowupStatus(runDir);
78
+ if (!followup)
79
+ continue;
80
+ try {
81
+ process.kill(followup.pid, 0);
82
+ }
83
+ catch {
84
+ deleteFollowupStatus(runDir);
85
+ appendRunMessage(taskDir, runId, {
86
+ role: "status",
87
+ time: Date.now(),
88
+ content: "",
89
+ type: "failed",
90
+ });
91
+ await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
92
+ }
62
93
  }
63
- catch { /* fallback to taskId */ }
64
- await publishHostEvent(nc, config.hostId, taskId, {
65
- event_type: "running-state",
66
- running_state: "failed",
67
- name: taskName,
68
- });
69
94
  }
70
95
  }
71
96
  export async function serveCommand() {