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.
Files changed (237) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/docs/agents.md +15 -5
  3. package/docs/configuration.md +14 -2
  4. package/docs/extension-api.md +2 -2
  5. package/docs/missions.md +27 -3
  6. package/docs/models.md +6 -2
  7. package/docs/observability.md +8 -3
  8. package/docs/tool-reference.md +48 -8
  9. package/docs/workflows.md +3 -3
  10. package/package.json +2 -1
  11. package/src/agents/agent-memory.js +3 -3
  12. package/src/agents/agent-memory.js.map +1 -1
  13. package/src/agents/agent-serializer.d.ts.map +1 -1
  14. package/src/agents/agent-serializer.js +8 -1
  15. package/src/agents/agent-serializer.js.map +1 -1
  16. package/src/agents/agents.d.ts +1 -0
  17. package/src/agents/agents.d.ts.map +1 -1
  18. package/src/agents/agents.js +137 -95
  19. package/src/agents/agents.js.map +1 -1
  20. package/src/api/preflight.d.ts +1 -1
  21. package/src/api/preflight.d.ts.map +1 -1
  22. package/src/api/preflight.js +44 -1
  23. package/src/api/preflight.js.map +1 -1
  24. package/src/extension/config.d.ts.map +1 -1
  25. package/src/extension/config.js +4 -1
  26. package/src/extension/config.js.map +1 -1
  27. package/src/extension/index.d.ts.map +1 -1
  28. package/src/extension/index.js +62 -22
  29. package/src/extension/index.js.map +1 -1
  30. package/src/extension/public-execution.d.ts.map +1 -1
  31. package/src/extension/public-execution.js +4 -0
  32. package/src/extension/public-execution.js.map +1 -1
  33. package/src/extension/reply-workflow-script.d.ts.map +1 -1
  34. package/src/extension/reply-workflow-script.js +21 -12
  35. package/src/extension/reply-workflow-script.js.map +1 -1
  36. package/src/extension/rpc.js +1 -1
  37. package/src/extension/rpc.js.map +1 -1
  38. package/src/extension/schemas.d.ts +2 -1
  39. package/src/extension/schemas.d.ts.map +1 -1
  40. package/src/extension/schemas.js +13 -11
  41. package/src/extension/schemas.js.map +1 -1
  42. package/src/extension/tool-description.js +2 -2
  43. package/src/extension/tool-description.js.map +1 -1
  44. package/src/intercom/result-intercom.d.ts.map +1 -1
  45. package/src/intercom/result-intercom.js +2 -1
  46. package/src/intercom/result-intercom.js.map +1 -1
  47. package/src/missions/goal-driver.d.ts +1 -0
  48. package/src/missions/goal-driver.d.ts.map +1 -1
  49. package/src/missions/goal-driver.js +49 -34
  50. package/src/missions/goal-driver.js.map +1 -1
  51. package/src/missions/store.d.ts.map +1 -1
  52. package/src/missions/store.js +11 -1
  53. package/src/missions/store.js.map +1 -1
  54. package/src/profiles/profiles.d.ts +1 -1
  55. package/src/profiles/profiles.d.ts.map +1 -1
  56. package/src/profiles/profiles.js +29 -22
  57. package/src/profiles/profiles.js.map +1 -1
  58. package/src/runs/background/active-async-capacity.d.ts +15 -1
  59. package/src/runs/background/active-async-capacity.d.ts.map +1 -1
  60. package/src/runs/background/active-async-capacity.js +51 -6
  61. package/src/runs/background/active-async-capacity.js.map +1 -1
  62. package/src/runs/background/async-execution.d.ts +13 -0
  63. package/src/runs/background/async-execution.d.ts.map +1 -1
  64. package/src/runs/background/async-execution.js +89 -6
  65. package/src/runs/background/async-execution.js.map +1 -1
  66. package/src/runs/background/async-job-tracker.d.ts +4 -2
  67. package/src/runs/background/async-job-tracker.d.ts.map +1 -1
  68. package/src/runs/background/async-job-tracker.js +4 -3
  69. package/src/runs/background/async-job-tracker.js.map +1 -1
  70. package/src/runs/background/await-async-run.d.ts.map +1 -1
  71. package/src/runs/background/await-async-run.js +4 -2
  72. package/src/runs/background/await-async-run.js.map +1 -1
  73. package/src/runs/background/calendar-schedule.d.ts +27 -0
  74. package/src/runs/background/calendar-schedule.d.ts.map +1 -0
  75. package/src/runs/background/calendar-schedule.js +94 -0
  76. package/src/runs/background/calendar-schedule.js.map +1 -0
  77. package/src/runs/background/control-channel.d.ts +10 -0
  78. package/src/runs/background/control-channel.d.ts.map +1 -1
  79. package/src/runs/background/control-channel.js +51 -0
  80. package/src/runs/background/control-channel.js.map +1 -1
  81. package/src/runs/background/notify.d.ts +6 -0
  82. package/src/runs/background/notify.d.ts.map +1 -1
  83. package/src/runs/background/notify.js +24 -4
  84. package/src/runs/background/notify.js.map +1 -1
  85. package/src/runs/background/result-watcher.d.ts +5 -1
  86. package/src/runs/background/result-watcher.d.ts.map +1 -1
  87. package/src/runs/background/result-watcher.js +44 -7
  88. package/src/runs/background/result-watcher.js.map +1 -1
  89. package/src/runs/background/run-child-session.d.ts +2 -0
  90. package/src/runs/background/run-child-session.d.ts.map +1 -1
  91. package/src/runs/background/run-child-session.js +32 -7
  92. package/src/runs/background/run-child-session.js.map +1 -1
  93. package/src/runs/background/runner-aliases.d.ts +1 -0
  94. package/src/runs/background/runner-aliases.d.ts.map +1 -1
  95. package/src/runs/background/runner-aliases.js +6 -2
  96. package/src/runs/background/runner-aliases.js.map +1 -1
  97. package/src/runs/background/scheduled-runs.d.ts +5 -2
  98. package/src/runs/background/scheduled-runs.d.ts.map +1 -1
  99. package/src/runs/background/scheduled-runs.js +210 -56
  100. package/src/runs/background/scheduled-runs.js.map +1 -1
  101. package/src/runs/background/stale-run-reconciler.d.ts +1 -1
  102. package/src/runs/background/stale-run-reconciler.d.ts.map +1 -1
  103. package/src/runs/background/stale-run-reconciler.js +15 -2
  104. package/src/runs/background/stale-run-reconciler.js.map +1 -1
  105. package/src/runs/background/subagent-runner.d.ts +1 -0
  106. package/src/runs/background/subagent-runner.d.ts.map +1 -1
  107. package/src/runs/background/subagent-runner.js +58 -9
  108. package/src/runs/background/subagent-runner.js.map +1 -1
  109. package/src/runs/background/wait-completions.d.ts.map +1 -1
  110. package/src/runs/background/wait-completions.js +1 -0
  111. package/src/runs/background/wait-completions.js.map +1 -1
  112. package/src/runs/background/workflow-terminal-proof.d.ts +14 -1
  113. package/src/runs/background/workflow-terminal-proof.d.ts.map +1 -1
  114. package/src/runs/background/workflow-terminal-proof.js +41 -28
  115. package/src/runs/background/workflow-terminal-proof.js.map +1 -1
  116. package/src/runs/foreground/command-action.d.ts +13 -0
  117. package/src/runs/foreground/command-action.d.ts.map +1 -0
  118. package/src/runs/foreground/command-action.js +38 -0
  119. package/src/runs/foreground/command-action.js.map +1 -0
  120. package/src/runs/foreground/execution.d.ts.map +1 -1
  121. package/src/runs/foreground/execution.js +42 -8
  122. package/src/runs/foreground/execution.js.map +1 -1
  123. package/src/runs/foreground/foreground-history.d.ts.map +1 -1
  124. package/src/runs/foreground/foreground-history.js +1 -0
  125. package/src/runs/foreground/foreground-history.js.map +1 -1
  126. package/src/runs/foreground/subagent-executor.d.ts +2 -1
  127. package/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  128. package/src/runs/foreground/subagent-executor.js +108 -17
  129. package/src/runs/foreground/subagent-executor.js.map +1 -1
  130. package/src/runs/shared/child-commands.d.ts +28 -0
  131. package/src/runs/shared/child-commands.d.ts.map +1 -0
  132. package/src/runs/shared/child-commands.js +250 -0
  133. package/src/runs/shared/child-commands.js.map +1 -0
  134. package/src/runs/shared/child-session.d.ts +4 -0
  135. package/src/runs/shared/child-session.d.ts.map +1 -1
  136. package/src/runs/shared/child-session.js +107 -22
  137. package/src/runs/shared/child-session.js.map +1 -1
  138. package/src/runs/shared/child-tool-plan.js +1 -1
  139. package/src/runs/shared/child-tool-plan.js.map +1 -1
  140. package/src/runs/shared/claude-code-adapter.d.ts +56 -0
  141. package/src/runs/shared/claude-code-adapter.d.ts.map +1 -1
  142. package/src/runs/shared/claude-code-adapter.js +138 -2
  143. package/src/runs/shared/claude-code-adapter.js.map +1 -1
  144. package/src/runs/shared/dynamic-fanout.js +1 -1
  145. package/src/runs/shared/dynamic-fanout.js.map +1 -1
  146. package/src/runs/shared/external-cli-preflight.d.ts +10 -0
  147. package/src/runs/shared/external-cli-preflight.d.ts.map +1 -1
  148. package/src/runs/shared/external-cli-preflight.js +50 -6
  149. package/src/runs/shared/external-cli-preflight.js.map +1 -1
  150. package/src/runs/shared/external-cli-runner.d.ts.map +1 -1
  151. package/src/runs/shared/external-cli-runner.js +4 -2
  152. package/src/runs/shared/external-cli-runner.js.map +1 -1
  153. package/src/runs/shared/mcp-direct-tool-allowlist.d.ts.map +1 -1
  154. package/src/runs/shared/mcp-direct-tool-allowlist.js +8 -5
  155. package/src/runs/shared/mcp-direct-tool-allowlist.js.map +1 -1
  156. package/src/runs/shared/model-resolution.d.ts +6 -2
  157. package/src/runs/shared/model-resolution.d.ts.map +1 -1
  158. package/src/runs/shared/model-resolution.js +9 -2
  159. package/src/runs/shared/model-resolution.js.map +1 -1
  160. package/src/runs/shared/parallel-handoff.d.ts +10 -4
  161. package/src/runs/shared/parallel-handoff.d.ts.map +1 -1
  162. package/src/runs/shared/parallel-handoff.js +53 -6
  163. package/src/runs/shared/parallel-handoff.js.map +1 -1
  164. package/src/runs/shared/parallel-utils.d.ts +2 -0
  165. package/src/runs/shared/parallel-utils.d.ts.map +1 -1
  166. package/src/runs/shared/parallel-utils.js.map +1 -1
  167. package/src/runs/shared/partial-output.d.ts +18 -0
  168. package/src/runs/shared/partial-output.d.ts.map +1 -0
  169. package/src/runs/shared/partial-output.js +29 -0
  170. package/src/runs/shared/partial-output.js.map +1 -0
  171. package/src/runs/shared/run-history.d.ts +47 -0
  172. package/src/runs/shared/run-history.d.ts.map +1 -1
  173. package/src/runs/shared/run-history.js +70 -0
  174. package/src/runs/shared/run-history.js.map +1 -1
  175. package/src/runs/shared/subagent-control.d.ts.map +1 -1
  176. package/src/runs/shared/subagent-control.js +30 -17
  177. package/src/runs/shared/subagent-control.js.map +1 -1
  178. package/src/runs/shared/subagent-prompt-runtime.d.ts +1 -0
  179. package/src/runs/shared/subagent-prompt-runtime.d.ts.map +1 -1
  180. package/src/runs/shared/subagent-prompt-runtime.js +12 -5
  181. package/src/runs/shared/subagent-prompt-runtime.js.map +1 -1
  182. package/src/runs/shared/worktree-cleanup-apply.d.ts +37 -0
  183. package/src/runs/shared/worktree-cleanup-apply.d.ts.map +1 -0
  184. package/src/runs/shared/worktree-cleanup-apply.js +179 -0
  185. package/src/runs/shared/worktree-cleanup-apply.js.map +1 -0
  186. package/src/runs/shared/worktree-cleanup-plan.d.ts +7 -0
  187. package/src/runs/shared/worktree-cleanup-plan.d.ts.map +1 -1
  188. package/src/runs/shared/worktree-cleanup-plan.js +41 -19
  189. package/src/runs/shared/worktree-cleanup-plan.js.map +1 -1
  190. package/src/runs/shared/worktree-lock.d.ts +6 -0
  191. package/src/runs/shared/worktree-lock.d.ts.map +1 -0
  192. package/src/runs/shared/worktree-lock.js +41 -0
  193. package/src/runs/shared/worktree-lock.js.map +1 -0
  194. package/src/runs/shared/worktree.d.ts +3 -0
  195. package/src/runs/shared/worktree.d.ts.map +1 -1
  196. package/src/runs/shared/worktree.js +9 -1
  197. package/src/runs/shared/worktree.js.map +1 -1
  198. package/src/shared/file-lease.d.ts +5 -0
  199. package/src/shared/file-lease.d.ts.map +1 -0
  200. package/src/shared/file-lease.js +101 -0
  201. package/src/shared/file-lease.js.map +1 -0
  202. package/src/shared/required-child-extensions.d.ts +16 -1
  203. package/src/shared/required-child-extensions.d.ts.map +1 -1
  204. package/src/shared/required-child-extensions.js +37 -5
  205. package/src/shared/required-child-extensions.js.map +1 -1
  206. package/src/shared/settings-file-lease.d.ts +5 -0
  207. package/src/shared/settings-file-lease.d.ts.map +1 -0
  208. package/src/shared/settings-file-lease.js +39 -0
  209. package/src/shared/settings-file-lease.js.map +1 -0
  210. package/src/shared/types.d.ts +16 -1
  211. package/src/shared/types.d.ts.map +1 -1
  212. package/src/shared/types.js +1 -1
  213. package/src/shared/types.js.map +1 -1
  214. package/src/slash/subagent-cost.d.ts.map +1 -1
  215. package/src/slash/subagent-cost.js +8 -4
  216. package/src/slash/subagent-cost.js.map +1 -1
  217. package/src/tui/render.d.ts +1 -1
  218. package/src/tui/render.d.ts.map +1 -1
  219. package/src/tui/render.js +11 -6
  220. package/src/tui/render.js.map +1 -1
  221. package/src/watchdog/diff-tool.js +2 -2
  222. package/src/watchdog/diff-tool.js.map +1 -1
  223. package/src/watchdog/register-main.js +1 -2
  224. package/src/watchdog/register-main.js.map +1 -1
  225. package/src/watchdog/runtime.d.ts +3 -3
  226. package/src/watchdog/runtime.d.ts.map +1 -1
  227. package/src/watchdog/runtime.js +7 -3
  228. package/src/watchdog/runtime.js.map +1 -1
  229. package/src/watchdog/settings.d.ts.map +1 -1
  230. package/src/watchdog/settings.js +22 -17
  231. package/src/watchdog/settings.js.map +1 -1
  232. package/src/workflows/workflow-receipt.js +2 -2
  233. package/src/workflows/workflow-receipt.js.map +1 -1
  234. package/src/workflows/workflow-resources.d.ts +4 -0
  235. package/src/workflows/workflow-resources.d.ts.map +1 -1
  236. package/src/workflows/workflow-resources.js +7 -4
  237. 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
