palmier 1.1.24 → 1.1.26

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 CHANGED
@@ -53,8 +53,6 @@ Every task Palmier runs gets these capabilities automatically. Their usage is wr
53
53
  | Capability | Description |
54
54
  |------------|-------------|
55
55
  | `notify` | Send a push notification to the user's device |
56
- | `request-input` | Request input from the user (blocks until response; aborting the prompt aborts the task) |
57
- | `request-choice` | Ask the user a question with a fixed set of options and return the one picked; a single option serves as a confirmation (blocks until response; aborting aborts the task) |
58
56
  | `fill-password` | Fill a saved password or PIN into the active browser session and submit the form, prompting the user if unknown (the secret is never revealed to the agent) |
59
57
  | `device-geolocation` | Get GPS location of the user's mobile device |
60
58
  | `read-contacts` | Read the contact list from the user's device |
@@ -75,6 +73,12 @@ Every task Palmier runs gets these capabilities automatically. Their usage is wr
75
73
 
76
74
  All device tools work while the Palmier Android app is in the background — they communicate via FCM data messages which wake the app's service even when it's not in the foreground. Each host has one **linked device**: the phone the host uses for SMS, contacts, location, and other device capabilities. Choose it at pair time (the "Link the host to this device" checkbox) or later from the drawer. Each capability must be enabled and its Android permission granted via toggles in the linked device's drawer.
77
75
 
76
+ ### Asking the User
77
+
78
+ When an agent needs something from the user, it writes its question as its final message and ends with the `[PALMIER_TASK_NEED_INPUT]` marker. The agent CLI then exits and the run moves to a "needs input" state, with a push notification that opens the run. The user answers in the run view's message box, or aborts from there.
79
+
80
+ `palmier run` stays alive during the wait, so anything the agent left running, such as its browser, stays open. Palmier polls for the reply and resumes the same agent session with it, repeating until the agent reports success or failure. An unanswered question fails the run after 24 hours. The reply is handed over through a file in the run directory, so a daemon restart doesn't lose the wait.
81
+
78
82
  ### Files in Agent Output
79
83
 
80
84
  Agents run with the task's run directory (`~/palmier/tasks/<task-id>/<run-id>/`) as their working directory and are instructed to link any file they produce with a relative markdown link, e.g. `[View report](report.md)` or `![Chart](chart.png)`. The app opens these links over whichever transport is in use (local, relay, or auto-LAN). It fetches the file through the `task.file` RPC in 512 KiB chunks, so large files fit within the relay's message size. Markdown files render in-app, with their own relative links and images resolved from the file's location. Text and images are previewed, and other file types are offered as a browser download. The host only serves files whose real path (after resolving symlinks) is inside that task's directory, up to 25 MB. A run whose output links an existing file triggers a "report ready" push notification.
@@ -2,7 +2,6 @@ import { registerPending } from "./pending-requests.js";
2
2
  import { getLinkedDevice } from "./linked-device.js";
3
3
  import { getNotifications } from "./notification-store.js";
4
4
  import { getSmsMessages } from "./sms-store.js";
5
- import { getTaskDir, readHistory, spliceUserMessage } from "./task.js";
6
5
  import { lookupPassword, savePassword, normalizeOrigin } from "./password-store.js";
7
6
  import { fillPasswordInBrowser } from "./playwright-fill.js";
8
7
  export class ToolError extends Error {
@@ -41,84 +40,6 @@ const notifyTool = {
41
40
  throw new ToolError(result.error ?? "Push notification failed", 502);
42
41
  },
43
42
  };
