palmier 0.4.3 → 0.4.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.
@@ -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, createResultFile } from "./task.js";
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: meta.running_state,
35
- start_time: meta.start_time ? Number(meta.start_time) : undefined,
36
- end_time: meta.end_time ? Number(meta.end_time) : undefined,
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 resultFileName = createResultFile(taskDir, name, Date.now());
246
- appendHistory(config.projectRoot, { task_id: id, result_file: resultFileName });
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, result_file: resultFileName };
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 runResultFileName = createResultFile(runTaskDir, runTask.frontmatter.name, Date.now());
264
- appendHistory(config.projectRoot, { task_id: params.id, result_file: runResultFileName });
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, result_file: runResultFileName };
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.result_file) {
308
- return { error: "result_file is required" };
454
+ if (!params.run_id) {
455
+ return { error: "run_id is required" };
309
456
  }
310
- const resultPath = path.join(config.projectRoot, "tasks", params.id, params.result_file);
457
+ const taskrunPath = path.join(config.projectRoot, "tasks", params.id, params.run_id, "TASKRUN.md");
311
458
  try {
312
- const raw = fs.readFileSync(resultPath, "utf-8");
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: "No result file found" };
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: "report_files must be a non-empty array" };
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(config.projectRoot, "tasks", params.id, basename);
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 "activity.list": {
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 resultPath = path.join(config.projectRoot, "tasks", entry.task_id, entry.result_file);
514
+ const taskrunPath = path.join(config.projectRoot, "tasks", entry.task_id, entry.run_id, "TASKRUN.md");
367
515
  try {
368
- const raw = fs.readFileSync(resultPath, "utf-8");
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: "Result file not found" };
522
+ return { ...entry, error: "Run not found" };
376
523
  }
377
524
  });
378
525
  return { entries: enriched, total };
379
526
  }