- return { ...record, target: parseScheduleTarget(record.target, file) };
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
- const runs = [run, ...this.history(schedule.id).filter((item) => item.id !== run.id)].slice(0, MAX_HISTORY);
352
- writePrivateAtomicJson(path.join(dir, "history.json"), { schemaVersion: 1, runs });
353
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
354
- 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 });
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
- let next = plannedAt + trigger.everyMs;
373
- while (next <= now)
374
- next += trigger.everyMs;
375
- return timestamp(next);
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" || schedule.trigger.kind !== "interval")
424
+ if (next === undefined || next > now || schedule.catchUp !== "latest")
389
425
  return next;
390
- return next + Math.floor((now - next) / schedule.trigger.everyMs) * schedule.trigger.everyMs;
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 run = store.history(id).find((item) => item.asyncId === asyncId && item.state === "running");
576
- if (!run)
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
- if (Boolean(at) === Boolean(every))
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.missionId !== undefined || params.mission !== undefined || params.missionUpdate !== undefined || params.missionStatus !== undefined || params.missionScope !== undefined)
601
- return textResult("Mission attachment is deferred from this first schedule slice.", undefined, undefined, true);
602
- if (params.on !== undefined || params.timezone !== undefined || every === "day" || every === "week" || every === "month" || every === "year")
603
- return textResult("Calendar schedules are deferred from this first safe slice. Use a fixed interval such as every:'24h' or every:'7d'.", undefined, undefined, true);
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 (at) {
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
- schemaVersion: 1,
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 === "interval" && params.quiet === true ? { quiet: true } : {}),
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: ${at ? `at ${at}` : `every ${every}`}\nSession only: ${schedule.sessionOnly === true ? "yes" : "no"}\nQuiet: ${schedule.quiet === true ? "yes" : "no"}\nNext: ${schedule.trigger.nextRunAt}\nTarget: ${targetLabel(schedule.target)}`, [schedule]);
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 === "interval")
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 = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);
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 = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);
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 === "interval" ? duePlannedAt(schedule, now) : undefined;
820
- const notBefore = planned !== undefined && planned <= now ? Date.parse(nextAfter(schedule.trigger, planned, now)) : undefined;
821
- this.restoreOne(store, schedule, notBefore, schedule.trigger.kind === "interval");
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.nextRunAt = nextAfter(schedule.trigger, planned, now);
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
- if (advance) {
875
- schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);
876
- schedule.updatedAt = timestamp(now);
877
- store.write(schedule);
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.nextRunAt = nextAfter(schedule.trigger, planned, now);
991
+ schedule.trigger = nextAfter(schedule.trigger, planned, now);
887
992
  schedule.updatedAt = timestamp(now);
888
- store.write(schedule);
889
- store.writeRun(schedule, run, "schedule.run.started");
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.nextRunAt = nextRunAtBeforeClaim;
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
- this.arm(latest, store);
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
- const skipped = {
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.nextRunAt = nextAfter(schedule.trigger, planned, now);
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
- store.writeRun(schedule, run, success ? "schedule.run.completed" : "schedule.run.failed");
946
- this.arm(schedule, store);
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.nextRunAt = nextAfter(schedule.trigger, planned, this.now());
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
  }