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.
@@ -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 { spawn, type ChildProcess } from "child_process";
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 script = process.argv[1] || "palmier";
326
- const child = spawn(process.execPath, [script, "run", id], {
327
- detached: true,
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
- return { error: "No active follow-up for this run" };
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 (process.platform === "win32" && child.pid) {
456
- try {
457
- const { execFileSync } = await import("child_process");
458
- execFileSync("taskkill", ["/pid", String(child.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
459
- } catch { /* may have already exited */ }
460
- } else {
461
- child.kill();
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 {