380
- case "activity.delete": {
527
+ case "taskrun.delete": {
381
528
  const params = request.params;
382
- if (!params.task_id || !params.result_file) {
383
- return { error: "task_id and result_file are required" };
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.result_file);
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, result_file: params.result_file };
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 the initial result file when a task starts running.
43
- * Contains only start_time and running_state=started; no end_time or content yet.
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 createResultFile(taskDir: string, taskName: string, startTime: number): string;
45
+ export declare function createRunDir(taskDir: string, taskName: string, startTime: number): string;
47
46
  /**
48
- * Append a conversation message to a RESULT file.
47
+ * Get the path to a run directory.
49
48
  */
50
- export declare function appendResultMessage(taskDir: string, resultFile: string, msg: ConversationMessage): void;
49
+ export declare function getRunDir(taskDir: string, runId: string): string;
51
50
  /**
52
- * Update frontmatter fields in a RESULT file without touching the body.
51
+ * Append a conversation message to a run's TASKRUN.md file.
53
52
  */
54
- export declare function finalizeResultFrontmatter(taskDir: string, resultFile: string, updates: {
55
- end_time?: number;
56
- running_state?: string;
57
- }): void;
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 result/task-snapshot files.
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, resultFile: string): boolean;
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 the initial result file when a task starts running.
134
- * Contains only start_time and running_state=started; no end_time or content yet.
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 createResultFile(taskDir, taskName, startTime) {
138
- const resultFileName = `RESULT-${startTime}.md`;
139
- const taskSnapshotName = `TASK-${startTime}.md`;
140
- const content = `---\ntask_name: ${taskName}\nrunning_state: started\nstart_time: ${startTime}\ntask_file: ${taskSnapshotName}\n---\n\n`;
141
- fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
142
- return resultFileName;
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
- * Append a conversation message to a RESULT file.
145
+ * Get the path to a run directory.
146
146
  */
147
- export function appendResultMessage(taskDir, resultFile, msg) {
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, resultFile), entry, "utf-8");
156
- }
157
- /**
158
- * Update frontmatter fields in a RESULT file without touching the body.
159
- */
160
- export function finalizeResultFrontmatter(taskDir, resultFile, updates) {
161
- const filePath = path.join(taskDir, resultFile);
162
- const raw = fs.readFileSync(filePath, "utf-8");
163
- const fmEnd = raw.indexOf("\n---\n", 4); // skip opening ---
164
- if (fmEnd === -1)
165
- return;
166
- let frontmatter = raw.slice(0, fmEnd);
167
- const body = raw.slice(fmEnd);
168
- for (const [key, value] of Object.entries(updates)) {
169
- const regex = new RegExp(`^${key}:.*$`, "m");
170
- if (regex.test(frontmatter)) {
171
- frontmatter = frontmatter.replace(regex, `${key}: ${value}`);
172
- }
173
- else {
174
- frontmatter += `\n${key}: ${value}`;
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
- fs.writeFileSync(filePath, frontmatter + body, "utf-8");
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 result/task-snapshot files.
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, resultFile) {
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.result_file === resultFile) {
218
+ if (entry.task_id === taskId && entry.run_id === runId) {
201
219
  found = true;
202
- continue; // skip this entry
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 result file
213
- const resultPath = path.join(projectRoot, "tasks", taskId, resultFile);
214
- if (fs.existsSync(resultPath)) {
215
- fs.unlinkSync(resultPath);
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
  }
package/dist/types.d.ts CHANGED
@@ -61,7 +61,7 @@ export interface TaskStatus {
61
61
  }
62
62
  export interface HistoryEntry {
63
63
  task_id: string;
64
- result_file: string;
64
+ run_id: string;
65
65
  }
66
66
  export interface RequiredPermission {
67
67
  name: string;
@@ -71,7 +71,7 @@ export interface ConversationMessage {
71
71
  role: "assistant" | "user" | "status";
72
72
  time: number;
73
73
  content: string;
74
- type?: "input" | "permission" | "confirmation" | "started" | "finished" | "failed" | "aborted";
74
+ type?: "input" | "permission" | "confirmation" | "started" | "finished" | "failed" | "aborted" | "stopped";
75
75
  attachments?: string[];
76
76
  }
77
77
  export interface RpcMessage {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -1,14 +1,13 @@
1
1
  import { loadConfig } from "../config.js";
2
2
  import { connectNats } from "../nats-client.js";
3
- import { getTaskDir, parseTaskFile, appendResultMessage } from "../task.js";
3
+ import { getTaskDir, parseTaskFile, appendRunMessage } from "../task.js";
4
4
  import { requestUserInput, publishInputResolved } from "../user-input.js";
5
5
 
6
6
  /**
7
7
  * Request input from the user and print responses to stdout.
8
8
  * Usage: palmier request-input --description "Question 1" --description "Question 2"
9
9
  *
10
- * Requires PALMIER_TASK_ID environment variable to be set.
11
- * Outputs each response on its own line: "description: value"
10
+ * Requires PALMIER_TASK_ID and PALMIER_RUN_DIR environment variables.
12
11
  */
13
12
  export async function requestInputCommand(opts: { description: string[] }): Promise<void> {
14
13
  const taskId = process.env.PALMIER_TASK_ID;
@@ -21,36 +20,23 @@ export async function requestInputCommand(opts: { description: string[] }): Prom
21
20
  const nc = await connectNats(config);
22
21
  const taskDir = getTaskDir(config.projectRoot, taskId);
23
22
  const task = parseTaskFile(taskDir);
23
+ const runId = process.env.PALMIER_RUN_DIR?.split(/[/\\]/).pop();
24
24
 
25
25
  try {
26
26
  const response = await requestUserInput(nc, config, taskId, task.frontmatter.name, taskDir, opts.description);
27
27
  await publishInputResolved(nc, config, taskId, response === "aborted" ? "aborted" : "provided");
28
28
 
29
29
  if (response === "aborted") {
30
- // Write abort as user message if RESULT file is available
31
- const resultFile = process.env.PALMIER_RESULT_FILE;
32
- if (resultFile) {
33
- appendResultMessage(taskDir, resultFile, {
34
- role: "user",
35
- time: Date.now(),
36
- content: "Input request aborted.",
37
- type: "input",
38
- });
30
+ if (runId) {
31
+ appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Input request aborted.", type: "input" });
39
32
  }
40
33
  console.error("User aborted the input request.");
41
34
  process.exit(1);
42
35
  }
43
36
 
44
- // Write user input as a conversation message
45
- const resultFile = process.env.PALMIER_RESULT_FILE;
46
- if (resultFile) {
37
+ if (runId) {
47
38
  const lines = opts.description.map((desc, i) => `**${desc}** ${response[i]}`);
48
- appendResultMessage(taskDir, resultFile, {
49
- role: "user",
50
- time: Date.now(),
51
- content: lines.join("\n"),
52
- type: "input",
53
- });
39
+ appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: lines.join("\n"), type: "input" });
54
40
  }
55
41
 
56
42
  for (let i = 0; i < opts.description.length; i++) {