pi-subagents 0.74.0 → 0.76.0
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/CHANGELOG.md +83 -0
- package/docs/agents.md +15 -5
- package/docs/configuration.md +14 -2
- package/docs/extension-api.md +2 -2
- package/docs/missions.md +27 -3
- package/docs/models.md +6 -2
- package/docs/observability.md +8 -3
- package/docs/tool-reference.md +48 -8
- package/docs/workflows.md +3 -3
- package/package.json +2 -1
- package/src/agents/agent-memory.js +3 -3
- package/src/agents/agent-memory.js.map +1 -1
- package/src/agents/agent-serializer.d.ts.map +1 -1
- package/src/agents/agent-serializer.js +8 -1
- package/src/agents/agent-serializer.js.map +1 -1
- package/src/agents/agents.d.ts +1 -0
- package/src/agents/agents.d.ts.map +1 -1
- package/src/agents/agents.js +137 -95
- package/src/agents/agents.js.map +1 -1
- package/src/api/preflight.d.ts +1 -1
- package/src/api/preflight.d.ts.map +1 -1
- package/src/api/preflight.js +44 -1
- package/src/api/preflight.js.map +1 -1
- package/src/extension/config.d.ts.map +1 -1
- package/src/extension/config.js +4 -1
- package/src/extension/config.js.map +1 -1
- package/src/extension/index.d.ts.map +1 -1
- package/src/extension/index.js +62 -22
- package/src/extension/index.js.map +1 -1
- package/src/extension/public-execution.d.ts.map +1 -1
- package/src/extension/public-execution.js +4 -0
- package/src/extension/public-execution.js.map +1 -1
- package/src/extension/reply-workflow-script.d.ts.map +1 -1
- package/src/extension/reply-workflow-script.js +21 -12
- package/src/extension/reply-workflow-script.js.map +1 -1
- package/src/extension/rpc.js +1 -1
- package/src/extension/rpc.js.map +1 -1
- package/src/extension/schemas.d.ts +2 -1
- package/src/extension/schemas.d.ts.map +1 -1
- package/src/extension/schemas.js +13 -11
- package/src/extension/schemas.js.map +1 -1
- package/src/extension/tool-description.js +2 -2
- package/src/extension/tool-description.js.map +1 -1
- package/src/intercom/result-intercom.d.ts.map +1 -1
- package/src/intercom/result-intercom.js +2 -1
- package/src/intercom/result-intercom.js.map +1 -1
- package/src/missions/goal-driver.d.ts +1 -0
- package/src/missions/goal-driver.d.ts.map +1 -1
- package/src/missions/goal-driver.js +49 -34
- package/src/missions/goal-driver.js.map +1 -1
- package/src/missions/store.d.ts.map +1 -1
- package/src/missions/store.js +11 -1
- package/src/missions/store.js.map +1 -1
- package/src/profiles/profiles.d.ts +1 -1
- package/src/profiles/profiles.d.ts.map +1 -1
- package/src/profiles/profiles.js +29 -22
- package/src/profiles/profiles.js.map +1 -1
- package/src/runs/background/active-async-capacity.d.ts +15 -1
- package/src/runs/background/active-async-capacity.d.ts.map +1 -1
- package/src/runs/background/active-async-capacity.js +51 -6
- package/src/runs/background/active-async-capacity.js.map +1 -1
- package/src/runs/background/async-execution.d.ts +13 -0
- package/src/runs/background/async-execution.d.ts.map +1 -1
- package/src/runs/background/async-execution.js +89 -6
- package/src/runs/background/async-execution.js.map +1 -1
- package/src/runs/background/async-job-tracker.d.ts +4 -2
- package/src/runs/background/async-job-tracker.d.ts.map +1 -1
- package/src/runs/background/async-job-tracker.js +4 -3
- package/src/runs/background/async-job-tracker.js.map +1 -1
- package/src/runs/background/await-async-run.d.ts.map +1 -1
- package/src/runs/background/await-async-run.js +4 -2
- package/src/runs/background/await-async-run.js.map +1 -1
- package/src/runs/background/calendar-schedule.d.ts +27 -0
- package/src/runs/background/calendar-schedule.d.ts.map +1 -0
- package/src/runs/background/calendar-schedule.js +94 -0
- package/src/runs/background/calendar-schedule.js.map +1 -0
- package/src/runs/background/control-channel.d.ts +10 -0
- package/src/runs/background/control-channel.d.ts.map +1 -1
- package/src/runs/background/control-channel.js +51 -0
- package/src/runs/background/control-channel.js.map +1 -1
- package/src/runs/background/notify.d.ts +6 -0
- package/src/runs/background/notify.d.ts.map +1 -1
- package/src/runs/background/notify.js +24 -4
- package/src/runs/background/notify.js.map +1 -1
- package/src/runs/background/result-watcher.d.ts +5 -1
- package/src/runs/background/result-watcher.d.ts.map +1 -1
- package/src/runs/background/result-watcher.js +44 -7
- package/src/runs/background/result-watcher.js.map +1 -1
- package/src/runs/background/run-child-session.d.ts +2 -0
- package/src/runs/background/run-child-session.d.ts.map +1 -1
- package/src/runs/background/run-child-session.js +32 -7
- package/src/runs/background/run-child-session.js.map +1 -1
- package/src/runs/background/runner-aliases.d.ts +1 -0
- package/src/runs/background/runner-aliases.d.ts.map +1 -1
- package/src/runs/background/runner-aliases.js +6 -2
- package/src/runs/background/runner-aliases.js.map +1 -1
- package/src/runs/background/scheduled-runs.d.ts +5 -2
- package/src/runs/background/scheduled-runs.d.ts.map +1 -1
- package/src/runs/background/scheduled-runs.js +210 -56
- package/src/runs/background/scheduled-runs.js.map +1 -1
- package/src/runs/background/stale-run-reconciler.d.ts +1 -1
- package/src/runs/background/stale-run-reconciler.d.ts.map +1 -1
- package/src/runs/background/stale-run-reconciler.js +15 -2
- package/src/runs/background/stale-run-reconciler.js.map +1 -1
- package/src/runs/background/subagent-runner.d.ts +1 -0
- package/src/runs/background/subagent-runner.d.ts.map +1 -1
- package/src/runs/background/subagent-runner.js +58 -9
- package/src/runs/background/subagent-runner.js.map +1 -1
- package/src/runs/background/wait-completions.d.ts.map +1 -1
- package/src/runs/background/wait-completions.js +1 -0
- package/src/runs/background/wait-completions.js.map +1 -1
- package/src/runs/background/workflow-terminal-proof.d.ts +14 -1
- package/src/runs/background/workflow-terminal-proof.d.ts.map +1 -1
- package/src/runs/background/workflow-terminal-proof.js +41 -28
- package/src/runs/background/workflow-terminal-proof.js.map +1 -1
- package/src/runs/foreground/command-action.d.ts +13 -0
- package/src/runs/foreground/command-action.d.ts.map +1 -0
- package/src/runs/foreground/command-action.js +38 -0
- package/src/runs/foreground/command-action.js.map +1 -0
- package/src/runs/foreground/execution.d.ts.map +1 -1
- package/src/runs/foreground/execution.js +42 -8
- package/src/runs/foreground/execution.js.map +1 -1
- package/src/runs/foreground/foreground-history.d.ts.map +1 -1
- package/src/runs/foreground/foreground-history.js +1 -0
- package/src/runs/foreground/foreground-history.js.map +1 -1
- package/src/runs/foreground/subagent-executor.d.ts +2 -1
- package/src/runs/foreground/subagent-executor.d.ts.map +1 -1
- package/src/runs/foreground/subagent-executor.js +108 -17
- package/src/runs/foreground/subagent-executor.js.map +1 -1
- package/src/runs/shared/child-commands.d.ts +28 -0
- package/src/runs/shared/child-commands.d.ts.map +1 -0
- package/src/runs/shared/child-commands.js +250 -0
- package/src/runs/shared/child-commands.js.map +1 -0
- package/src/runs/shared/child-session.d.ts +4 -0
- package/src/runs/shared/child-session.d.ts.map +1 -1
- package/src/runs/shared/child-session.js +107 -22
- package/src/runs/shared/child-session.js.map +1 -1
- package/src/runs/shared/child-tool-plan.js +1 -1
- package/src/runs/shared/child-tool-plan.js.map +1 -1
- package/src/runs/shared/claude-code-adapter.d.ts +56 -0
- package/src/runs/shared/claude-code-adapter.d.ts.map +1 -1
- package/src/runs/shared/claude-code-adapter.js +138 -2
- package/src/runs/shared/claude-code-adapter.js.map +1 -1
- package/src/runs/shared/dynamic-fanout.js +1 -1
- package/src/runs/shared/dynamic-fanout.js.map +1 -1
- package/src/runs/shared/external-cli-preflight.d.ts +10 -0
- package/src/runs/shared/external-cli-preflight.d.ts.map +1 -1
- package/src/runs/shared/external-cli-preflight.js +50 -6
- package/src/runs/shared/external-cli-preflight.js.map +1 -1
- package/src/runs/shared/external-cli-runner.d.ts.map +1 -1
- package/src/runs/shared/external-cli-runner.js +4 -2
- package/src/runs/shared/external-cli-runner.js.map +1 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.d.ts.map +1 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.js +8 -5
- package/src/runs/shared/mcp-direct-tool-allowlist.js.map +1 -1
- package/src/runs/shared/model-resolution.d.ts +6 -2
- package/src/runs/shared/model-resolution.d.ts.map +1 -1
- package/src/runs/shared/model-resolution.js +9 -2
- package/src/runs/shared/model-resolution.js.map +1 -1
- package/src/runs/shared/parallel-handoff.d.ts +10 -4
- package/src/runs/shared/parallel-handoff.d.ts.map +1 -1
- package/src/runs/shared/parallel-handoff.js +53 -6
- package/src/runs/shared/parallel-handoff.js.map +1 -1
- package/src/runs/shared/parallel-utils.d.ts +2 -0
- package/src/runs/shared/parallel-utils.d.ts.map +1 -1
- package/src/runs/shared/parallel-utils.js.map +1 -1
- package/src/runs/shared/partial-output.d.ts +18 -0
- package/src/runs/shared/partial-output.d.ts.map +1 -0
- package/src/runs/shared/partial-output.js +29 -0
- package/src/runs/shared/partial-output.js.map +1 -0
- package/src/runs/shared/run-history.d.ts +47 -0
- package/src/runs/shared/run-history.d.ts.map +1 -1
- package/src/runs/shared/run-history.js +70 -0
- package/src/runs/shared/run-history.js.map +1 -1
- package/src/runs/shared/subagent-control.d.ts.map +1 -1
- package/src/runs/shared/subagent-control.js +30 -17
- package/src/runs/shared/subagent-control.js.map +1 -1
- package/src/runs/shared/subagent-prompt-runtime.d.ts +1 -0
- package/src/runs/shared/subagent-prompt-runtime.d.ts.map +1 -1
- package/src/runs/shared/subagent-prompt-runtime.js +12 -5
- package/src/runs/shared/subagent-prompt-runtime.js.map +1 -1
- package/src/runs/shared/worktree-cleanup-apply.d.ts +37 -0
- package/src/runs/shared/worktree-cleanup-apply.d.ts.map +1 -0
- package/src/runs/shared/worktree-cleanup-apply.js +179 -0
- package/src/runs/shared/worktree-cleanup-apply.js.map +1 -0
- package/src/runs/shared/worktree-cleanup-plan.d.ts +7 -0
- package/src/runs/shared/worktree-cleanup-plan.d.ts.map +1 -1
- package/src/runs/shared/worktree-cleanup-plan.js +41 -19
- package/src/runs/shared/worktree-cleanup-plan.js.map +1 -1
- package/src/runs/shared/worktree-lock.d.ts +6 -0
- package/src/runs/shared/worktree-lock.d.ts.map +1 -0
- package/src/runs/shared/worktree-lock.js +41 -0
- package/src/runs/shared/worktree-lock.js.map +1 -0
- package/src/runs/shared/worktree.d.ts +3 -0
- package/src/runs/shared/worktree.d.ts.map +1 -1
- package/src/runs/shared/worktree.js +9 -1
- package/src/runs/shared/worktree.js.map +1 -1
- package/src/shared/file-lease.d.ts +5 -0
- package/src/shared/file-lease.d.ts.map +1 -0
- package/src/shared/file-lease.js +101 -0
- package/src/shared/file-lease.js.map +1 -0
- package/src/shared/required-child-extensions.d.ts +16 -1
- package/src/shared/required-child-extensions.d.ts.map +1 -1
- package/src/shared/required-child-extensions.js +37 -5
- package/src/shared/required-child-extensions.js.map +1 -1
- package/src/shared/settings-file-lease.d.ts +5 -0
- package/src/shared/settings-file-lease.d.ts.map +1 -0
- package/src/shared/settings-file-lease.js +39 -0
- package/src/shared/settings-file-lease.js.map +1 -0
- package/src/shared/types.d.ts +16 -1
- package/src/shared/types.d.ts.map +1 -1
- package/src/shared/types.js +1 -1
- package/src/shared/types.js.map +1 -1
- package/src/slash/subagent-cost.d.ts.map +1 -1
- package/src/slash/subagent-cost.js +8 -4
- package/src/slash/subagent-cost.js.map +1 -1
- package/src/tui/render.d.ts +1 -1
- package/src/tui/render.d.ts.map +1 -1
- package/src/tui/render.js +11 -6
- package/src/tui/render.js.map +1 -1
- package/src/watchdog/diff-tool.js +2 -2
- package/src/watchdog/diff-tool.js.map +1 -1
- package/src/watchdog/register-main.js +1 -2
- package/src/watchdog/register-main.js.map +1 -1
- package/src/watchdog/runtime.d.ts +3 -3
- package/src/watchdog/runtime.d.ts.map +1 -1
- package/src/watchdog/runtime.js +7 -3
- package/src/watchdog/runtime.js.map +1 -1
- package/src/watchdog/settings.d.ts.map +1 -1
- package/src/watchdog/settings.js +22 -17
- package/src/watchdog/settings.js.map +1 -1
- package/src/workflows/workflow-receipt.js +2 -2
- package/src/workflows/workflow-receipt.js.map +1 -1
- package/src/workflows/workflow-resources.d.ts +4 -0
- package/src/workflows/workflow-resources.d.ts.map +1 -1
- package/src/workflows/workflow-resources.js +7 -4
- package/src/workflows/workflow-resources.js.map +1 -1
|
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { getProjectSubagentsDir } from "../../shared/artifacts.js";
|
|
6
6
|
import { writePrivateAtomicJson } from "../../shared/atomic-json.js";
|
|
7
|
+
import { withFileLease } from "../../shared/file-lease.js";
|
|
7
8
|
import { shortenPath } from "../../shared/formatters.js";
|
|
8
9
|
import { validateExecutionAcceptance } from "../shared/acceptance.js";
|
|
9
10
|
import { previewSimpleWorkflowRun } from "../../workflows/scripted-workflow.js";
|
|
@@ -11,6 +12,8 @@ import { resolveGitRepositoryIdentity } from "../../workflows/chat-progress.js";
|
|
|
11
12
|
import { getConfigDirName } from "../../shared/utils.js";
|
|
12
13
|
import { normalizeWorktreeBaseRef } from "../shared/worktree.js";
|
|
13
14
|
import { deepFreezeWorkflowArgs, normalizeWorkflowArgs } from "../../workflows/workflow-resources.js";
|
|
15
|
+
import { readMission, resolveMissionStoreLocation, validateMissionId } from "../../missions/store.js";
|
|
16
|
+
import { calendarDateAfter, latestCalendarOccurrence, nextCalendarOccurrence, normalizeCalendarRule, restoreCalendarTrigger } from "./calendar-schedule.js";
|
|
14
17
|
export const SCHEDULED_RUN_ACTIONS = [
|
|
15
18
|
"schedule.create",
|
|
16
19
|
"schedule.list",
|
|
@@ -252,16 +255,23 @@ function parseScheduleTarget(value, file) {
|
|
|
252
255
|
const target = value;
|
|
253
256
|
if (typeof target.workflowScript === "string" && target.workflowScript.trim()) {
|
|
254
257
|
let baseRef;
|
|
258
|
+
let missionId;
|
|
255
259
|
try {
|
|
256
260
|
baseRef = normalizeWorktreeBaseRef(target.baseRef);
|
|
257
261
|
}
|
|
258
262
|
catch (error) {
|
|
259
263
|
throw new Error(`Schedule record '${file}' has an invalid baseRef: ${error instanceof Error ? error.message : String(error)}`);
|
|
260
264
|
}
|
|
265
|
+
try {
|
|
266
|
+
missionId = target.missionId === undefined ? undefined : validateMissionId(target.missionId);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
throw new Error(`Schedule record '${file}' has an invalid missionId: ${error instanceof Error ? error.message : String(error)}`);
|
|
270
|
+
}
|
|
261
271
|
const normalizedArgs = normalizeWorkflowArgs(target.args);
|
|
262
272
|
if ("error" in normalizedArgs)
|
|
263
273
|
throw new Error(`Schedule record '${file}' has invalid args: ${normalizedArgs.error}`);
|
|
264
|
-
return { workflowScript: target.workflowScript.trim(), args: deepFreezeWorkflowArgs(normalizedArgs.args), ...(baseRef === undefined ? {} : { baseRef }) };
|
|
274
|
+
return { workflowScript: target.workflowScript.trim(), args: deepFreezeWorkflowArgs(normalizedArgs.args), ...(baseRef === undefined ? {} : { baseRef }), ...(missionId === undefined ? {} : { missionId }) };
|
|
265
275
|
}
|
|
266
276
|
if (target.agent !== undefined || target.task !== undefined)
|
|
267
277
|
throw new Error(`Schedule record '${file}' uses a removed legacy agent target; recreate it with schedule.create and workflow: true or a workflow script path.`);
|
|
@@ -271,7 +281,7 @@ function parseSchedule(value, file) {
|
|
|
271
281
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
272
282
|
throw new Error(`Schedule record '${file}' must be a JSON object.`);
|
|
273
283
|
const record = value;
|
|
274
|
-
if (record.schemaVersion !== 1 || typeof record.id !== "string" || typeof record.name !== "string" || typeof record.cwd !== "string" || typeof record.createdAt !== "string" || typeof record.updatedAt !== "string" || typeof record.paused !== "boolean")
|
|
284
|
+
if ((record.schemaVersion !== 1 && record.schemaVersion !== 2) || typeof record.id !== "string" || typeof record.name !== "string" || typeof record.cwd !== "string" || typeof record.createdAt !== "string" || typeof record.updatedAt !== "string" || typeof record.paused !== "boolean")
|
|
275
285
|
throw new Error(`Schedule record '${file}' has invalid required fields.`);
|
|
276
286
|
validateScheduleId(record.id);
|
|
277
287
|
if (!record.trigger || typeof record.trigger !== "object" || !record.target || typeof record.target !== "object")
|
|
@@ -286,6 +296,9 @@ function parseSchedule(value, file) {
|
|
|
286
296
|
if (typeof record.trigger.every !== "string" || typeof record.trigger.everyMs !== "number" || typeof record.trigger.anchorAt !== "string" || typeof record.trigger.nextRunAt !== "string")
|
|
287
297
|
throw new Error(`Schedule record '${file}' has an invalid interval trigger.`);
|
|
288
298
|
}
|
|
299
|
+
else if (record.trigger.kind === "calendar") {
|
|
300
|
+
restoreCalendarTrigger(record.trigger); // Validate without rewriting during read-only list/show.
|
|
301
|
+
}
|
|
289
302
|
else
|
|
290
303
|
throw new Error(`Schedule record '${file}' has an unsupported trigger.`);
|
|
291
304
|
if (record.sessionOnly !== undefined && typeof record.sessionOnly !== "boolean")
|
|
@@ -294,7 +307,10 @@ function parseSchedule(value, file) {
|
|
|
294
307
|
throw new Error(`Schedule record '${file}' has invalid quiet.`);
|
|
295
308
|
if (record.sessionOnly === true && (typeof record.ownerSessionFile !== "string" || !record.ownerSessionFile.trim()))
|
|
296
309
|
throw new Error(`Schedule record '${file}' is session-only but has no owner session file.`);
|
|
297
|
-
|
|
310
|
+
const target = parseScheduleTarget(record.target, file);
|
|
311
|
+
if ((record.schemaVersion === 2) !== (target.missionId !== undefined))
|
|
312
|
+
throw new Error(`Schedule record '${file}' requires schemaVersion 2 exactly when missionId is present.`);
|
|
313
|
+
return { ...record, target };
|
|
298
314
|
}
|
|
299
315
|
class ScheduleStore {
|
|
300
316
|
root;
|
|
@@ -345,13 +361,30 @@ class ScheduleStore {
|
|
|
345
361
|
throw new Error(`Schedule history '${file}' has invalid fields.`);
|
|
346
362
|
return value.runs;
|
|
347
363
|
}
|
|
364
|
+
/** The run's own receipt; it is written before history.json and is never trimmed. */
|
|
365
|
+
getRun(id, runId) {
|
|
366
|
+
if (!SCHEDULE_ID.test(runId))
|
|
367
|
+
throw new Error(`Invalid schedule run id '${runId}'.`);
|
|
368
|
+
const file = path.join(scheduleDir(this.root, id, false, this.projectCwd), "runs", `${runId}.json`);
|
|
369
|
+
if (!fs.existsSync(file))
|
|
370
|
+
return undefined;
|
|
371
|
+
const run = readJson(file, "schedule run");
|
|
372
|
+
if (run?.id !== runId || run.scheduleId !== id)
|
|
373
|
+
throw new Error(`Schedule run '${file}' has invalid fields.`);
|
|
374
|
+
return run;
|
|
375
|
+
}
|
|
348
376
|
writeRun(schedule, run, event) {
|
|
349
377
|
const dir = scheduleDir(this.root, schedule.id, true, this.projectCwd);
|
|
350
378
|
writePrivateAtomicJson(path.join(dir, "runs", `${run.id}.json`), run);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
379
|
+
// Sessions sharing this project update history.json from their own snapshots.
|
|
380
|
+
withFileLease(path.join(dir, "history.json"), () => {
|
|
381
|
+
// An earlier update may have timed out; a run's own receipt is newer than a "running" entry.
|
|
382
|
+
const earlier = this.history(schedule.id).filter((item) => item.id !== run.id)
|
|
383
|
+
.map((item) => item.state === "running" ? this.getRun(schedule.id, item.id) ?? item : item);
|
|
384
|
+
const runs = [run, ...earlier].slice(0, MAX_HISTORY);
|
|
385
|
+
writePrivateAtomicJson(path.join(dir, "history.json"), { schemaVersion: 1, runs });
|
|
386
|
+
fs.appendFileSync(path.join(dir, "events.jsonl"), `${JSON.stringify({ schemaVersion: 1, timestamp: new Date().toISOString(), event, scheduleId: schedule.id, runId: run.id, state: run.state })}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
387
|
+
});
|
|
355
388
|
}
|
|
356
389
|
appendEvent(schedule, event) {
|
|
357
390
|
const dir = scheduleDir(this.root, schedule.id, true, this.projectCwd);
|
|
@@ -368,11 +401,14 @@ function hasPendingScheduleWork(schedule) {
|
|
|
368
401
|
}
|
|
369
402
|
function nextAfter(trigger, plannedAt, now) {
|
|
370
403
|
if (trigger.kind === "once")
|
|
371
|
-
return undefined;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
404
|
+
return { ...trigger, nextRunAt: undefined };
|
|
405
|
+
if (trigger.kind === "calendar")
|
|
406
|
+
return {
|
|
407
|
+
...trigger,
|
|
408
|
+
...nextCalendarOccurrence(trigger, Math.max(plannedAt, now), calendarDateAfter(trigger, plannedAt, trigger.nextLocalDate)),
|
|
409
|
+
};
|
|
410
|
+
const next = plannedAt + (Math.floor(Math.max(0, now - plannedAt) / trigger.everyMs) + 1) * trigger.everyMs;
|
|
411
|
+
return { ...trigger, nextRunAt: timestamp(next) };
|
|
376
412
|
}
|
|
377
413
|
function nextRunAt(schedule) {
|
|
378
414
|
const value = schedule.trigger.nextRunAt;
|
|
@@ -385,9 +421,18 @@ function nextRunAt(schedule) {
|
|
|
385
421
|
}
|
|
386
422
|
function duePlannedAt(schedule, now) {
|
|
387
423
|
const next = nextRunAt(schedule);
|
|
388
|
-
if (next === undefined || next > now || schedule.catchUp !== "latest"
|
|
424
|
+
if (next === undefined || next > now || schedule.catchUp !== "latest")
|
|
389
425
|
return next;
|
|
390
|
-
|
|
426
|
+
if (schedule.trigger.kind === "calendar")
|
|
427
|
+
return Date.parse(latestCalendarOccurrence(schedule.trigger, now, schedule.trigger.nextLocalDate).nextRunAt);
|
|
428
|
+
if (schedule.trigger.kind === "interval")
|
|
429
|
+
return next + Math.floor((now - next) / schedule.trigger.everyMs) * schedule.trigger.everyMs;
|
|
430
|
+
return next;
|
|
431
|
+
}
|
|
432
|
+
function triggerLabel(trigger) {
|
|
433
|
+
if (trigger.kind === "calendar")
|
|
434
|
+
return `every ${trigger.every}${trigger.on ? ` (${trigger.on.join(", ")})` : ""} at ${trigger.at} ${trigger.timezone}`;
|
|
435
|
+
return trigger.kind === "once" ? `at ${trigger.at}` : `every ${trigger.every}`;
|
|
391
436
|
}
|
|
392
437
|
function textResult(text, schedules, runs, isError = false) {
|
|
393
438
|
const publicSchedules = schedules?.map(publicScheduleRecord);
|
|
@@ -417,8 +462,10 @@ function sanitizeTarget(params) {
|
|
|
417
462
|
if (params.async === false)
|
|
418
463
|
return { error: "Scheduled runs are always async." };
|
|
419
464
|
let baseRef;
|
|
465
|
+
let missionId;
|
|
420
466
|
try {
|
|
421
467
|
baseRef = normalizeWorktreeBaseRef(params.baseRef);
|
|
468
|
+
missionId = params.missionId === undefined ? undefined : validateMissionId(params.missionId);
|
|
422
469
|
}
|
|
423
470
|
catch (error) {
|
|
424
471
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
@@ -429,7 +476,7 @@ function sanitizeTarget(params) {
|
|
|
429
476
|
const normalizedArgs = normalizeWorkflowArgs(params.args);
|
|
430
477
|
if ("error" in normalizedArgs)
|
|
431
478
|
return { error: normalizedArgs.error };
|
|
432
|
-
return { target: { workflowScript: params.workflowScript.trim(), args: deepFreezeWorkflowArgs(normalizedArgs.args), ...(baseRef === undefined ? {} : { baseRef }) } };
|
|
479
|
+
return { target: { workflowScript: params.workflowScript.trim(), args: deepFreezeWorkflowArgs(normalizedArgs.args), ...(baseRef === undefined ? {} : { baseRef }), ...(missionId === undefined ? {} : { missionId }) } };
|
|
433
480
|
}
|
|
434
481
|
function executionParams(schedule, quiet = false) {
|
|
435
482
|
return {
|
|
@@ -437,7 +484,7 @@ function executionParams(schedule, quiet = false) {
|
|
|
437
484
|
async: true,
|
|
438
485
|
context: "fresh",
|
|
439
486
|
cwd: schedule.cwd,
|
|
440
|
-
mission: false,
|
|
487
|
+
...(schedule.target.missionId === undefined ? { mission: false } : {}),
|
|
441
488
|
// Scheduled fires have no operator watching, so completions must name the origin.
|
|
442
489
|
scheduleOrigin: { id: schedule.id, ...(schedule.name ? { name: schedule.name } : {}), ...(quiet ? { quiet: true } : {}) },
|
|
443
490
|
...(schedule.timeoutMs === undefined ? {} : { timeoutMs: schedule.timeoutMs }),
|
|
@@ -572,8 +619,10 @@ export class ScheduledRunManager {
|
|
|
572
619
|
for (const id of ids) {
|
|
573
620
|
try {
|
|
574
621
|
const schedule = store.get(id);
|
|
575
|
-
const
|
|
576
|
-
|
|
622
|
+
const listed = store.history(id).find((item) => item.asyncId === asyncId) ?? this.activeRun(store, schedule);
|
|
623
|
+
// history.json can lag the run's receipt when its update timed out.
|
|
624
|
+
const run = listed && (store.getRun(id, listed.id) ?? listed);
|
|
625
|
+
if (run?.asyncId !== asyncId || run.state !== "running")
|
|
577
626
|
continue;
|
|
578
627
|
this.finishRun(store, schedule, run, data.success === true, typeof data.summary === "string" ? data.summary : undefined);
|
|
579
628
|
return;
|
|
@@ -591,19 +640,20 @@ export class ScheduledRunManager {
|
|
|
591
640
|
return textResult(target.error, undefined, undefined, true);
|
|
592
641
|
const at = params.at?.trim();
|
|
593
642
|
const every = params.every?.trim();
|
|
594
|
-
|
|
643
|
+
const calendar = every === "day" || every === "week";
|
|
644
|
+
if (!calendar && Boolean(at) === Boolean(every))
|
|
595
645
|
return textResult("schedule.create requires exactly one trigger: at or every.", undefined, undefined, true);
|
|
596
646
|
if (params.overlap !== undefined && params.overlap !== "skip")
|
|
597
647
|
return textResult("This first recurring slice supports overlap='skip' only.", undefined, undefined, true);
|
|
598
648
|
if (params.catchUp !== undefined && params.catchUp !== "none" && params.catchUp !== "latest")
|
|
599
649
|
return textResult("catchUp must be 'none' or 'latest'.", undefined, undefined, true);
|
|
600
|
-
if (params.
|
|
601
|
-
return textResult("
|
|
602
|
-
if (params.on !== undefined || params.timezone !== undefined
|
|
603
|
-
return textResult("
|
|
650
|
+
if (params.mission !== undefined || params.missionUpdate !== undefined || params.missionStatus !== undefined || params.missionScope !== undefined)
|
|
651
|
+
return textResult("Schedules accept only an existing missionId; mission creation and updates are unsupported.", undefined, undefined, true);
|
|
652
|
+
if (!calendar && (params.on !== undefined || params.timezone !== undefined))
|
|
653
|
+
return textResult("on and timezone require every:'day' or every:'week'.", undefined, undefined, true);
|
|
604
654
|
if (params.quiet !== undefined && typeof params.quiet !== "boolean")
|
|
605
655
|
return textResult("quiet must be a boolean.", undefined, undefined, true);
|
|
606
|
-
if (at && params.quiet === true)
|
|
656
|
+
if (!calendar && at && params.quiet === true)
|
|
607
657
|
return textResult("quiet is only supported for recurring schedules.", undefined, undefined, true);
|
|
608
658
|
const sessionOnly = params.sessionOnly === true;
|
|
609
659
|
if (sessionOnly && params.cwd !== undefined && !samePath(params.cwd, ctx.cwd))
|
|
@@ -611,6 +661,9 @@ export class ScheduledRunManager {
|
|
|
611
661
|
const ownerSessionFile = sessionOnly ? ctx.sessionManager.getSessionFile() : undefined;
|
|
612
662
|
if (sessionOnly && !ownerSessionFile)
|
|
613
663
|
return textResult("sessionOnly schedules require a persisted current session.", undefined, undefined, true);
|
|
664
|
+
if (target.target.missionId !== undefined) {
|
|
665
|
+
readMission(resolveMissionStoreLocation({ projectRoot: path.resolve(params.cwd ?? ctx.cwd), ...(this.deps.config.missions ? { config: this.deps.config.missions } : {}) }), target.target.missionId);
|
|
666
|
+
}
|
|
614
667
|
const sessionId = ctx.sessionManager.getSessionId() ?? "unknown";
|
|
615
668
|
if (this.deps.resolveCapabilityCeiling?.(sessionId))
|
|
616
669
|
return textResult("Cannot persist a schedule while a capability ceiling is active.", undefined, undefined, true);
|
|
@@ -623,7 +676,11 @@ export class ScheduledRunManager {
|
|
|
623
676
|
return textResult(`Schedule '${id}' already exists.`, undefined, undefined, true);
|
|
624
677
|
const now = this.now();
|
|
625
678
|
let trigger;
|
|
626
|
-
if (
|
|
679
|
+
if (calendar) {
|
|
680
|
+
const rule = normalizeCalendarRule({ every, at, on: params.on, timezone: params.timezone });
|
|
681
|
+
trigger = { kind: "calendar", ...rule, ...nextCalendarOccurrence(rule, now) };
|
|
682
|
+
}
|
|
683
|
+
else if (at) {
|
|
627
684
|
const planned = parseScheduledRunTime(at, now);
|
|
628
685
|
trigger = { kind: "once", at, nextRunAt: timestamp(planned) };
|
|
629
686
|
}
|
|
@@ -632,7 +689,8 @@ export class ScheduledRunManager {
|
|
|
632
689
|
trigger = { kind: "interval", every: every, everyMs, anchorAt: timestamp(now), nextRunAt: timestamp(now + everyMs) };
|
|
633
690
|
}
|
|
634
691
|
const schedule = {
|
|
635
|
-
|
|
692
|
+
// Older readers ignore unknown target fields; they must reject mission-bound definitions.
|
|
693
|
+
schemaVersion: target.target.missionId === undefined ? 1 : 2,
|
|
636
694
|
id,
|
|
637
695
|
name: params.name?.trim() || targetLabel(target.target),
|
|
638
696
|
cwd: path.resolve(params.cwd ?? ctx.cwd),
|
|
@@ -643,24 +701,24 @@ export class ScheduledRunManager {
|
|
|
643
701
|
...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }),
|
|
644
702
|
paused: false,
|
|
645
703
|
...(sessionOnly ? { sessionOnly: true, ownerSessionFile: path.resolve(ownerSessionFile) } : {}),
|
|
646
|
-
...(trigger.kind
|
|
704
|
+
...(trigger.kind !== "once" && params.quiet === true ? { quiet: true } : {}),
|
|
647
705
|
createdAt: timestamp(now),
|
|
648
706
|
updatedAt: timestamp(now),
|
|
649
707
|
};
|
|
650
708
|
store.write(schedule);
|
|
651
709
|
store.appendEvent(schedule, "schedule.created");
|
|
652
710
|
this.arm(schedule, store);
|
|
653
|
-
return textResult(`Created schedule ${id}.\nName: ${schedule.name}\nTrigger: ${
|
|
711
|
+
return textResult(`Created schedule ${id}.\nName: ${schedule.name}\nTrigger: ${triggerLabel(trigger)}\nSession only: ${schedule.sessionOnly === true ? "yes" : "no"}\nQuiet: ${schedule.quiet === true ? "yes" : "no"}\nNext: ${schedule.trigger.nextRunAt}\nTarget: ${targetLabel(schedule.target)}${schedule.target.missionId === undefined ? "" : `\nMission: ${schedule.target.missionId}`}`, [schedule]);
|
|
654
712
|
}
|
|
655
713
|
list() {
|
|
656
714
|
const schedules = this.requireStore().list().sort((a, b) => (a.trigger.nextRunAt ?? "").localeCompare(b.trigger.nextRunAt ?? ""));
|
|
657
715
|
if (!schedules.length)
|
|
658
716
|
return textResult("No project schedules.", []);
|
|
659
|
-
return textResult([`Project schedules: ${schedules.length}`, ...schedules.map((item) => `- ${item.id} | ${item.paused ? "paused" : item.activeRunId ? "running" : "scheduled"} | ${item.trigger.nextRunAt ?? "no next run"} | ${item.sessionOnly === true ? "session-only" : "project"} | ${item.name}`)].join("\n"), schedules);
|
|
717
|
+
return textResult([`Project schedules: ${schedules.length}`, ...schedules.map((item) => `- ${item.id} | ${item.paused ? "paused" : item.activeRunId ? "running" : "scheduled"} | ${item.trigger.nextRunAt ?? "no next run"} | ${item.sessionOnly === true ? "session-only" : "project"} | ${item.name}${item.target.missionId === undefined ? "" : ` | mission ${item.target.missionId}`}`)].join("\n"), schedules);
|
|
660
718
|
}
|
|
661
719
|
show(params) {
|
|
662
720
|
const schedule = this.resolve(params);
|
|
663
|
-
return textResult([`Schedule: ${schedule.id}`, `Name: ${schedule.name}`, `State: ${schedule.paused ? "paused" : schedule.activeRunId ? "running" : "scheduled"}`, `Session only: ${schedule.sessionOnly === true ? "yes" : "no"}`, `Quiet: ${schedule.quiet === true ? "yes" : "no"}`, `Target: ${targetLabel(schedule.target)}`, `CWD: ${shortenPath(schedule.cwd)}`, `Next: ${schedule.trigger.nextRunAt ?? "none"}`, `Catch up: ${schedule.catchUp}`, schedule.activeRunId ? `Active run: ${schedule.activeRunId}` : undefined].filter(Boolean).join("\n"), [schedule]);
|
|
721
|
+
return textResult([`Schedule: ${schedule.id}`, `Name: ${schedule.name}`, `State: ${schedule.paused ? "paused" : schedule.activeRunId ? "running" : "scheduled"}`, `Session only: ${schedule.sessionOnly === true ? "yes" : "no"}`, `Quiet: ${schedule.quiet === true ? "yes" : "no"}`, `Target: ${targetLabel(schedule.target)}`, schedule.target.missionId === undefined ? undefined : `Mission: ${schedule.target.missionId}`, `CWD: ${shortenPath(schedule.cwd)}`, `Trigger: ${triggerLabel(schedule.trigger)}`, `Next: ${schedule.trigger.nextRunAt ?? "none"}`, `Catch up: ${schedule.catchUp}`, schedule.activeRunId ? `Active run: ${schedule.activeRunId}` : undefined].filter(Boolean).join("\n"), [schedule]);
|
|
664
722
|
}
|
|
665
723
|
history(params) {
|
|
666
724
|
const schedule = this.resolve(params);
|
|
@@ -691,11 +749,20 @@ export class ScheduledRunManager {
|
|
|
691
749
|
}
|
|
692
750
|
if (params.quiet !== undefined && typeof params.quiet !== "boolean")
|
|
693
751
|
return textResult("quiet must be a boolean.", undefined, undefined, true);
|
|
752
|
+
const manualTrigger = schedule.trigger;
|
|
694
753
|
const run = await this.launch(store, schedule, this.now(), "manual", false, params.quiet === true);
|
|
695
754
|
const updated = store.get(schedule.id);
|
|
696
755
|
if (run.state === "running") {
|
|
697
756
|
const now = this.now();
|
|
698
|
-
if (updated.trigger.kind === "
|
|
757
|
+
if (updated.trigger.kind === "calendar") {
|
|
758
|
+
if (manualTrigger.kind !== "calendar")
|
|
759
|
+
throw new Error("Schedule trigger changed during manual launch.");
|
|
760
|
+
const consumed = latestCalendarOccurrence(manualTrigger, now, manualTrigger.nextLocalDate)?.nextRunAt ?? manualTrigger.nextRunAt;
|
|
761
|
+
const advanced = nextAfter(manualTrigger, Date.parse(consumed), now);
|
|
762
|
+
if (advanced.nextLocalDate > updated.trigger.nextLocalDate)
|
|
763
|
+
updated.trigger = advanced;
|
|
764
|
+
}
|
|
765
|
+
else if (updated.trigger.kind === "interval")
|
|
699
766
|
updated.trigger.nextRunAt = timestamp(now + updated.trigger.everyMs);
|
|
700
767
|
else
|
|
701
768
|
updated.trigger.nextRunAt = undefined;
|
|
@@ -724,7 +791,7 @@ export class ScheduledRunManager {
|
|
|
724
791
|
const schedule = this.resolve(params);
|
|
725
792
|
const store = this.requireStore();
|
|
726
793
|
if (schedule.activeRunId) {
|
|
727
|
-
const run =
|
|
794
|
+
const run = this.activeRun(store, schedule);
|
|
728
795
|
let terminal = false;
|
|
729
796
|
if (run?.scheduleId === schedule.id && run.state === "running" && run.asyncId && run.asyncDir) {
|
|
730
797
|
const status = readJson(path.join(run.asyncDir, "status.json"), "async status");
|
|
@@ -745,8 +812,16 @@ export class ScheduledRunManager {
|
|
|
745
812
|
restoreOne(store, schedule, notBefore, rearm = true) {
|
|
746
813
|
if (!scheduleBelongsToSession(schedule, this.requireContext(store)))
|
|
747
814
|
return;
|
|
815
|
+
if (schedule.trigger.kind === "calendar") {
|
|
816
|
+
const refreshed = restoreCalendarTrigger(schedule.trigger);
|
|
817
|
+
if (JSON.stringify(refreshed) !== JSON.stringify(schedule.trigger)) {
|
|
818
|
+
schedule.trigger = refreshed;
|
|
819
|
+
schedule.updatedAt = timestamp(this.now());
|
|
820
|
+
store.write(schedule);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
748
823
|
if (schedule.activeRunId) {
|
|
749
|
-
const run =
|
|
824
|
+
const run = this.activeRun(store, schedule);
|
|
750
825
|
if (run?.state === "running" && run.asyncId)
|
|
751
826
|
this.observedAsyncIds.add(run.asyncId);
|
|
752
827
|
const startedAt = run?.startedAt ? Date.parse(run.startedAt) : Number.NaN;
|
|
@@ -816,9 +891,9 @@ export class ScheduledRunManager {
|
|
|
816
891
|
if (!schedule)
|
|
817
892
|
return;
|
|
818
893
|
const now = this.now();
|
|
819
|
-
const planned = schedule.trigger.kind
|
|
820
|
-
const notBefore = planned !== undefined && planned <= now ? Date.parse(nextAfter(schedule.trigger, planned, now)) : undefined;
|
|
821
|
-
this.restoreOne(store, schedule, notBefore, schedule.trigger.kind
|
|
894
|
+
const planned = schedule.trigger.kind !== "once" ? duePlannedAt(schedule, now) : undefined;
|
|
895
|
+
const notBefore = planned !== undefined && planned <= now ? Date.parse(nextAfter(schedule.trigger, planned, now).nextRunAt) : undefined;
|
|
896
|
+
this.restoreOne(store, schedule, notBefore, schedule.trigger.kind !== "once");
|
|
822
897
|
}
|
|
823
898
|
catch (error) {
|
|
824
899
|
console.warn(`[pi-subagents] Scheduled run '${id}' could not be restored after fire failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -845,12 +920,13 @@ export class ScheduledRunManager {
|
|
|
845
920
|
async launch(store, schedule, planned, dueReason, advance, quiet) {
|
|
846
921
|
const now = this.now();
|
|
847
922
|
const nextRunAtBeforeClaim = schedule.trigger.nextRunAt;
|
|
923
|
+
const nextLocalDateBeforeClaim = schedule.trigger.kind === "calendar" ? schedule.trigger.nextLocalDate : undefined;
|
|
848
924
|
const run = { schemaVersion: 1, id: this.randomId(), scheduleId: schedule.id, plannedAt: timestamp(planned), dueReason, state: "running", startedAt: timestamp(now) };
|
|
849
925
|
if (schedule.activeRunId) {
|
|
850
926
|
run.state = "skipped";
|
|
851
927
|
run.completedAt = timestamp(now);
|
|
852
928
|
if (advance) {
|
|
853
|
-
schedule.trigger
|
|
929
|
+
schedule.trigger = nextAfter(schedule.trigger, planned, now);
|
|
854
930
|
schedule.updatedAt = timestamp(now);
|
|
855
931
|
store.write(schedule);
|
|
856
932
|
}
|
|
@@ -867,26 +943,75 @@ export class ScheduledRunManager {
|
|
|
867
943
|
fs.closeSync(lock);
|
|
868
944
|
}
|
|
869
945
|
catch (error) {
|
|
946
|
+
if (lock !== undefined) {
|
|
947
|
+
// An empty or partial claim would make every later launch skip. Keep the
|
|
948
|
+
// descriptor open until the inode check so a replacement owner's lock survives.
|
|
949
|
+
try {
|
|
950
|
+
// Bigint stats: Windows file ids exceed 2^53, so number inodes of nearby files can compare equal.
|
|
951
|
+
const owned = fs.fstatSync(lock, { bigint: true });
|
|
952
|
+
const current = fs.lstatSync(lockPath, { bigint: true });
|
|
953
|
+
if (owned.ino !== 0n && owned.dev === current.dev && owned.ino === current.ino)
|
|
954
|
+
fs.rmSync(lockPath);
|
|
955
|
+
}
|
|
956
|
+
catch { /* Preserve the original error. */ }
|
|
957
|
+
try {
|
|
958
|
+
fs.closeSync(lock);
|
|
959
|
+
}
|
|
960
|
+
catch { /* Preserve the original error. */ }
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
870
963
|
if (error.code !== "EEXIST")
|
|
871
964
|
throw error;
|
|
872
965
|
run.state = "skipped";
|
|
873
966
|
run.completedAt = timestamp(now);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
967
|
+
// Losing the claim gives this snapshot no authority to change the owner.
|
|
968
|
+
const latest = store.find(schedule.id);
|
|
969
|
+
if (!latest) {
|
|
970
|
+
this.clearTimer(store, schedule.id);
|
|
971
|
+
return run;
|
|
972
|
+
}
|
|
973
|
+
store.writeRun(latest, run, "schedule.skipped_overlap");
|
|
974
|
+
if (latest.trigger.kind === "once") {
|
|
975
|
+
this.clearTimer(store, latest.id);
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
// The owner may hold the lock before persisting its cursor. Back off
|
|
979
|
+
// locally without consuming that pending occurrence on disk.
|
|
980
|
+
const next = nextRunAt(latest);
|
|
981
|
+
const notBefore = next !== undefined && next <= now
|
|
982
|
+
? Date.parse(nextAfter(latest.trigger, duePlannedAt(latest, now) ?? planned, now).nextRunAt)
|
|
983
|
+
: undefined;
|
|
984
|
+
this.arm(latest, store, notBefore);
|
|
878
985
|
}
|
|
879
|
-
store.writeRun(schedule, run, "schedule.skipped_overlap");
|
|
880
|
-
this.arm(schedule, store);
|
|
881
986
|
return run;
|
|
882
987
|
}
|
|
883
988
|
schedule.activeRunId = run.id;
|
|
884
989
|
schedule.lastRunId = run.id;
|
|
885
990
|
if (advance)
|
|
886
|
-
schedule.trigger
|
|
991
|
+
schedule.trigger = nextAfter(schedule.trigger, planned, now);
|
|
887
992
|
schedule.updatedAt = timestamp(now);
|
|
888
|
-
|
|
889
|
-
|
|
993
|
+
try {
|
|
994
|
+
store.write(schedule);
|
|
995
|
+
store.writeRun(schedule, run, "schedule.run.started");
|
|
996
|
+
}
|
|
997
|
+
catch (error) {
|
|
998
|
+
// No child has launched: release this run's claim, then report the original error.
|
|
999
|
+
// Another session may already have recovered the claim and launched its own run.
|
|
1000
|
+
try {
|
|
1001
|
+
const latest = store.find(schedule.id);
|
|
1002
|
+
if (latest?.activeRunId === run.id) {
|
|
1003
|
+
latest.activeRunId = undefined;
|
|
1004
|
+
latest.updatedAt = timestamp(this.now());
|
|
1005
|
+
store.write(latest);
|
|
1006
|
+
}
|
|
1007
|
+
if (fs.readFileSync(lockPath, "utf-8") === run.id)
|
|
1008
|
+
fs.rmSync(lockPath);
|
|
1009
|
+
}
|
|
1010
|
+
catch (cleanupError) {
|
|
1011
|
+
console.warn(`[pi-subagents] Could not release schedule claim '${run.id}': ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
1012
|
+
}
|
|
1013
|
+
throw error;
|
|
1014
|
+
}
|
|
890
1015
|
try {
|
|
891
1016
|
const result = await this.deps.launch(executionParams(schedule, dueReason === "manual" ? quiet === true : schedule.quiet === true), this.requireContext(store), new AbortController().signal);
|
|
892
1017
|
const asyncId = result.details?.asyncId ?? result.details?.runId;
|
|
@@ -900,27 +1025,45 @@ export class ScheduledRunManager {
|
|
|
900
1025
|
return run;
|
|
901
1026
|
}
|
|
902
1027
|
catch (error) {
|
|
1028
|
+
// The child is running, so its claim stays until completion.
|
|
1029
|
+
if (run.asyncId)
|
|
1030
|
+
throw new Error(`Scheduled run '${run.id}' attached to async run '${run.asyncId}', but recording it failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
903
1031
|
run.state = "failed_launch";
|
|
904
1032
|
run.completedAt = timestamp(this.now());
|
|
905
1033
|
run.error = error instanceof Error ? error.message : String(error);
|
|
906
1034
|
const latest = store.get(schedule.id);
|
|
907
1035
|
latest.activeRunId = undefined;
|
|
908
|
-
if (!advance && nextRunAtBeforeClaim)
|
|
909
|
-
latest.trigger.
|
|
1036
|
+
if (!advance && nextRunAtBeforeClaim) {
|
|
1037
|
+
if (latest.trigger.kind === "calendar") {
|
|
1038
|
+
// An overlapping timer may have advanced both cursor fields while
|
|
1039
|
+
// this manual attachment was pending. Failure satisfies neither.
|
|
1040
|
+
if (nextLocalDateBeforeClaim !== undefined) {
|
|
1041
|
+
latest.trigger.nextLocalDate = nextLocalDateBeforeClaim;
|
|
1042
|
+
latest.trigger.nextRunAt = nextRunAtBeforeClaim;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
else
|
|
1046
|
+
latest.trigger.nextRunAt = nextRunAtBeforeClaim;
|
|
1047
|
+
}
|
|
910
1048
|
latest.updatedAt = timestamp(this.now());
|
|
911
1049
|
store.write(latest);
|
|
912
|
-
store.writeRun(latest, run, "schedule.run.failed");
|
|
913
1050
|
fs.rmSync(lockPath, { force: true });
|
|
914
|
-
|
|
1051
|
+
try {
|
|
1052
|
+
store.writeRun(latest, run, "schedule.run.failed");
|
|
1053
|
+
}
|
|
1054
|
+
finally {
|
|
1055
|
+
this.arm(latest, store);
|
|
1056
|
+
}
|
|
915
1057
|
return run;
|
|
916
1058
|
}
|
|
917
1059
|
}
|
|
918
1060
|
finishRun(store, schedule, run, success, error) {
|
|
919
1061
|
const now = this.now();
|
|
920
1062
|
const next = nextRunAt(schedule);
|
|
1063
|
+
let skipped;
|
|
921
1064
|
if (next !== undefined && next <= now) {
|
|
922
1065
|
const planned = duePlannedAt(schedule, now);
|
|
923
|
-
|
|
1066
|
+
skipped = {
|
|
924
1067
|
schemaVersion: 1,
|
|
925
1068
|
id: this.randomId(),
|
|
926
1069
|
scheduleId: schedule.id,
|
|
@@ -929,8 +1072,7 @@ export class ScheduledRunManager {
|
|
|
929
1072
|
state: "skipped",
|
|
930
1073
|
completedAt: timestamp(now),
|
|
931
1074
|
};
|
|
932
|
-
schedule.trigger
|
|
933
|
-
store.writeRun(schedule, skipped, "schedule.skipped_overlap");
|
|
1075
|
+
schedule.trigger = nextAfter(schedule.trigger, planned, now);
|
|
934
1076
|
}
|
|
935
1077
|
if (run.asyncId)
|
|
936
1078
|
this.observedAsyncIds.delete(run.asyncId);
|
|
@@ -942,8 +1084,15 @@ export class ScheduledRunManager {
|
|
|
942
1084
|
schedule.updatedAt = timestamp(now);
|
|
943
1085
|
store.write(schedule);
|
|
944
1086
|
fs.rmSync(path.join(store.directory(schedule.id), "active.lock"), { force: true });
|
|
945
|
-
|
|
946
|
-
|
|
1087
|
+
// The schedule is already released; a history.json timeout must not leave it unarmed.
|
|
1088
|
+
try {
|
|
1089
|
+
store.writeRun(schedule, run, success ? "schedule.run.completed" : "schedule.run.failed");
|
|
1090
|
+
if (skipped)
|
|
1091
|
+
store.writeRun(schedule, skipped, "schedule.skipped_overlap");
|
|
1092
|
+
}
|
|
1093
|
+
finally {
|
|
1094
|
+
this.arm(schedule, store);
|
|
1095
|
+
}
|
|
947
1096
|
}
|
|
948
1097
|
recordMissed(store, schedule, planned, dueReason) {
|
|
949
1098
|
const run = {
|
|
@@ -955,7 +1104,7 @@ export class ScheduledRunManager {
|
|
|
955
1104
|
state: "missed",
|
|
956
1105
|
completedAt: timestamp(this.now()),
|
|
957
1106
|
};
|
|
958
|
-
schedule.trigger
|
|
1107
|
+
schedule.trigger = nextAfter(schedule.trigger, planned, this.now());
|
|
959
1108
|
schedule.updatedAt = timestamp(this.now());
|
|
960
1109
|
store.write(schedule);
|
|
961
1110
|
store.writeRun(schedule, run, "schedule.missed");
|
|
@@ -1001,6 +1150,11 @@ export class ScheduledRunManager {
|
|
|
1001
1150
|
throw new Error("Schedule runtime context is unavailable.");
|
|
1002
1151
|
return ctx;
|
|
1003
1152
|
}
|
|
1153
|
+
activeRun(store, schedule) {
|
|
1154
|
+
if (!schedule.activeRunId)
|
|
1155
|
+
return undefined;
|
|
1156
|
+
return store.getRun(schedule.id, schedule.activeRunId) ?? store.history(schedule.id).find((run) => run.id === schedule.activeRunId);
|
|
1157
|
+
}
|
|
1004
1158
|
timerKey(store, id) {
|
|
1005
1159
|
return `${store.root}\0${id}`;
|
|
1006
1160
|
}
|