44
- const requestInputTool = {
45
- name: "request-input",
46
- description: [
47
- "Request input from the user.",
48
- "The request blocks until the user responds.",
49
- 'Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.',
50
- "When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment — use this instead.",
51
- ],
52
- inputSchema: {
53
- type: "object",
54
- properties: {
55
- description: { type: "string", description: "Context or heading for the input request" },
56
- questions: {
57
- type: "array",
58
- items: { type: "string" },
59
- description: "Questions to present to the user",
60
- minItems: 1,
61
- },
62
- },
63
- required: ["questions"],
64
- },
65
- async handler(args, ctx) {
66
- const { description, questions } = args;
67
- if (!questions?.length)
68
- throw new ToolError("questions is required", 400);
69
- const pendingPromise = registerPending(ctx.taskId, "input", questions, {
70
- session_id: ctx.taskId,
71
- description,
72
- input_questions: questions,
73
- });
74
- await ctx.publishEvent("_input", {
75
- event_type: "input-request",
76
- host_id: ctx.config.hostId,
77
- session_id: ctx.taskId,
78
- description,
79
- input_questions: questions,
80
- });
81
- const response = await pendingPromise;
82
- const aborted = response.length === 1 && response[0] === "aborted";
83
- recordUserResponseToRun(ctx, aborted
84
- ? "Aborted"
85
- : questions.map((q, i) => `**${q}**\n\n${response[i] ?? ""}`).join("\n\n"));
86
- if (aborted) {
87
- await ctx.publishEvent("_input", {
88
- event_type: "input-resolved", host_id: ctx.config.hostId,
89
- session_id: ctx.taskId, status: "aborted",
90
- });
91
- await ctx.abortTask();
92
- return { aborted: true };
93
- }
94
- await ctx.publishEvent("_input", {
95
- event_type: "input-resolved", host_id: ctx.config.hostId,
96
- session_id: ctx.taskId, status: "provided",
97
- });
98
- return { values: response };
99
- },
100
- };
101
- /** Splice the user's answer into the run file so history shows it even if the agent never echoes it. */
102
- function recordUserResponseToRun(ctx, content) {
103
- const taskId = ctx.taskId;
104
- const taskDir = getTaskDir(ctx.config.projectRoot, taskId);
105
- const { entries } = readHistory(ctx.config.projectRoot, { task_id: taskId, limit: 1 });
106
- const runId = entries[0]?.run_id;
107
- if (!runId)
108
- return;
109
- try {
110
- spliceUserMessage(taskDir, runId, {
111
- role: "user",
112
- time: Date.now(),
113
- content,
114
- type: "input",
115
- });
116
- void ctx.publishEvent(taskId, { event_type: "result-updated", run_id: runId });
117
- }
118
- catch {
119
- // Run file missing or unwritable — best-effort, do not fail the tool call.
120
- }
121
- }
122
43
  export const requestConfirmationTool = {
123
44
  name: "request-confirmation",
124
45
  description: [
@@ -137,7 +58,7 @@ export const requestConfirmationTool = {
137
58
  const { description } = args;
138
59
  if (!description)
139
60
  throw new ToolError("description is required", 400);
140
- const pendingPromise = registerPending(ctx.taskId, "confirmation", undefined, {
61
+ const pendingPromise = registerPending(ctx.taskId, "confirmation", {
141
62
  session_id: ctx.taskId,
142
63
  description,
143
64
  });
@@ -160,66 +81,6 @@ export const requestConfirmationTool = {
160
81
  return { confirmed };
161
82
  },
162
83
  };
163
- const requestChoiceTool = {
164
- name: "request-choice",
165
- description: [
166
- "Ask the user a question with a fixed set of answers and let them pick one.",
167
- "The request blocks until the user picks an option or aborts the task.",
168
- 'Response: `{"choice": "<option>"}` with the selected option, or `{"aborted": true}` if the user aborts.',
169
- "Use this instead of request-input whenever the possible answers are known in advance.",
170
- "An abort option that ends the task is always shown, so to ask for confirmation before continuing, pass a single option naming the action (e.g. [\"Send email\"]). Add a decline option only if declining should let the task continue differently.",
171
- ],
172
- inputSchema: {
173
- type: "object",
174
- properties: {
175
- question: { type: "string", description: "The question to ask" },
176
- options: {
177
- type: "array",
178
- items: { type: "string" },
179
- description: 'Unique option labels, in display order (e.g. ["Reply now", "Reply later"])',
180
- minItems: 1,
181
- },
182
- },
183
- required: ["question", "options"],
184
- },
185
- async handler(args, ctx) {
186
- const { question, options } = args;
187
- if (!question)
188
- throw new ToolError("question is required", 400);
189
- if (!Array.isArray(options) || options.length < 1 || options.some((o) => typeof o !== "string" || !o.trim())) {
190
- throw new ToolError("options must contain at least 1 non-empty string", 400);
191
- }
192
- if (new Set(options).size !== options.length)
193
- throw new ToolError("options must be unique", 400);
194
- const pendingPromise = registerPending(ctx.taskId, "choice", options, {
195
- session_id: ctx.taskId,
196
- description: question,
197
- });
198
- await ctx.publishEvent("_choice", {
199
- event_type: "choice-request",
200
- host_id: ctx.config.hostId,
201
- session_id: ctx.taskId,
202
- description: question,
203
- options,
204
- });
205
- // Clients answer with the option index so no label can collide with the "aborted" sentinel.
206
- const response = await pendingPromise;
207
- const choice = /^\d+$/.test(response[0] ?? "") ? options[Number(response[0])] : undefined;
208
- const aborted = choice === undefined;
209
- recordUserResponseToRun(ctx, aborted ? "Aborted" : `**${question}**\n\n${choice}`);
210
- await ctx.publishEvent("_choice", {
211
- event_type: "choice-resolved",
212
- host_id: ctx.config.hostId,
213
- session_id: ctx.taskId,
214
- status: aborted ? "aborted" : "chosen",
215
- });
216
- if (aborted) {
217
- await ctx.abortTask();
218
- return { aborted: true };
219
- }
220
- return { choice };
221
- },
222
- };
223
84
  const fillPasswordTool = {
224
85
  name: "fill-password",
225
86
  description: [
@@ -247,36 +108,30 @@ const fillPasswordTool = {
247
108
  const origin = normalizeOrigin(url);
248
109
  let password = lookupPassword(origin, username);
249
110
  if (password === undefined) {
250
- const question = `Password or PIN for ${username} on ${origin}`;
251
111
  const description = `Enter the password or PIN to sign in as ${username} on ${origin}.`;
252
- const pendingPromise = registerPending(ctx.taskId, "input", [question], {
112
+ const pendingPromise = registerPending(ctx.taskId, "password", {
253
113
  session_id: ctx.taskId,
254
114
  description,
255
- input_questions: [question],
256
- secret: true,
257
115
  });
258
- await ctx.publishEvent("_input", {
259
- event_type: "input-request",
116
+ await ctx.publishEvent("_password", {
117
+ event_type: "password-request",
260
118
  host_id: ctx.config.hostId,
261
119
  session_id: ctx.taskId,
262
120
  description,
263
- input_questions: [question],
264
- secret: true,
265
121
  });
266
122
  const response = await pendingPromise;
267
123
  const aborted = response.length === 1 && response[0] === "aborted";
268
- await ctx.publishEvent("_input", {
269
- event_type: "input-resolved", host_id: ctx.config.hostId,
124
+ await ctx.publishEvent("_password", {
125
+ event_type: "password-resolved", host_id: ctx.config.hostId,
270
126
  session_id: ctx.taskId, status: aborted ? "aborted" : "provided",
271
127
  });
272
- // Never record the secret to the task run file (unlike request-input).
128
+ // Never record the secret to the task run file.
273
129
  if (aborted) {
274
130
  await ctx.abortTask();
275
131
  return { aborted: true };
276
132
  }
277
133
  password = response[0];
278
- // The PWA appends a "save"/"nosave" flag for password prompts; persist
279
- // unless the user opted out via the dialog's checkbox.
134
+ // The PWA appends a "save"/"nosave" flag; persist unless the user opted out.
280
135
  if (response[1] !== "nosave")
281
136
  savePassword(origin, username, password);
282
137
  }
@@ -779,7 +634,7 @@ const sendEmailTool = {
779
634
  return result;
780
635
  },
781
636
  };
782
- export const agentTools = [notifyTool, requestInputTool, requestChoiceTool, fillPasswordTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlarmTool, readBatteryTool, setRingerModeTool];
637
+ export const agentTools = [notifyTool, fillPasswordTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlarmTool, readBatteryTool, setRingerModeTool];
783
638
  export const agentToolMap = new Map(agentTools.map((t) => [t.name, t]));
784
639
  const deviceNotificationsResource = {
785
640
  name: "Device Notifications",
@@ -13,22 +13,29 @@ The user reads your output on a different device and can only open files you lin
13
13
 
14
14
  ## Completion
15
15
 
16
- When you are done, output exactly one of these markers as the very last line on stdout (no other text on the same line):
16
+ When you stop, output exactly one of these markers as the very last line on stdout (no other text on the same line):
17
17
  [PALMIER_TASK_SUCCESS]
18
18
  [PALMIER_TASK_FAILURE]
19
+ [PALMIER_TASK_NEED_INPUT]
19
20
 
20
21
  On the line right before that marker, in the same final message, print the model you are running as, using the exact model name or ID from your system prompt (skip this line if you do not know it rather than guessing):
21
22
  [PALMIER_AGENT_MODEL] <model>
22
23
 
23
24
  Do this at the end of every response, including responses to follow-up messages.
24
25
 
26
+ ## Asking the User
27
+
28
+ When you need something from the user (answers to questions, preferences, confirmation before acting, clarifications, etc.), do not guess or fail. Write your questions in full as your final message, then end it with `[PALMIER_TASK_NEED_INPUT]`. Your process will exit, and the user's reply will arrive as the next message in this same session. Anything you left running, such as a browser, stays open, so leave it as it is.
29
+
30
+ If the possible answers are known in advance, list them in your question. For passwords and PINs, use `fill-password` instead.
31
+
25
32
  ## Browsers
26
33
 
27
34
  Whenever the task needs a browser, launch it with the Playwright CLI (`playwright-cli open`). Do not use any other browser tool, automation library, or launch method.
28
35
 
29
36
  When launching, always pass `--headed` (never run headless) and `--persistent` (reuse a persistent profile so logins and cookies survive across runs).
30
37
 
31
- When the task is finished, close any browser you opened with the Playwright CLI `close` command (pass the same `-s=<session>` if you used one) before printing the completion marker.
38
+ When the task has succeeded or failed, close any browser you opened with the Playwright CLI `close` command (pass the same `-s=<session>` if you used one) before printing the completion marker. Do not close it when you are stopping to ask the user for input.
32
39
 
33
40
  ## HTTP Endpoints
34
41
 
@@ -10,10 +10,8 @@ import { kimiAgent } from "./kimi.js";
10
10
  import { gooseAgent } from "./goose.js";
11
11
  import { openCodeAgent } from "./opencode.js";
12
12
  import { deepAgentsAgent } from "./deepagents.js";
13
- import { aiderAgent } from "./aider.js";
14
13
  import { cursorAgent } from "./cursor.js";
15
14
  import { kiroAgent } from "./kiro.js";
16
- import { clineAgent } from "./cline.js";
17
15
  import { qoderAgent } from "./qoder.js";
18
16
  import { hermesAgent } from "./hermes.js";
19
17
  import { grokAgent } from "./grok.js";
@@ -66,10 +64,8 @@ const agentRegistry = {
66
64
  goose: gooseAgent,
67
65
  opencode: openCodeAgent,
68
66
  deepagents: deepAgentsAgent,
69
- aider: aiderAgent,
70
67
  cursor: cursorAgent,
71
68
  kiro: kiroAgent,
72
- cline: clineAgent,
73
69
  openclaw: openClawAgent,
74
70
  hermes: hermesAgent,
75
71
  };
@@ -2,4 +2,7 @@ import type { ParsedTask } from "../types.js";
2
2
  export declare function getAgentInstructions(task: ParsedTask): string;
3
3
  export declare const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
4
4
  export declare const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
5
+ export declare const TASK_NEED_INPUT_MARKER = "[PALMIER_TASK_NEED_INPUT]";
5
6
  export declare const AGENT_MODEL_PREFIX = "[PALMIER_AGENT_MODEL]";
7
+ /** A resumed session may have lost the original instructions to compaction, and a missing marker silently ends the run. */
8
+ export declare function getResumePrompt(userMessage: string): string;
@@ -14,4 +14,9 @@ export function getAgentInstructions(task) {
14
14
  }
15
15
  export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
16
16
  export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
17
+ export const TASK_NEED_INPUT_MARKER = "[PALMIER_TASK_NEED_INPUT]";
17
18
  export const AGENT_MODEL_PREFIX = "[PALMIER_AGENT_MODEL]";
19
+ /** A resumed session may have lost the original instructions to compaction, and a missing marker silently ends the run. */
20
+ export function getResumePrompt(userMessage) {
21
+ return `${userMessage}\n\n---\nEnd your response with exactly one of these as the very last line on stdout: ${TASK_SUCCESS_MARKER}, ${TASK_FAILURE_MARKER}, or ${TASK_NEED_INPUT_MARKER} if you need more from the user.`;
22
+ }
@@ -7,5 +7,5 @@ export declare function listLinkedTaskFiles(taskDir: string, runId: string, outp
7
7
  export declare function parseAgentModel(output: string): string | undefined;
8
8
  /** Returns a function that accumulates stderr chunks and yields the model once the CLI banner reveals it. */
9
9
  export declare function createCliModelCapture(agent: AgentTool): (stderrChunk: string) => string | undefined;
10
- /** Falls back to "finished" if no success/failure marker is found. */
10
+ /** Falls back to "finished" if no outcome marker is found. */
11
11
  export declare function parseTaskOutcome(output: string): TaskRunningState;
@@ -2,17 +2,17 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { spawnCommand } from "../spawn-command.js";
4
4
  import { loadConfig } from "../config.js";
5
- import { parseTaskFile, taskDisplayName, getTaskDir, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, claimPendingRun, appendRunMessage, getRunDir, beginStreamingMessage, formatAgentModelComment } from "../task.js";
5
+ import { parseTaskFile, taskDisplayName, getTaskDir, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, claimPendingRun, appendRunMessage, getRunDir, beginStreamingMessage, formatAgentModelComment, takeUserReply } from "../task.js";
6
6
  import { getAgent } from "../agents/agent.js";
7
7
  import { getPlatform } from "../platform/index.js";
8
- import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, AGENT_MODEL_PREFIX } from "../agents/shared-prompt.js";
8
+ import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_NEED_INPUT_MARKER, AGENT_MODEL_PREFIX, getResumePrompt } from "../agents/shared-prompt.js";
9
9
  import { extractFileLinks, resolveTaskFile } from "../task-files.js";
10
10
  import { publishHostEvent } from "../events.js";
11
11
  /**
12
12
  * `invokeTask` is the ParsedTask whose prompt is passed to the agent (for
13
13
  * triggered tasks this is the per-trigger augmented task).
14
14
  */
15
- async function invokeAgent(ctx, invokeTask) {
15
+ async function invokeAgent(ctx, invokeTask, followupPrompt) {
16
16
  let writer;
17
17
  let activeStream;
18
18
  const lineBufs = { stdout: "", stderr: "" };
@@ -77,7 +77,7 @@ async function invokeAgent(ctx, invokeTask) {
77
77
  ensureWriter(stream).write(trailing);
78
78
  }
79
79
  }
80
- const { args, stdin, env: agentEnv, files } = ctx.agent.getTaskRunCommandLine(invokeTask);
80
+ const { args, stdin, env: agentEnv, files } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt);
81
81
  const command = ctx.agent.command;
82
82
  const runDir = getRunDir(ctx.taskDir, ctx.runId);
83
83
  if (files) {
@@ -130,6 +130,47 @@ function resolveOutcome(taskDir, outcome) {
130
130
  return "aborted";
131
131
  return outcome;
132
132
  }
133
+ const USER_REPLY_POLL_MS = 500;
134
+ // A recurring task can't fire again while this run is waiting, so an unanswered question can't hold it forever.
135
+ const USER_REPLY_TIMEOUT_MS = 24 * 60 * 60 * 1000;
136
+ async function waitForUserReply(runDir) {
137
+ const deadline = Date.now() + USER_REPLY_TIMEOUT_MS;
138
+ while (Date.now() < deadline) {
139
+ const reply = takeUserReply(runDir);
140
+ if (reply)
141
+ return reply;
142
+ await new Promise((resolve) => setTimeout(resolve, USER_REPLY_POLL_MS));
143
+ }
144
+ return undefined;
145
+ }
146
+ /**
147
+ * Only the agent CLI exits while the user answers. This process stays alive because
148
+ * the OS scheduler would otherwise clean up what the agent left running, like its browser.
149
+ */
150
+ async function invokeAgentUntilDone(ctx, invokeTask) {
151
+ const taskName = taskDisplayName(ctx.task.frontmatter);
152
+ const runDir = getRunDir(ctx.taskDir, ctx.runId);
153
+ let result = await invokeAgent(ctx, invokeTask);
154
+ while (resolveOutcome(ctx.taskDir, result.outcome) === "needs_input") {
155
+ appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "needs_input" });
156
+ await publishTaskEvent(ctx.taskDir, ctx.taskId, "needs_input", taskName, ctx.runId);
157
+ const reply = await waitForUserReply(runDir);
158
+ if (!reply) {
159
+ await appendAndNotify(ctx, {
160
+ role: "assistant",
161
+ time: Date.now(),
162
+ content: "The task stopped because no reply arrived within 24 hours.",
163
+ });
164
+ return { outcome: "failed" };
165
+ }
166
+ appendRunMessage(ctx.taskDir, ctx.runId, { role: "user", time: Date.now(), content: reply, type: "input" });
167
+ appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "started" });
168
+ await publishTaskEvent(ctx.taskDir, ctx.taskId, "started", taskName, ctx.runId);
169
+ await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
170
+ result = await invokeAgent(ctx, invokeTask, getResumePrompt(reply));
171
+ }
172
+ return result;
173
+ }
133
174
  export async function runCommand(taskId) {
134
175
  const config = loadConfig();
135
176
  const taskDir = getTaskDir(config.projectRoot, taskId);
@@ -196,7 +237,7 @@ export async function runCommand(taskId) {
196
237
  time: Date.now(),
197
238
  content: task.frontmatter.user_prompt,
198
239
  });
199
- const result = await invokeAgent(ctx, task);
240
+ const result = await invokeAgentUntilDone(ctx, task);
200
241
  const outcome = resolveOutcome(taskDir, result.outcome);
201
242
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
202
243
  await publishTaskEvent(taskDir, taskId, outcome, taskName, runId);
@@ -257,7 +298,7 @@ async function runEventTriggeredMode(ctx) {
257
298
  const perEventTask = {
258
299
  frontmatter: { ...ctx.task.frontmatter, user_prompt: perEventPrompt },
259
300
  };
260
- const result = await invokeAgent(ctx, perEventTask);
301
+ const result = await invokeAgentUntilDone(ctx, perEventTask);
261
302
  lastOutcome = result.outcome;
262
303
  }
263
304
  }
@@ -273,7 +314,8 @@ async function publishTaskEvent(taskDir, taskId, eventType, taskName, runId) {
273
314
  writeTaskStatus(taskDir, {
274
315
  running_state: eventType,
275
316
  time_stamp: Date.now(),
276
- ...(eventType === "started" ? { pid: process.pid } : {}),
317
+ ...(eventType === "started" || eventType === "needs_input" ? { pid: process.pid } : {}),
318
+ ...(eventType === "needs_input" ? { run_id: runId } : {}),
277
319
  });
278
320
  const payload = { event_type: "running-state", running_state: eventType };
279
321
  if (taskName)
@@ -344,10 +386,10 @@ function sanitizeAgentModel(raw) {
344
386
  return undefined;
345
387
  return model;
346
388
  }
347
- /** Falls back to "finished" if no success/failure marker is found. */
389
+ /** Falls back to "finished" if no outcome marker is found. */
348
390
  export function parseTaskOutcome(output) {
349
391
  const lastChunk = output.slice(-500);
350
- const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
392
+ const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$|^\\${TASK_NEED_INPUT_MARKER}$`, "gm");
351
393
  let last = null;
352
394
  let match;
353
395
  while ((match = regex.exec(lastChunk)) !== null) {
@@ -355,6 +397,8 @@ export function parseTaskOutcome(output) {
355
397
  }
356
398
  if (last === TASK_FAILURE_MARKER)
357
399
  return "failed";
400
+ if (last === TASK_NEED_INPUT_MARKER)
401
+ return "needs_input";
358
402
  if (last === TASK_SUCCESS_MARKER)
359
403
  return "finished";
360
404
  return "finished";
@@ -42,7 +42,9 @@ async function checkStaleTasks(config) {
42
42
  catch {
43
43
  continue;
44
44
  }
45
- if (status.running_state === "started" && !platform.isTaskRunning(taskId)) {
45
+ // Follow-up turns also end in needs_input, but no process is held open for them.
46
+ const isWaitingRun = status.running_state === "needs_input" && status.pid !== undefined;
47
+ if ((status.running_state === "started" || isWaitingRun) && !platform.isTaskRunning(taskId)) {
46
48
  console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
47
49
  const endTime = Date.now();
48
50
  writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
@@ -64,7 +66,7 @@ async function checkStaleTasks(config) {
64
66
  name: taskDisplayName(task.frontmatter) || taskId,
65
67
  });
66
68
  }
67
- if (task.frontmatter.one_off && status.running_state !== "started") {
69
+ if (task.frontmatter.one_off && status.running_state !== "started" && !isWaitingRun) {
68
70
  try {
69
71
  platform.removeTaskTimer(taskId);
70
72
  }
@@ -3,15 +3,10 @@ export interface PendingRequestMeta {
3
3
  session_id?: string;
4
4
  session_name?: string;
5
5
  description?: string;
6
- input_questions?: string[];
7
- /** Render the input field(s) masked (password entry). */
8
- secret?: boolean;
9
6
  }
10
7
  export interface PendingRequest {
11
- type: "confirmation" | "input" | "choice";
8
+ type: "confirmation" | "password";
12
9
  resolve: (value: string[]) => void;
13
- /** Input descriptions (for 'input') or option labels (for 'choice'). */
14
- params?: string[];
15
10
  /** Display context for PWAs that connect while this request is already open. */
16
11
  meta?: PendingRequestMeta;
17
12
  }
@@ -19,7 +14,7 @@ export interface PendingRequest {
19
14
  * Keyed by taskId. Only one pending request per key at a time. `meta` is surfaced
20
15
  * via host.info so a freshly-connected PWA can render the modal without replaying events.
21
16
  */
22
- export declare function registerPending(key: string, type: PendingRequest["type"], params?: PendingRequest["params"], meta?: PendingRequestMeta): Promise<string[]>;
17
+ export declare function registerPending(key: string, type: PendingRequest["type"], meta?: PendingRequestMeta): Promise<string[]>;
23
18
  export declare function resolvePending(key: string, value: string[]): boolean;
24
19
  export declare function getPending(key: string): PendingRequest | undefined;
25
20
  export declare function removePending(key: string): void;
@@ -27,6 +22,5 @@ export declare function removePending(key: string): void;
27
22
  export declare function listPending(): Array<{
28
23
  key: string;
29
24
  type: PendingRequest["type"];
30
- params?: PendingRequest["params"];
31
25
  meta?: PendingRequestMeta;
32
26
  }>;
@@ -3,12 +3,12 @@ const pending = new Map();
3
3
  * Keyed by taskId. Only one pending request per key at a time. `meta` is surfaced
4
4
  * via host.info so a freshly-connected PWA can render the modal without replaying events.
5
5
  */
6
- export function registerPending(key, type, params, meta) {
6
+ export function registerPending(key, type, meta) {
7
7
  if (pending.has(key)) {
8
8
  return Promise.reject(new Error(`Key ${key} already has a pending request`));
9
9
  }
10
10
  return new Promise((resolve) => {
11
- pending.set(key, { type, resolve, params, meta });
11
+ pending.set(key, { type, resolve, meta });
12
12
  });
13
13
  }
14
14
  export function resolvePending(key, value) {
@@ -30,7 +30,6 @@ export function listPending() {
30
30
  return [...pending.entries()].map(([key, entry]) => ({
31
31
  key,
32
32
  type: entry.type,
33
- params: entry.params,
34
33
  meta: entry.meta,
35
34
  }));
36
35
  }