pi-subagents 0.75.0 → 0.76.1
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 +62 -0
- package/docs/agents.md +1 -1
- package/docs/configuration.md +4 -2
- package/docs/extension-api.md +3 -3
- package/docs/missions.md +12 -2
- package/docs/tool-reference.md +21 -6
- package/docs/workflows.md +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +5 -0
- package/index.js.map +1 -1
- package/package.json +3 -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.map +1 -1
- package/src/agents/agents.js +101 -121
- package/src/agents/agents.js.map +1 -1
- package/src/extension/doctor.d.ts +3 -1
- package/src/extension/doctor.d.ts.map +1 -1
- package/src/extension/doctor.js +3 -9
- package/src/extension/doctor.js.map +1 -1
- package/src/extension/index.d.ts.map +1 -1
- package/src/extension/index.js +55 -11
- package/src/extension/index.js.map +1 -1
- package/src/extension/reply-workflow-script.d.ts.map +1 -1
- package/src/extension/reply-workflow-script.js +20 -11
- package/src/extension/reply-workflow-script.js.map +1 -1
- package/src/extension/schemas.d.ts +1 -1
- package/src/extension/schemas.js +8 -8
- package/src/extension/schemas.js.map +1 -1
- package/src/extension/tool-description.js +1 -1
- package/src/extension/tool-description.js.map +1 -1
- package/src/integrations/herdr-status.d.ts +0 -1
- package/src/integrations/herdr-status.d.ts.map +1 -1
- package/src/integrations/herdr-status.js +18 -52
- package/src/integrations/herdr-status.js.map +1 -1
- package/src/intercom/native-supervisor-channel.d.ts +1 -0
- package/src/intercom/native-supervisor-channel.d.ts.map +1 -1
- package/src/intercom/native-supervisor-channel.js +1 -1
- package/src/intercom/native-supervisor-channel.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/profiles/profiles.d.ts.map +1 -1
- package/src/profiles/profiles.js +25 -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-job-tracker.d.ts +3 -2
- package/src/runs/background/async-job-tracker.d.ts.map +1 -1
- package/src/runs/background/async-job-tracker.js +2 -1
- package/src/runs/background/async-job-tracker.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/notify.d.ts +13 -2
- package/src/runs/background/notify.d.ts.map +1 -1
- package/src/runs/background/notify.js +48 -5
- package/src/runs/background/notify.js.map +1 -1
- package/src/runs/background/result-files.d.ts +2 -0
- package/src/runs/background/result-files.d.ts.map +1 -1
- package/src/runs/background/result-files.js +4 -0
- package/src/runs/background/result-files.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 +12 -1
- package/src/runs/background/run-child-session.js.map +1 -1
- package/src/runs/background/scheduled-runs.d.ts +3 -1
- package/src/runs/background/scheduled-runs.d.ts.map +1 -1
- package/src/runs/background/scheduled-runs.js +184 -47
- package/src/runs/background/scheduled-runs.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 +14 -6
- 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/async-stop-action.d.ts.map +1 -1
- package/src/runs/foreground/async-stop-action.js +30 -11
- package/src/runs/foreground/async-stop-action.js.map +1 -1
- package/src/runs/foreground/execution.d.ts.map +1 -1
- package/src/runs/foreground/execution.js +33 -13
- 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 +54 -11
- package/src/runs/foreground/subagent-executor.js.map +1 -1
- package/src/runs/shared/child-session.d.ts.map +1 -1
- package/src/runs/shared/child-session.js +10 -2
- package/src/runs/shared/child-session.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 +5 -3
- package/src/runs/shared/mcp-direct-tool-allowlist.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/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/subagent-prompt-runtime.d.ts.map +1 -1
- package/src/runs/shared/subagent-prompt-runtime.js +4 -3
- 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/package-version.d.ts +3 -0
- package/src/shared/package-version.d.ts.map +1 -0
- package/src/shared/package-version.js +15 -0
- package/src/shared/package-version.js.map +1 -0
- package/src/shared/parent-wake.d.ts +18 -0
- package/src/shared/parent-wake.d.ts.map +1 -0
- package/src/shared/parent-wake.js +47 -0
- package/src/shared/parent-wake.js.map +1 -0
- 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 +10 -0
- package/src/shared/types.d.ts.map +1 -1
- package/src/shared/types.js.map +1 -1
- package/src/tui/render.d.ts.map +1 -1
- package/src/tui/render.js +7 -2
- 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
|
@@ -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";
|
|
@@ -12,6 +13,7 @@ import { getConfigDirName } from "../../shared/utils.js";
|
|
|
12
13
|
import { normalizeWorktreeBaseRef } from "../shared/worktree.js";
|
|
13
14
|
import { deepFreezeWorkflowArgs, normalizeWorkflowArgs } from "../../workflows/workflow-resources.js";
|
|
14
15
|
import { readMission, resolveMissionStoreLocation, validateMissionId } from "../../missions/store.js";
|
|
16
|
+
import { calendarDateAfter, latestCalendarOccurrence, nextCalendarOccurrence, normalizeCalendarRule, restoreCalendarTrigger } from "./calendar-schedule.js";
|
|
15
17
|
export const SCHEDULED_RUN_ACTIONS = [
|
|
16
18
|
"schedule.create",
|
|
17
19
|
"schedule.list",
|
|
@@ -294,6 +296,9 @@ function parseSchedule(value, file) {
|
|
|
294
296
|
if (typeof record.trigger.every !== "string" || typeof record.trigger.everyMs !== "number" || typeof record.trigger.anchorAt !== "string" || typeof record.trigger.nextRunAt !== "string")
|
|
295
297
|
throw new Error(`Schedule record '${file}' has an invalid interval trigger.`);
|
|
296
298
|
}
|
|
299
|
+
else if (record.trigger.kind === "calendar") {
|
|
300
|
+
restoreCalendarTrigger(record.trigger); // Validate without rewriting during read-only list/show.
|
|
301
|
+
}
|
|
297
302
|
else
|
|
298
303
|
throw new Error(`Schedule record '${file}' has an unsupported trigger.`);
|
|
299
304
|
if (record.sessionOnly !== undefined && typeof record.sessionOnly !== "boolean")
|
|
@@ -356,13 +361,30 @@ class ScheduleStore {
|
|
|
356
361
|
throw new Error(`Schedule history '${file}' has invalid fields.`);
|
|
357
362
|
return value.runs;
|
|
358
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
|
+
}
|
|
359
376
|
writeRun(schedule, run, event) {
|
|
360
377
|
const dir = scheduleDir(this.root, schedule.id, true, this.projectCwd);
|
|
361
378
|
writePrivateAtomicJson(path.join(dir, "runs", `${run.id}.json`), run);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
});
|
|
366
388
|
}
|
|
367
389
|
appendEvent(schedule, event) {
|
|
368
390
|
const dir = scheduleDir(this.root, schedule.id, true, this.projectCwd);
|
|
@@ -379,11 +401,14 @@ function hasPendingScheduleWork(schedule) {
|
|
|
379
401
|
}
|
|
380
402
|
function nextAfter(trigger, plannedAt, now) {
|
|
381
403
|
if (trigger.kind === "once")
|
|
382
|
-
return undefined;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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) };
|
|
387
412
|
}
|
|
388
413
|
function nextRunAt(schedule) {
|
|
389
414
|
const value = schedule.trigger.nextRunAt;
|
|
@@ -396,9 +421,18 @@ function nextRunAt(schedule) {
|
|
|
396
421
|
}
|
|
397
422
|
function duePlannedAt(schedule, now) {
|
|
398
423
|
const next = nextRunAt(schedule);
|
|
399
|
-
if (next === undefined || next > now || schedule.catchUp !== "latest"
|
|
424
|
+
if (next === undefined || next > now || schedule.catchUp !== "latest")
|
|
400
425
|
return next;
|
|
401
|
-
|
|
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}`;
|
|
402
436
|
}
|
|
403
437
|
function textResult(text, schedules, runs, isError = false) {
|
|
404
438
|
const publicSchedules = schedules?.map(publicScheduleRecord);
|
|
@@ -585,8 +619,10 @@ export class ScheduledRunManager {
|
|
|
585
619
|
for (const id of ids) {
|
|
586
620
|
try {
|
|
587
621
|
const schedule = store.get(id);
|
|
588
|
-
const
|
|
589
|
-
|
|
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")
|
|
590
626
|
continue;
|
|
591
627
|
this.finishRun(store, schedule, run, data.success === true, typeof data.summary === "string" ? data.summary : undefined);
|
|
592
628
|
return;
|
|
@@ -604,7 +640,8 @@ export class ScheduledRunManager {
|
|
|
604
640
|
return textResult(target.error, undefined, undefined, true);
|
|
605
641
|
const at = params.at?.trim();
|
|
606
642
|
const every = params.every?.trim();
|
|
607
|
-
|
|
643
|
+
const calendar = every === "day" || every === "week";
|
|
644
|
+
if (!calendar && Boolean(at) === Boolean(every))
|
|
608
645
|
return textResult("schedule.create requires exactly one trigger: at or every.", undefined, undefined, true);
|
|
609
646
|
if (params.overlap !== undefined && params.overlap !== "skip")
|
|
610
647
|
return textResult("This first recurring slice supports overlap='skip' only.", undefined, undefined, true);
|
|
@@ -612,11 +649,11 @@ export class ScheduledRunManager {
|
|
|
612
649
|
return textResult("catchUp must be 'none' or 'latest'.", undefined, undefined, true);
|
|
613
650
|
if (params.mission !== undefined || params.missionUpdate !== undefined || params.missionStatus !== undefined || params.missionScope !== undefined)
|
|
614
651
|
return textResult("Schedules accept only an existing missionId; mission creation and updates are unsupported.", undefined, undefined, true);
|
|
615
|
-
if (params.on !== undefined || params.timezone !== undefined
|
|
616
|
-
return textResult("
|
|
652
|
+
if (!calendar && (params.on !== undefined || params.timezone !== undefined))
|
|
653
|
+
return textResult("on and timezone require every:'day' or every:'week'.", undefined, undefined, true);
|
|
617
654
|
if (params.quiet !== undefined && typeof params.quiet !== "boolean")
|
|
618
655
|
return textResult("quiet must be a boolean.", undefined, undefined, true);
|
|
619
|
-
if (at && params.quiet === true)
|
|
656
|
+
if (!calendar && at && params.quiet === true)
|
|
620
657
|
return textResult("quiet is only supported for recurring schedules.", undefined, undefined, true);
|
|
621
658
|
const sessionOnly = params.sessionOnly === true;
|
|
622
659
|
if (sessionOnly && params.cwd !== undefined && !samePath(params.cwd, ctx.cwd))
|
|
@@ -639,7 +676,11 @@ export class ScheduledRunManager {
|
|
|
639
676
|
return textResult(`Schedule '${id}' already exists.`, undefined, undefined, true);
|
|
640
677
|
const now = this.now();
|
|
641
678
|
let trigger;
|
|
642
|
-
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) {
|
|
643
684
|
const planned = parseScheduledRunTime(at, now);
|
|
644
685
|
trigger = { kind: "once", at, nextRunAt: timestamp(planned) };
|
|
645
686
|
}
|
|
@@ -660,14 +701,14 @@ export class ScheduledRunManager {
|
|
|
660
701
|
...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }),
|
|
661
702
|
paused: false,
|
|
662
703
|
...(sessionOnly ? { sessionOnly: true, ownerSessionFile: path.resolve(ownerSessionFile) } : {}),
|
|
663
|
-
...(trigger.kind
|
|
704
|
+
...(trigger.kind !== "once" && params.quiet === true ? { quiet: true } : {}),
|
|
664
705
|
createdAt: timestamp(now),
|
|
665
706
|
updatedAt: timestamp(now),
|
|
666
707
|
};
|
|
667
708
|
store.write(schedule);
|
|
668
709
|
store.appendEvent(schedule, "schedule.created");
|
|
669
710
|
this.arm(schedule, store);
|
|
670
|
-
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]);
|
|
671
712
|
}
|
|
672
713
|
list() {
|
|
673
714
|
const schedules = this.requireStore().list().sort((a, b) => (a.trigger.nextRunAt ?? "").localeCompare(b.trigger.nextRunAt ?? ""));
|
|
@@ -677,7 +718,7 @@ export class ScheduledRunManager {
|
|
|
677
718
|
}
|
|
678
719
|
show(params) {
|
|
679
720
|
const schedule = this.resolve(params);
|
|
680
|
-
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)}`, `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]);
|
|
681
722
|
}
|
|
682
723
|
history(params) {
|
|
683
724
|
const schedule = this.resolve(params);
|
|
@@ -708,11 +749,20 @@ export class ScheduledRunManager {
|
|
|
708
749
|
}
|
|
709
750
|
if (params.quiet !== undefined && typeof params.quiet !== "boolean")
|
|
710
751
|
return textResult("quiet must be a boolean.", undefined, undefined, true);
|
|
752
|
+
const manualTrigger = schedule.trigger;
|
|
711
753
|
const run = await this.launch(store, schedule, this.now(), "manual", false, params.quiet === true);
|
|
712
754
|
const updated = store.get(schedule.id);
|
|
713
755
|
if (run.state === "running") {
|
|
714
756
|
const now = this.now();
|
|
715
|
-
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")
|
|
716
766
|
updated.trigger.nextRunAt = timestamp(now + updated.trigger.everyMs);
|
|
717
767
|
else
|
|
718
768
|
updated.trigger.nextRunAt = undefined;
|
|
@@ -741,7 +791,7 @@ export class ScheduledRunManager {
|
|
|
741
791
|
const schedule = this.resolve(params);
|
|
742
792
|
const store = this.requireStore();
|
|
743
793
|
if (schedule.activeRunId) {
|
|
744
|
-
const run =
|
|
794
|
+
const run = this.activeRun(store, schedule);
|
|
745
795
|
let terminal = false;
|
|
746
796
|
if (run?.scheduleId === schedule.id && run.state === "running" && run.asyncId && run.asyncDir) {
|
|
747
797
|
const status = readJson(path.join(run.asyncDir, "status.json"), "async status");
|
|
@@ -762,8 +812,16 @@ export class ScheduledRunManager {
|
|
|
762
812
|
restoreOne(store, schedule, notBefore, rearm = true) {
|
|
763
813
|
if (!scheduleBelongsToSession(schedule, this.requireContext(store)))
|
|
764
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
|
+
}
|
|
765
823
|
if (schedule.activeRunId) {
|
|
766
|
-
const run =
|
|
824
|
+
const run = this.activeRun(store, schedule);
|
|
767
825
|
if (run?.state === "running" && run.asyncId)
|
|
768
826
|
this.observedAsyncIds.add(run.asyncId);
|
|
769
827
|
const startedAt = run?.startedAt ? Date.parse(run.startedAt) : Number.NaN;
|
|
@@ -833,9 +891,9 @@ export class ScheduledRunManager {
|
|
|
833
891
|
if (!schedule)
|
|
834
892
|
return;
|
|
835
893
|
const now = this.now();
|
|
836
|
-
const planned = schedule.trigger.kind
|
|
837
|
-
const notBefore = planned !== undefined && planned <= now ? Date.parse(nextAfter(schedule.trigger, planned, now)) : undefined;
|
|
838
|
-
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");
|
|
839
897
|
}
|
|
840
898
|
catch (error) {
|
|
841
899
|
console.warn(`[pi-subagents] Scheduled run '${id}' could not be restored after fire failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -862,12 +920,13 @@ export class ScheduledRunManager {
|
|
|
862
920
|
async launch(store, schedule, planned, dueReason, advance, quiet) {
|
|
863
921
|
const now = this.now();
|
|
864
922
|
const nextRunAtBeforeClaim = schedule.trigger.nextRunAt;
|
|
923
|
+
const nextLocalDateBeforeClaim = schedule.trigger.kind === "calendar" ? schedule.trigger.nextLocalDate : undefined;
|
|
865
924
|
const run = { schemaVersion: 1, id: this.randomId(), scheduleId: schedule.id, plannedAt: timestamp(planned), dueReason, state: "running", startedAt: timestamp(now) };
|
|
866
925
|
if (schedule.activeRunId) {
|
|
867
926
|
run.state = "skipped";
|
|
868
927
|
run.completedAt = timestamp(now);
|
|
869
928
|
if (advance) {
|
|
870
|
-
schedule.trigger
|
|
929
|
+
schedule.trigger = nextAfter(schedule.trigger, planned, now);
|
|
871
930
|
schedule.updatedAt = timestamp(now);
|
|
872
931
|
store.write(schedule);
|
|
873
932
|
}
|
|
@@ -884,26 +943,75 @@ export class ScheduledRunManager {
|
|
|
884
943
|
fs.closeSync(lock);
|
|
885
944
|
}
|
|
886
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
|
+
}
|
|
887
963
|
if (error.code !== "EEXIST")
|
|
888
964
|
throw error;
|
|
889
965
|
run.state = "skipped";
|
|
890
966
|
run.completedAt = timestamp(now);
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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);
|
|
895
985
|
}
|
|
896
|
-
store.writeRun(schedule, run, "schedule.skipped_overlap");
|
|
897
|
-
this.arm(schedule, store);
|
|
898
986
|
return run;
|
|
899
987
|
}
|
|
900
988
|
schedule.activeRunId = run.id;
|
|
901
989
|
schedule.lastRunId = run.id;
|
|
902
990
|
if (advance)
|
|
903
|
-
schedule.trigger
|
|
991
|
+
schedule.trigger = nextAfter(schedule.trigger, planned, now);
|
|
904
992
|
schedule.updatedAt = timestamp(now);
|
|
905
|
-
|
|
906
|
-
|
|
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
|
+
}
|
|
907
1015
|
try {
|
|
908
1016
|
const result = await this.deps.launch(executionParams(schedule, dueReason === "manual" ? quiet === true : schedule.quiet === true), this.requireContext(store), new AbortController().signal);
|
|
909
1017
|
const asyncId = result.details?.asyncId ?? result.details?.runId;
|
|
@@ -917,27 +1025,45 @@ export class ScheduledRunManager {
|
|
|
917
1025
|
return run;
|
|
918
1026
|
}
|
|
919
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 });
|
|
920
1031
|
run.state = "failed_launch";
|
|
921
1032
|
run.completedAt = timestamp(this.now());
|
|
922
1033
|
run.error = error instanceof Error ? error.message : String(error);
|
|
923
1034
|
const latest = store.get(schedule.id);
|
|
924
1035
|
latest.activeRunId = undefined;
|
|
925
|
-
if (!advance && nextRunAtBeforeClaim)
|
|
926
|
-
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
|
+
}
|
|
927
1048
|
latest.updatedAt = timestamp(this.now());
|
|
928
1049
|
store.write(latest);
|
|
929
|
-
store.writeRun(latest, run, "schedule.run.failed");
|
|
930
1050
|
fs.rmSync(lockPath, { force: true });
|
|
931
|
-
|
|
1051
|
+
try {
|
|
1052
|
+
store.writeRun(latest, run, "schedule.run.failed");
|
|
1053
|
+
}
|
|
1054
|
+
finally {
|
|
1055
|
+
this.arm(latest, store);
|
|
1056
|
+
}
|
|
932
1057
|
return run;
|
|
933
1058
|
}
|
|
934
1059
|
}
|
|
935
1060
|
finishRun(store, schedule, run, success, error) {
|
|
936
1061
|
const now = this.now();
|
|
937
1062
|
const next = nextRunAt(schedule);
|
|
1063
|
+
let skipped;
|
|
938
1064
|
if (next !== undefined && next <= now) {
|
|
939
1065
|
const planned = duePlannedAt(schedule, now);
|
|
940
|
-
|
|
1066
|
+
skipped = {
|
|
941
1067
|
schemaVersion: 1,
|
|
942
1068
|
id: this.randomId(),
|
|
943
1069
|
scheduleId: schedule.id,
|
|
@@ -946,8 +1072,7 @@ export class ScheduledRunManager {
|
|
|
946
1072
|
state: "skipped",
|
|
947
1073
|
completedAt: timestamp(now),
|
|
948
1074
|
};
|
|
949
|
-
schedule.trigger
|
|
950
|
-
store.writeRun(schedule, skipped, "schedule.skipped_overlap");
|
|
1075
|
+
schedule.trigger = nextAfter(schedule.trigger, planned, now);
|
|
951
1076
|
}
|
|
952
1077
|
if (run.asyncId)
|
|
953
1078
|
this.observedAsyncIds.delete(run.asyncId);
|
|
@@ -959,8 +1084,15 @@ export class ScheduledRunManager {
|
|
|
959
1084
|
schedule.updatedAt = timestamp(now);
|
|
960
1085
|
store.write(schedule);
|
|
961
1086
|
fs.rmSync(path.join(store.directory(schedule.id), "active.lock"), { force: true });
|
|
962
|
-
|
|
963
|
-
|
|
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
|
+
}
|
|
964
1096
|
}
|
|
965
1097
|
recordMissed(store, schedule, planned, dueReason) {
|
|
966
1098
|
const run = {
|
|
@@ -972,7 +1104,7 @@ export class ScheduledRunManager {
|
|
|
972
1104
|
state: "missed",
|
|
973
1105
|
completedAt: timestamp(this.now()),
|
|
974
1106
|
};
|
|
975
|
-
schedule.trigger
|
|
1107
|
+
schedule.trigger = nextAfter(schedule.trigger, planned, this.now());
|
|
976
1108
|
schedule.updatedAt = timestamp(this.now());
|
|
977
1109
|
store.write(schedule);
|
|
978
1110
|
store.writeRun(schedule, run, "schedule.missed");
|
|
@@ -1018,6 +1150,11 @@ export class ScheduledRunManager {
|
|
|
1018
1150
|
throw new Error("Schedule runtime context is unavailable.");
|
|
1019
1151
|
return ctx;
|
|
1020
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
|
+
}
|
|
1021
1158
|
timerKey(store, id) {
|
|
1022
1159
|
return `${store.root}\0${id}`;
|
|
1023
1160
|
}
|