palmier 1.1.24 → 1.1.25
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 +6 -1
- package/dist/agent-tools.js +3 -60
- package/dist/agents/agent-instructions.md +9 -2
- package/dist/agents/agent.js +0 -4
- package/dist/agents/shared-prompt.d.ts +3 -0
- package/dist/agents/shared-prompt.js +5 -0
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.js +53 -9
- package/dist/commands/serve.js +4 -2
- package/dist/pwa/assets/index-D64fpB5p.js +117 -0
- package/dist/pwa/assets/{web-durUDXGk.js → web-Flj1nZsf.js} +1 -1
- package/dist/pwa/assets/{web-BIh8c8oo.js → web-YFetWCPM.js} +1 -1
- package/dist/pwa/assets/{web-DTphq1C9.js → web-YYfGpkCq.js} +1 -1
- package/dist/pwa/index.html +1 -1
- package/dist/rpc-handler.js +12 -2
- package/dist/task.d.ts +3 -0
- package/dist/task.js +29 -0
- package/dist/types.d.ts +5 -2
- package/package.json +1 -1
- package/dist/agents/aider.d.ts +0 -2
- package/dist/agents/aider.js +0 -13
- package/dist/agents/cline.d.ts +0 -2
- package/dist/agents/cline.js +0 -14
- package/dist/pwa/assets/index-CXiDfnsf.js +0 -117
package/README.md
CHANGED
|
@@ -53,7 +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
56
|
| `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
57
|
| `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
58
|
| `device-geolocation` | Get GPS location of the user's mobile device |
|
|
@@ -75,6 +74,12 @@ Every task Palmier runs gets these capabilities automatically. Their usage is wr
|
|
|
75
74
|
|
|
76
75
|
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
76
|
|
|
77
|
+
### Asking the User
|
|
78
|
+
|
|
79
|
+
When an agent needs an answer it can't get from a fixed list, 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.
|
|
80
|
+
|
|
81
|
+
`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.
|
|
82
|
+
|
|
78
83
|
### Files in Agent Output
|
|
79
84
|
|
|
80
85
|
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 ``. 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.
|
package/dist/agent-tools.js
CHANGED
|
@@ -41,63 +41,6 @@ const notifyTool = {
|
|
|
41
41
|
throw new ToolError(result.error ?? "Push notification failed", 502);
|
|
42
42
|
},
|
|
43
43
|
};
|
|
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
44
|
/** Splice the user's answer into the run file so history shows it even if the agent never echoes it. */
|
|
102
45
|
function recordUserResponseToRun(ctx, content) {
|
|
103
46
|
const taskId = ctx.taskId;
|
|
@@ -166,7 +109,7 @@ const requestChoiceTool = {
|
|
|
166
109
|
"Ask the user a question with a fixed set of answers and let them pick one.",
|
|
167
110
|
"The request blocks until the user picks an option or aborts the task.",
|
|
168
111
|
'Response: `{"choice": "<option>"}` with the selected option, or `{"aborted": true}` if the user aborts.',
|
|
169
|
-
"Use this
|
|
112
|
+
"Use this whenever the possible answers are known in advance.",
|
|
170
113
|
"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
114
|
],
|
|
172
115
|
inputSchema: {
|
|
@@ -269,7 +212,7 @@ const fillPasswordTool = {
|
|
|
269
212
|
event_type: "input-resolved", host_id: ctx.config.hostId,
|
|
270
213
|
session_id: ctx.taskId, status: aborted ? "aborted" : "provided",
|
|
271
214
|
});
|
|
272
|
-
// Never record the secret to the task run file
|
|
215
|
+
// Never record the secret to the task run file.
|
|
273
216
|
if (aborted) {
|
|
274
217
|
await ctx.abortTask();
|
|
275
218
|
return { aborted: true };
|
|
@@ -779,7 +722,7 @@ const sendEmailTool = {
|
|
|
779
722
|
return result;
|
|
780
723
|
},
|
|
781
724
|
};
|
|
782
|
-
export const agentTools = [notifyTool,
|
|
725
|
+
export const agentTools = [notifyTool, requestChoiceTool, fillPasswordTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlarmTool, readBatteryTool, setRingerModeTool];
|
|
783
726
|
export const agentToolMap = new Map(agentTools.map((t) => [t.name, t]));
|
|
784
727
|
const deviceNotificationsResource = {
|
|
785
728
|
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
|
|
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 information from the user (answers to questions, preferences, 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, use the `request-choice` endpoint instead. For passwords and PINs, use `fill-password`.
|
|
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
|
|
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
|
|
package/dist/agents/agent.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -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
|
|
10
|
+
/** Falls back to "finished" if no outcome marker is found. */
|
|
11
11
|
export declare function parseTaskOutcome(output: string): TaskRunningState;
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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";
|
package/dist/commands/serve.js
CHANGED
|
@@ -42,7 +42,9 @@ async function checkStaleTasks(config) {
|
|
|
42
42
|
catch {
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
|
-
|
|
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
|
}
|