@duckmind/dm-windows-x64 0.61.4 → 0.61.9
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +211 -67
- package/extensions/dm-subagents/agents/claude-code-writer.md +15 -0
- package/extensions/dm-subagents/agents/claude-code.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec-writer.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec.md +15 -0
- package/extensions/dm-subagents/agents/cursor-agent-writer.md +14 -0
- package/extensions/dm-subagents/agents/cursor-agent.md +14 -0
- package/extensions/dm-subagents/agents/delegate.md +3 -2
- package/extensions/dm-subagents/agents/oracle.md +10 -5
- package/extensions/dm-subagents/agents/researcher.md +2 -2
- package/extensions/dm-subagents/agents/reviewer.md +17 -7
- package/extensions/dm-subagents/agents/scout.md +5 -5
- package/extensions/dm-subagents/agents/worker.md +6 -2
- package/extensions/dm-subagents/async-retention-discovery-worker.mjs +167 -0
- package/extensions/dm-subagents/index.js +4 -0
- package/extensions/dm-subagents/inspector-runner.mjs +10 -0
- package/extensions/dm-subagents/install.mjs +3 -2
- package/extensions/dm-subagents/package.json +2 -2
- package/extensions/dm-subagents/prompts/council.md +60 -0
- package/extensions/dm-subagents/prompts/parallel-review.md +5 -1
- package/extensions/dm-subagents/prompts/review-loop.md +13 -7
- package/extensions/dm-subagents/skills/council-mode/SKILL.md +59 -0
- package/extensions/dm-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/extensions/dm-subagents/skills/dm-subagents/SKILL.md +96 -911
- package/extensions/dm-subagents/skills/dm-subagents/references/constraints-and-recipes.md +70 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/execution-controls.md +539 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/management-authoring-rpc.md +161 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/multi-lane-orchestration.md +51 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/prompting-and-roles.md +295 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/review-and-validation.md +73 -0
- package/extensions/dm-subagents/src/agents/agent-management.js +716 -484
- package/extensions/dm-subagents/src/agents/agent-refinements.js +563 -0
- package/extensions/dm-subagents/src/agents/agent-serializer.js +70 -5
- package/extensions/dm-subagents/src/agents/agents.js +1447 -299
- package/extensions/dm-subagents/src/agents/builtin-names.js +15 -0
- package/extensions/dm-subagents/src/agents/chain-serializer.js +12 -7
- package/extensions/dm-subagents/src/agents/frontmatter.js +64 -14
- package/extensions/dm-subagents/src/agents/identity.js +1 -1
- package/extensions/dm-subagents/src/agents/proactive-skills.js +14 -11
- package/extensions/dm-subagents/src/agents/runtime-agent-events.js +49 -0
- package/extensions/dm-subagents/src/agents/runtime-agent-registry.js +412 -0
- package/extensions/dm-subagents/src/agents/skills.js +52 -35
- package/extensions/dm-subagents/src/api/agents.js +6 -0
- package/extensions/dm-subagents/src/api/background-work.js +151 -0
- package/extensions/dm-subagents/src/api/capability-ceiling.js +12 -0
- package/extensions/dm-subagents/src/api/control-channel.js +3 -0
- package/extensions/dm-subagents/src/api/delegation.js +5 -0
- package/extensions/dm-subagents/src/api/dm-args.js +3 -0
- package/extensions/dm-subagents/src/api/external-job-provider.js +137 -0
- package/extensions/dm-subagents/src/api/external-runs.js +233 -0
- package/extensions/dm-subagents/src/api/intercom-bridge.js +3 -0
- package/extensions/dm-subagents/src/api/preflight.js +322 -0
- package/extensions/dm-subagents/src/api/project-panes.js +11 -0
- package/extensions/dm-subagents/src/api/shared-types.js +4 -0
- package/extensions/dm-subagents/src/extension/config.js +175 -0
- package/extensions/dm-subagents/src/extension/control-notices.js +4 -43
- package/extensions/dm-subagents/src/extension/doctor.js +71 -17
- package/extensions/dm-subagents/src/extension/fanout-child.js +49 -32
- package/extensions/dm-subagents/src/extension/index.js +711 -265
- package/extensions/dm-subagents/src/extension/public-execution.js +114 -0
- package/extensions/dm-subagents/src/extension/rpc.js +427 -22
- package/extensions/dm-subagents/src/extension/schemas.js +158 -65
- package/extensions/dm-subagents/src/extension/steering-notices.js +23 -0
- package/extensions/dm-subagents/src/extension/subagent-guide.js +31 -0
- package/extensions/dm-subagents/src/extension/tool-description.js +92 -74
- package/extensions/dm-subagents/src/extension/tool-result.js +7 -0
- package/extensions/dm-subagents/src/inspectors/herdr/actions.js +218 -0
- package/extensions/dm-subagents/src/inspectors/herdr/client.js +123 -0
- package/extensions/dm-subagents/src/inspectors/herdr/focus.js +47 -0
- package/extensions/dm-subagents/src/inspectors/herdr/inspector-runner.js +160 -0
- package/extensions/dm-subagents/src/inspectors/herdr/project-panes.js +618 -0
- package/extensions/dm-subagents/src/inspectors/herdr/session-roots-codec.js +21 -0
- package/extensions/dm-subagents/src/inspectors/herdr/shell-command.js +15 -0
- package/extensions/dm-subagents/src/integrations/herdr-status.js +377 -0
- package/extensions/dm-subagents/src/intercom/intercom-bridge.js +17 -13
- package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +371 -79
- package/extensions/dm-subagents/src/intercom/result-intercom.js +47 -7
- package/extensions/dm-subagents/src/missions/actions.js +394 -0
- package/extensions/dm-subagents/src/missions/goal-driver.js +149 -0
- package/extensions/dm-subagents/src/missions/lifecycle.js +331 -0
- package/extensions/dm-subagents/src/missions/store.js +548 -0
- package/extensions/dm-subagents/src/missions/types.js +9 -0
- package/extensions/dm-subagents/src/missions/workflow-state.js +245 -0
- package/extensions/dm-subagents/src/policy/authority.js +37 -0
- package/extensions/dm-subagents/src/profiles/profiles.js +36 -18
- package/extensions/dm-subagents/src/runs/background/active-async-capacity.js +427 -0
- package/extensions/dm-subagents/src/runs/background/active-run-index.js +122 -0
- package/extensions/dm-subagents/src/runs/background/async-execution.js +925 -137
- package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +515 -148
- package/extensions/dm-subagents/src/runs/background/async-resume.js +378 -51
- package/extensions/dm-subagents/src/runs/background/async-retention.js +828 -0
- package/extensions/dm-subagents/src/runs/background/async-status-snapshot.js +31 -0
- package/extensions/dm-subagents/src/runs/background/async-status.js +259 -22
- package/extensions/dm-subagents/src/runs/background/auto-drain.js +46 -0
- package/extensions/dm-subagents/src/runs/background/chain-append.js +50 -15
- package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +67 -12
- package/extensions/dm-subagents/src/runs/background/completion-batcher.js +5 -1
- package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +3 -11
- package/extensions/dm-subagents/src/runs/background/completion-replay.js +245 -0
- package/extensions/dm-subagents/src/runs/background/control-channel.js +423 -36
- package/extensions/dm-subagents/src/runs/background/fleet-view.js +132 -59
- package/extensions/dm-subagents/src/runs/background/index-segment.js +38 -0
- package/extensions/dm-subagents/src/runs/background/inspect-rpc.js +373 -0
- package/extensions/dm-subagents/src/runs/background/notify.js +357 -57
- package/extensions/dm-subagents/src/runs/background/owned-process-tree.js +86 -0
- package/extensions/dm-subagents/src/runs/background/process-terminal.js +269 -0
- package/extensions/dm-subagents/src/runs/background/result-delivery-ownership.js +34 -0
- package/extensions/dm-subagents/src/runs/background/result-files.js +469 -0
- package/extensions/dm-subagents/src/runs/background/result-watcher.js +540 -75
- package/extensions/dm-subagents/src/runs/background/resume-guidance.js +44 -0
- package/extensions/dm-subagents/src/runs/background/retained-children.js +119 -0
- package/extensions/dm-subagents/src/runs/background/run-id-query.js +5 -0
- package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +93 -9
- package/extensions/dm-subagents/src/runs/background/run-status.js +303 -32
- package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +784 -376
- package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +75 -34
- package/extensions/dm-subagents/src/runs/background/steering.js +221 -0
- package/extensions/dm-subagents/src/runs/background/subagent-runner.js +3106 -844
- package/extensions/dm-subagents/src/runs/background/subagent-wait.js +529 -0
- package/extensions/dm-subagents/src/runs/background/terminal-run-index.js +106 -0
- package/extensions/dm-subagents/src/runs/background/top-level-async.js +1 -1
- package/extensions/dm-subagents/src/runs/background/wait-completions.js +155 -0
- package/extensions/dm-subagents/src/runs/background/wait-config.js +46 -0
- package/extensions/dm-subagents/src/runs/background/wait-subscriptions.js +278 -0
- package/extensions/dm-subagents/src/runs/background/wait-tool.js +47 -0
- package/extensions/dm-subagents/src/runs/foreground/async-dismiss-action.js +81 -0
- package/extensions/dm-subagents/src/runs/foreground/async-steering-action.js +245 -0
- package/extensions/dm-subagents/src/runs/foreground/async-stop-action.js +74 -0
- package/extensions/dm-subagents/src/runs/foreground/execution.js +1401 -342
- package/extensions/dm-subagents/src/runs/foreground/foreground-control.js +133 -0
- package/extensions/dm-subagents/src/runs/foreground/foreground-history.js +148 -0
- package/extensions/dm-subagents/src/runs/foreground/prompt-audit.js +139 -0
- package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +4278 -1441
- package/extensions/dm-subagents/src/runs/foreground/workflow-detach-reconcile.js +278 -0
- package/extensions/dm-subagents/src/runs/foreground/workflow-foreground-steering.js +155 -0
- package/extensions/dm-subagents/src/runs/shared/abort-recovery.js +97 -0
- package/extensions/dm-subagents/src/runs/shared/acceptance.js +597 -148
- package/extensions/dm-subagents/src/runs/shared/agent-contract.js +35 -0
- package/extensions/dm-subagents/src/runs/shared/async-status-projection.js +472 -0
- package/extensions/dm-subagents/src/runs/shared/background-process-options.js +6 -0
- package/extensions/dm-subagents/src/runs/shared/capability-ceiling.js +175 -0
- package/extensions/dm-subagents/src/runs/shared/child-identity.js +32 -0
- package/extensions/dm-subagents/src/runs/shared/child-launch-plan.js +65 -0
- package/extensions/dm-subagents/src/runs/shared/child-protocol.js +447 -0
- package/extensions/dm-subagents/src/runs/shared/claude-code-adapter.js +120 -0
- package/extensions/dm-subagents/src/runs/shared/codex-exec-adapter.js +129 -0
- package/extensions/dm-subagents/src/runs/shared/completion-evidence.js +40 -0
- package/extensions/dm-subagents/src/runs/shared/completion-guard.js +140 -83
- package/extensions/dm-subagents/src/runs/shared/context-mode.js +38 -0
- package/extensions/dm-subagents/src/runs/shared/cursor-agent-adapter.js +101 -0
- package/extensions/dm-subagents/src/runs/shared/dm-args.js +445 -72
- package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +27 -16
- package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +19 -6
- package/extensions/dm-subagents/src/runs/shared/extension-bindings.js +81 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-contract.js +134 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-preflight.js +98 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-runner.js +419 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-bridge.js +404 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-runner.js +334 -0
- package/extensions/dm-subagents/src/runs/shared/fast-mode-extension.js +8 -0
- package/extensions/dm-subagents/src/runs/shared/host-step-status.js +228 -0
- package/extensions/dm-subagents/src/runs/shared/lane-metadata.js +104 -0
- package/extensions/dm-subagents/src/runs/shared/launch-cwd.js +17 -0
- package/extensions/dm-subagents/src/runs/shared/llm-intent-arbiter.js +190 -0
- package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +48 -3
- package/extensions/dm-subagents/src/runs/shared/mcp-config-sources.js +387 -0
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +212 -137
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-grant.js +131 -0
- package/extensions/dm-subagents/src/runs/shared/model-exclusions.js +207 -0
- package/extensions/dm-subagents/src/runs/shared/model-fallback.js +225 -55
- package/extensions/dm-subagents/src/runs/shared/model-scope.js +85 -28
- package/extensions/dm-subagents/src/runs/shared/mutation-evidence.js +182 -0
- package/extensions/dm-subagents/src/runs/shared/nested-events.js +264 -102
- package/extensions/dm-subagents/src/runs/shared/nested-render.js +15 -5
- package/extensions/dm-subagents/src/runs/shared/orca-progress-tabs.js +505 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-handoff.js +653 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +29 -12
- package/extensions/dm-subagents/src/runs/shared/permissions.js +108 -0
- package/extensions/dm-subagents/src/runs/shared/process-signal.js +13 -0
- package/extensions/dm-subagents/src/runs/shared/run-fanout-budget.js +257 -0
- package/extensions/dm-subagents/src/runs/shared/run-history.js +133 -13
- package/extensions/dm-subagents/src/runs/shared/runtime-acknowledged-extensions.js +62 -0
- package/extensions/dm-subagents/src/runs/shared/session-lease.js +225 -0
- package/extensions/dm-subagents/src/runs/shared/single-output.js +129 -27
- package/extensions/dm-subagents/src/runs/shared/spawn-budget.js +95 -0
- package/extensions/dm-subagents/src/runs/shared/structured-output.js +129 -10
- package/extensions/dm-subagents/src/runs/shared/subagent-control.js +66 -11
- package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +548 -70
- package/extensions/dm-subagents/src/runs/shared/subagent-startup-retry.js +50 -0
- package/extensions/dm-subagents/src/runs/shared/task-intent.js +130 -0
- package/extensions/dm-subagents/src/runs/shared/tool-availability.js +59 -0
- package/extensions/dm-subagents/src/runs/shared/tool-budget.js +7 -5
- package/extensions/dm-subagents/src/runs/shared/tool-timeout.js +64 -0
- package/extensions/dm-subagents/src/runs/shared/usage-budget.js +74 -0
- package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +22 -0
- package/extensions/dm-subagents/src/runs/shared/worktree-cleanup-plan.js +721 -0
- package/extensions/dm-subagents/src/runs/shared/worktree.js +168 -19
- package/extensions/dm-subagents/src/shared/accessible-dir.js +35 -0
- package/extensions/dm-subagents/src/shared/agent-stream-options.js +3 -0
- package/extensions/dm-subagents/src/shared/artifacts.js +170 -11
- package/extensions/dm-subagents/src/shared/atomic-json.js +36 -38
- package/extensions/dm-subagents/src/shared/capacity-resilient-json.js +77 -0
- package/extensions/dm-subagents/src/shared/child-session-name.js +15 -0
- package/extensions/dm-subagents/src/shared/child-transcript.js +57 -2
- package/extensions/dm-subagents/src/shared/completion-owner.js +7 -0
- package/extensions/dm-subagents/src/shared/display-text.js +142 -0
- package/extensions/dm-subagents/src/shared/extension-context.js +17 -0
- package/extensions/dm-subagents/src/shared/file-coalescer.js +9 -0
- package/extensions/dm-subagents/src/shared/file-system-retry.js +56 -0
- package/extensions/dm-subagents/src/shared/fork-context.js +96 -33
- package/extensions/dm-subagents/src/shared/formatters.js +21 -7
- package/extensions/dm-subagents/src/shared/launch-contract.js +94 -0
- package/extensions/dm-subagents/src/shared/model-info.js +10 -5
- package/extensions/dm-subagents/src/shared/node-executable.js +19 -0
- package/extensions/dm-subagents/src/shared/prompt-resources.js +10 -0
- package/extensions/dm-subagents/src/shared/pruned-fork.js +427 -0
- package/extensions/dm-subagents/src/shared/session-file-trust.js +19 -0
- package/extensions/dm-subagents/src/shared/session-tokens.js +14 -3
- package/extensions/dm-subagents/src/shared/settings.js +39 -47
- package/extensions/dm-subagents/src/shared/shortcuts.js +16 -0
- package/extensions/dm-subagents/src/shared/status-format.js +9 -2
- package/extensions/dm-subagents/src/shared/thinking-ceiling.js +41 -0
- package/extensions/dm-subagents/src/shared/types.js +47 -7
- package/extensions/dm-subagents/src/shared/utf8.js +12 -0
- package/extensions/dm-subagents/src/shared/utils.js +151 -131
- package/extensions/dm-subagents/src/shared/watch-strategy.js +3 -0
- package/extensions/dm-subagents/src/shared/workflow-child-permit.js +84 -0
- package/extensions/dm-subagents/src/slash/delegation-adapters.js +274 -0
- package/extensions/dm-subagents/src/slash/delegation-json.js +113 -0
- package/extensions/dm-subagents/src/slash/delegation-request.js +152 -0
- package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +294 -243
- package/extensions/dm-subagents/src/slash/prompt-workflows.js +35 -73
- package/extensions/dm-subagents/src/slash/selector.js +101 -0
- package/extensions/dm-subagents/src/slash/slash-bridge.js +17 -1
- package/extensions/dm-subagents/src/slash/slash-commands.js +722 -732
- package/extensions/dm-subagents/src/slash/slash-live-state.js +37 -19
- package/extensions/dm-subagents/src/slash/subagents-admin.js +410 -0
- package/extensions/dm-subagents/src/tui/fleet-status.js +824 -0
- package/extensions/dm-subagents/src/tui/fleet-transcript.js +479 -0
- package/extensions/dm-subagents/src/tui/fleet.js +1326 -0
- package/extensions/dm-subagents/src/tui/render-helpers.js +22 -0
- package/extensions/dm-subagents/src/tui/render.js +1511 -261
- package/extensions/dm-subagents/src/watchdog/change-signature.js +220 -0
- package/extensions/dm-subagents/src/watchdog/child-status.js +151 -0
- package/extensions/dm-subagents/src/watchdog/emission-guard.js +90 -0
- package/extensions/dm-subagents/src/watchdog/lsp-diagnostics.js +484 -0
- package/extensions/dm-subagents/src/watchdog/model-selection.js +154 -0
- package/extensions/dm-subagents/src/watchdog/permission-arbiter.js +138 -0
- package/extensions/dm-subagents/src/watchdog/register-child.js +112 -0
- package/extensions/dm-subagents/src/watchdog/register-main.js +419 -0
- package/extensions/dm-subagents/src/watchdog/render.js +54 -0
- package/extensions/dm-subagents/src/watchdog/review.js +251 -0
- package/extensions/dm-subagents/src/watchdog/runtime.js +803 -0
- package/extensions/dm-subagents/src/watchdog/scope.js +56 -0
- package/extensions/dm-subagents/src/watchdog/settings.js +515 -0
- package/extensions/dm-subagents/src/watchdog/tool-actions.js +151 -0
- package/extensions/dm-subagents/src/watchdog/turn-delta.js +169 -0
- package/extensions/dm-subagents/src/watchdog/types.js +29 -0
- package/extensions/dm-subagents/src/watchdog/warning-format.js +58 -0
- package/extensions/dm-subagents/src/workflows/chat-progress.js +116 -0
- package/extensions/dm-subagents/src/workflows/host-command.js +227 -0
- package/extensions/dm-subagents/src/workflows/scripted-workflow.js +2011 -0
- package/extensions/dm-subagents/src/workflows/workflow-child-summary.js +116 -0
- package/extensions/dm-subagents/src/workflows/workflow-preflight.js +243 -0
- package/extensions/dm-subagents/src/workflows/workflow-receipt.js +387 -0
- package/extensions/dm-subagents/src/workflows/workflow-settlement.js +189 -0
- package/package.json +4 -3
- package/extensions/dm-fff/package.json +0 -21
- package/extensions/dm-fff/src/index.js +0 -691
- package/extensions/dm-fff/src/query.js +0 -60
- package/extensions/dm-subagents/agents/context-builder.md +0 -46
- package/extensions/dm-subagents/agents/planner.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-context-build.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-handoff-plan.md +0 -61
- package/extensions/dm-subagents/src/runs/background/wait.js +0 -206
- package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +0 -1013
- package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +0 -981
- package/extensions/dm-subagents/src/runs/shared/turn-budget.js +0 -50
|
@@ -1,480 +1,888 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
} from "../../
|
|
10
|
-
|
|
11
|
-
export const SCHEDULED_RUN_ACTIONS = [
|
|
5
|
+
import { getProjectSubagentsDir } from "../../shared/artifacts.js";
|
|
6
|
+
import { writePrivateAtomicJson } from "../../shared/atomic-json.js";
|
|
7
|
+
import { shortenPath } from "../../shared/formatters.js";
|
|
8
|
+
import { validateExecutionAcceptance } from "../shared/acceptance.js";
|
|
9
|
+
import { previewSimpleWorkflowRun } from "../../workflows/scripted-workflow.js";
|
|
10
|
+
import { resolveGitRepositoryIdentity } from "../../workflows/chat-progress.js";
|
|
11
|
+
import { getConfigDirName } from "../../shared/utils.js";
|
|
12
|
+
export const SCHEDULED_RUN_ACTIONS = [
|
|
13
|
+
"schedule.create",
|
|
14
|
+
"schedule.list",
|
|
15
|
+
"schedule.show",
|
|
16
|
+
"schedule.history",
|
|
17
|
+
"schedule.pause",
|
|
18
|
+
"schedule.resume",
|
|
19
|
+
"schedule.run",
|
|
20
|
+
"schedule.run-due",
|
|
21
|
+
"schedule.delete"
|
|
22
|
+
];
|
|
12
23
|
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
13
|
-
const DEFAULT_MAX_LATENESS_MS = 5 * 60 * 1000;
|
|
14
24
|
const DEFAULT_MAX_PENDING = 20;
|
|
25
|
+
const MAX_HISTORY = 100;
|
|
26
|
+
const STALE_LAUNCH_CLAIM_MS = 5 * 60000;
|
|
27
|
+
const SCHEDULE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
15
28
|
export function isScheduledRunAction(action) {
|
|
16
29
|
return typeof action === "string" && SCHEDULED_RUN_ACTIONS.includes(action);
|
|
17
30
|
}
|
|
18
31
|
export function scheduledRunsEnabled(config) {
|
|
19
|
-
return config.scheduledRuns?.enabled
|
|
32
|
+
return config.scheduledRuns?.enabled !== false;
|
|
20
33
|
}
|
|
21
|
-
export function scheduledRunStorePath(cwd,
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
export function scheduledRunStorePath(cwd, _sessionId, root) {
|
|
35
|
+
if (!root)
|
|
36
|
+
return path.join(getProjectSubagentsDir(path.resolve(cwd)), "schedules");
|
|
37
|
+
const projectKey = createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 20);
|
|
38
|
+
return path.join(root, projectKey);
|
|
24
39
|
}
|
|
25
|
-
export function parseScheduledRunTime(
|
|
26
|
-
const trimmed =
|
|
40
|
+
export function parseScheduledRunTime(at, now = Date.now()) {
|
|
41
|
+
const trimmed = at.trim();
|
|
27
42
|
const relative = trimmed.match(/^\+(\d+)(s|m|h|d)$/);
|
|
28
43
|
if (relative) {
|
|
29
44
|
const amount = Number(relative[1]);
|
|
30
45
|
if (!Number.isSafeInteger(amount) || amount < 1)
|
|
31
|
-
throw new Error(`Invalid
|
|
46
|
+
throw new Error(`Invalid at value "${at}". Relative delays must be positive, such as "+10m".`);
|
|
32
47
|
const unitMs = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[relative[2]];
|
|
33
|
-
const
|
|
34
|
-
if (!Number.isSafeInteger(
|
|
35
|
-
throw new Error(`Invalid
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
const result = now + amount * unitMs;
|
|
49
|
+
if (!Number.isSafeInteger(result))
|
|
50
|
+
throw new Error(`Invalid at value "${at}". Relative delay is too large.`);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
const iso = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/);
|
|
54
|
+
if (!iso)
|
|
55
|
+
throw new Error(`Invalid at value "${at}". Use a one-shot delay such as "+10m" or an ISO timestamp with timezone.`);
|
|
56
|
+
const year = Number(iso[1]);
|
|
57
|
+
const month = Number(iso[2]);
|
|
58
|
+
const day = Number(iso[3]);
|
|
59
|
+
const hour = Number(iso[4]);
|
|
60
|
+
const minute = Number(iso[5]);
|
|
61
|
+
const second = iso[6] === undefined ? 0 : Number(iso[6]);
|
|
62
|
+
const zone = iso[7];
|
|
63
|
+
const offsetHour = zone === "Z" ? 0 : Number(zone.slice(1, 3));
|
|
64
|
+
const offsetMinute = zone === "Z" ? 0 : Number(zone.slice(4, 6));
|
|
65
|
+
const daysInMonth = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
|
|
66
|
+
const parsed = Date.parse(trimmed);
|
|
67
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59 || !Number.isFinite(parsed))
|
|
68
|
+
throw new Error(`Invalid at value "${at}". Use a valid ISO timestamp.`);
|
|
69
|
+
if (parsed <= now)
|
|
70
|
+
throw new Error(`Scheduled time ${new Date(parsed).toISOString()} is in the past.`);
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
export function parseScheduleInterval(every) {
|
|
74
|
+
const match = every.trim().match(/^(\d+)(m|h|d|w)$/);
|
|
75
|
+
if (!match)
|
|
76
|
+
throw new Error(`Invalid every value "${every}". This first recurring slice supports fixed intervals such as "30m", "6h", "2d", or "2w".`);
|
|
77
|
+
const amount = Number(match[1]);
|
|
78
|
+
if (!Number.isSafeInteger(amount) || amount < 1)
|
|
79
|
+
throw new Error(`Invalid every value "${every}". Interval must be positive.`);
|
|
80
|
+
const unitMs = { m: 60000, h: 3600000, d: 86400000, w: 604800000 }[match[2]];
|
|
81
|
+
const result = amount * unitMs;
|
|
82
|
+
if (!Number.isSafeInteger(result))
|
|
83
|
+
throw new Error(`Invalid every value "${every}". Interval is too large.`);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function timestamp(value) {
|
|
87
|
+
return new Date(value).toISOString();
|
|
88
|
+
}
|
|
89
|
+
function validateScheduleId(id) {
|
|
90
|
+
if (!SCHEDULE_ID.test(id))
|
|
91
|
+
throw new Error("Schedule id must be 1-64 characters and contain only letters, numbers, '.', '_', or '-'.");
|
|
92
|
+
return id;
|
|
93
|
+
}
|
|
94
|
+
function normalizedComparisonPath(value) {
|
|
95
|
+
const absolute = path.resolve(value);
|
|
96
|
+
if (process.platform !== "win32")
|
|
97
|
+
return absolute;
|
|
98
|
+
let normalized = absolute;
|
|
99
|
+
try {
|
|
100
|
+
normalized = fs.realpathSync.native(absolute);
|
|
101
|
+
} catch {}
|
|
102
|
+
normalized = normalized.replaceAll("/", "\\");
|
|
103
|
+
if (normalized.startsWith("\\\\?\\UNC\\"))
|
|
104
|
+
normalized = `\\\\${normalized.slice(8)}`;
|
|
105
|
+
else if (normalized.startsWith("\\\\?\\"))
|
|
106
|
+
normalized = normalized.slice(4);
|
|
107
|
+
normalized = path.win32.normalize(normalized).toLowerCase();
|
|
108
|
+
if (normalized.length > 3)
|
|
109
|
+
normalized = normalized.replace(/[\\]+$/, "");
|
|
110
|
+
return normalized;
|
|
111
|
+
}
|
|
112
|
+
function pathWithin(root, candidate) {
|
|
113
|
+
const relative = path.relative(normalizedComparisonPath(root), normalizedComparisonPath(candidate));
|
|
114
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
115
|
+
}
|
|
116
|
+
function resolveGitCommonDirForCheckout(checkoutRoot) {
|
|
117
|
+
const gitPath = path.join(checkoutRoot, ".git");
|
|
118
|
+
try {
|
|
119
|
+
const stat = fs.statSync(gitPath);
|
|
120
|
+
if (stat.isDirectory())
|
|
121
|
+
return fs.realpathSync.native(gitPath);
|
|
122
|
+
if (!stat.isFile())
|
|
123
|
+
return;
|
|
124
|
+
const match = /^gitdir:[ \t]*([^\r\n]+)$/i.exec(fs.readFileSync(gitPath, "utf-8").trim());
|
|
125
|
+
return match?.[1] ? fs.realpathSync.native(path.resolve(checkoutRoot, match[1])) : undefined;
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
61
128
|
}
|
|
62
|
-
throw new Error(`Invalid schedule "${schedule}". Use a one-shot relative delay like "+10m" or a future ISO timestamp with timezone.`);
|
|
63
129
|
}
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
130
|
+
function samePath(left, right) {
|
|
131
|
+
return normalizedComparisonPath(left) === normalizedComparisonPath(right);
|
|
132
|
+
}
|
|
133
|
+
function resolveTrustedGitConfigRoot(checkoutRoot, commonDir) {
|
|
134
|
+
const resolvedCommonDir = resolveGitCommonDirForCheckout(checkoutRoot);
|
|
135
|
+
if (!resolvedCommonDir || !samePath(resolvedCommonDir, commonDir))
|
|
136
|
+
return;
|
|
137
|
+
const configRoot = path.join(checkoutRoot, getConfigDirName());
|
|
68
138
|
try {
|
|
69
|
-
|
|
139
|
+
const resolved = fs.realpathSync.native(configRoot);
|
|
140
|
+
return fs.statSync(resolved).isDirectory() && pathWithin(checkoutRoot, resolved) ? resolved : undefined;
|
|
141
|
+
} catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function registeredGitWorktreeRoots(projectCwd) {
|
|
146
|
+
const result = spawnSync("git", ["-C", projectCwd, "worktree", "list", "--porcelain"], { encoding: "utf-8", windowsHide: true });
|
|
147
|
+
if (result.status !== 0 || typeof result.stdout !== "string")
|
|
148
|
+
return [];
|
|
149
|
+
const roots = [];
|
|
150
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
151
|
+
if (!line.startsWith("worktree "))
|
|
152
|
+
continue;
|
|
153
|
+
try {
|
|
154
|
+
roots.push(fs.realpathSync.native(line.slice("worktree ".length).trim()));
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
return roots;
|
|
158
|
+
}
|
|
159
|
+
function resolveSharedGitConfigRoot(projectCwd) {
|
|
160
|
+
const repository = resolveGitRepositoryIdentity(projectCwd);
|
|
161
|
+
if (!repository)
|
|
162
|
+
return;
|
|
163
|
+
let projectConfigRoot;
|
|
164
|
+
try {
|
|
165
|
+
projectConfigRoot = fs.realpathSync.native(path.join(projectCwd, getConfigDirName()));
|
|
166
|
+
} catch {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
for (const checkoutRoot of new Set(registeredGitWorktreeRoots(projectCwd))) {
|
|
170
|
+
const resolved = resolveTrustedGitConfigRoot(checkoutRoot, repository.commonDir);
|
|
171
|
+
if (resolved && samePath(resolved, projectConfigRoot))
|
|
172
|
+
return resolved;
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
function assertScheduleRoot(root, projectCwd, create) {
|
|
177
|
+
if (!projectCwd) {
|
|
178
|
+
if (create)
|
|
179
|
+
fs.mkdirSync(root, { recursive: true, mode: 448 });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
let projectPath;
|
|
183
|
+
try {
|
|
184
|
+
projectPath = fs.realpathSync.native(projectCwd);
|
|
70
185
|
} catch (error) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
75
|
-
throw new Error(`Scheduled subagent store '${filePath}' must be a JSON object.`);
|
|
76
|
-
}
|
|
77
|
-
const data = parsed;
|
|
78
|
-
if (data.version !== 1)
|
|
79
|
-
throw new Error(`Unsupported scheduled subagent store version in '${filePath}'.`);
|
|
80
|
-
if (!Array.isArray(data.jobs))
|
|
81
|
-
throw new Error(`Scheduled subagent store '${filePath}' must contain a jobs array.`);
|
|
82
|
-
const jobs = [];
|
|
83
|
-
const validStates = new Set(["scheduled", "running", "fired", "canceled", "missed", "failed"]);
|
|
84
|
-
for (const [index, job] of data.jobs.entries()) {
|
|
85
|
-
if (!job || typeof job !== "object" || Array.isArray(job))
|
|
86
|
-
throw new Error(`Scheduled subagent store '${filePath}' job ${index} must be an object.`);
|
|
87
|
-
const candidate = job;
|
|
88
|
-
if (typeof candidate.id !== "string" || typeof candidate.name !== "string" || typeof candidate.schedule !== "string" || typeof candidate.cwd !== "string" || typeof candidate.sessionId !== "string") {
|
|
89
|
-
throw new Error(`Scheduled subagent store '${filePath}' job ${index} has invalid string fields.`);
|
|
90
|
-
}
|
|
91
|
-
const timestamps = [candidate.runAt, candidate.createdAt, candidate.updatedAt];
|
|
92
|
-
if (timestamps.some((value) => typeof value !== "number" || !Number.isFinite(value) || Number.isNaN(new Date(value).getTime()))) {
|
|
93
|
-
throw new Error(`Scheduled subagent store '${filePath}' job ${index} has invalid timestamps.`);
|
|
94
|
-
}
|
|
95
|
-
if (!candidate.state || !validStates.has(candidate.state))
|
|
96
|
-
throw new Error(`Scheduled subagent store '${filePath}' job ${index} has invalid state.`);
|
|
97
|
-
if (!candidate.params || typeof candidate.params !== "object" || Array.isArray(candidate.params))
|
|
98
|
-
throw new Error(`Scheduled subagent store '${filePath}' job ${index} has invalid params.`);
|
|
99
|
-
jobs.push(candidate);
|
|
186
|
+
if (!create && error.code === "ENOENT")
|
|
187
|
+
return;
|
|
188
|
+
throw error;
|
|
100
189
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
190
|
+
let existing = root;
|
|
191
|
+
while (!fs.existsSync(existing)) {
|
|
192
|
+
const parent = path.dirname(existing);
|
|
193
|
+
if (parent === existing)
|
|
194
|
+
break;
|
|
195
|
+
existing = parent;
|
|
196
|
+
}
|
|
197
|
+
const existingPath = fs.realpathSync.native(existing);
|
|
198
|
+
const sharedGitConfigRoot = pathWithin(projectPath, existingPath) ? undefined : resolveSharedGitConfigRoot(projectCwd);
|
|
199
|
+
const isTrustedPath = (candidate) => pathWithin(projectPath, candidate) || sharedGitConfigRoot !== undefined && pathWithin(sharedGitConfigRoot, candidate);
|
|
200
|
+
if (!isTrustedPath(existingPath))
|
|
201
|
+
throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
|
|
202
|
+
if (!create)
|
|
203
|
+
return;
|
|
204
|
+
fs.mkdirSync(root, { recursive: true, mode: 448 });
|
|
205
|
+
if (!isTrustedPath(fs.realpathSync.native(root)))
|
|
206
|
+
throw new Error(`Project schedule root '${root}' resolves outside the real project.`);
|
|
207
|
+
}
|
|
208
|
+
function scheduleDir(root, id, create = false, projectCwd) {
|
|
209
|
+
assertScheduleRoot(root, projectCwd, create);
|
|
210
|
+
const dir = path.join(root, validateScheduleId(id));
|
|
211
|
+
try {
|
|
212
|
+
const stat = fs.lstatSync(dir);
|
|
213
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
214
|
+
throw new Error(`Schedule path '${dir}' must be a real directory.`);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error.code !== "ENOENT")
|
|
217
|
+
throw error;
|
|
218
|
+
if (!create)
|
|
219
|
+
return dir;
|
|
220
|
+
fs.mkdirSync(dir, { mode: 448 });
|
|
221
|
+
}
|
|
222
|
+
const rootPath = fs.realpathSync.native(root);
|
|
223
|
+
const dirPath = fs.realpathSync.native(dir);
|
|
224
|
+
if (!samePath(dirPath, path.join(rootPath, id)))
|
|
225
|
+
throw new Error(`Schedule path '${dir}' escapes the project schedule root.`);
|
|
226
|
+
return dir;
|
|
227
|
+
}
|
|
228
|
+
function readJson(file, label) {
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw new Error(`Failed to read ${label} '${file}': ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function parseScheduleTarget(value, file) {
|
|
236
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
237
|
+
throw new Error(`Schedule record '${file}' has invalid trigger or target.`);
|
|
238
|
+
const target = value;
|
|
239
|
+
if (typeof target.workflowScript === "string" && target.workflowScript.trim())
|
|
240
|
+
return { workflowScript: target.workflowScript.trim() };
|
|
241
|
+
if (target.agent !== undefined || target.task !== undefined)
|
|
242
|
+
throw new Error(`Schedule record '${file}' uses a removed legacy agent target; recreate it with target.workflowScript.`);
|
|
243
|
+
throw new Error(`Schedule record '${file}' requires a workflowScript target.`);
|
|
244
|
+
}
|
|
245
|
+
function parseSchedule(value, file) {
|
|
246
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
247
|
+
throw new Error(`Schedule record '${file}' must be a JSON object.`);
|
|
248
|
+
const record = value;
|
|
249
|
+
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")
|
|
250
|
+
throw new Error(`Schedule record '${file}' has invalid required fields.`);
|
|
251
|
+
validateScheduleId(record.id);
|
|
252
|
+
if (!record.trigger || typeof record.trigger !== "object" || !record.target || typeof record.target !== "object")
|
|
253
|
+
throw new Error(`Schedule record '${file}' has invalid trigger or target.`);
|
|
254
|
+
if (record.overlap !== "skip" || record.catchUp !== "none" && record.catchUp !== "latest")
|
|
255
|
+
throw new Error(`Schedule record '${file}' has unsupported policy fields.`);
|
|
256
|
+
if (record.trigger.kind === "once") {
|
|
257
|
+
if (typeof record.trigger.at !== "string" || record.trigger.nextRunAt !== undefined && typeof record.trigger.nextRunAt !== "string")
|
|
258
|
+
throw new Error(`Schedule record '${file}' has an invalid one-shot trigger.`);
|
|
259
|
+
} else if (record.trigger.kind === "interval") {
|
|
260
|
+
if (typeof record.trigger.every !== "string" || typeof record.trigger.everyMs !== "number" || typeof record.trigger.anchorAt !== "string" || typeof record.trigger.nextRunAt !== "string")
|
|
261
|
+
throw new Error(`Schedule record '${file}' has an invalid interval trigger.`);
|
|
262
|
+
} else
|
|
263
|
+
throw new Error(`Schedule record '${file}' has an unsupported trigger.`);
|
|
264
|
+
return { ...record, target: parseScheduleTarget(record.target, file) };
|
|
107
265
|
}
|
|
108
266
|
|
|
109
|
-
class
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.
|
|
115
|
-
|
|
116
|
-
|
|
267
|
+
class ScheduleStore {
|
|
268
|
+
root;
|
|
269
|
+
projectCwd;
|
|
270
|
+
constructor(root, projectCwd) {
|
|
271
|
+
this.root = root;
|
|
272
|
+
this.projectCwd = projectCwd;
|
|
273
|
+
}
|
|
274
|
+
directory(id, create = false) {
|
|
275
|
+
return scheduleDir(this.root, id, create, this.projectCwd);
|
|
276
|
+
}
|
|
277
|
+
ids() {
|
|
278
|
+
assertScheduleRoot(this.root, this.projectCwd, false);
|
|
279
|
+
if (!fs.existsSync(this.root))
|
|
280
|
+
return [];
|
|
281
|
+
return fs.readdirSync(this.root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && SCHEDULE_ID.test(entry.name)).map((entry) => entry.name);
|
|
117
282
|
}
|
|
118
283
|
list() {
|
|
119
|
-
return
|
|
284
|
+
return this.ids().map((id) => this.find(id)).filter((record) => record !== undefined);
|
|
120
285
|
}
|
|
121
286
|
get(id) {
|
|
122
|
-
|
|
287
|
+
const record = this.find(id);
|
|
288
|
+
if (!record)
|
|
289
|
+
throw new Error(`Schedule '${id}' not found.`);
|
|
290
|
+
return record;
|
|
123
291
|
}
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return
|
|
292
|
+
find(id) {
|
|
293
|
+
const file = path.join(scheduleDir(this.root, id, false, this.projectCwd), "schedule.json");
|
|
294
|
+
if (!fs.existsSync(file))
|
|
295
|
+
return;
|
|
296
|
+
return parseSchedule(readJson(file, "schedule record"), file);
|
|
297
|
+
}
|
|
298
|
+
write(record) {
|
|
299
|
+
writePrivateAtomicJson(path.join(scheduleDir(this.root, record.id, true, this.projectCwd), "schedule.json"), record);
|
|
300
|
+
}
|
|
301
|
+
delete(id) {
|
|
302
|
+
fs.rmSync(scheduleDir(this.root, id, false, this.projectCwd), { recursive: true, force: true });
|
|
303
|
+
}
|
|
304
|
+
history(id) {
|
|
305
|
+
const file = path.join(scheduleDir(this.root, id, false, this.projectCwd), "history.json");
|
|
306
|
+
if (!fs.existsSync(file))
|
|
307
|
+
return [];
|
|
308
|
+
const value = readJson(file, "schedule history");
|
|
309
|
+
if (value?.schemaVersion !== 1 || !Array.isArray(value.runs))
|
|
310
|
+
throw new Error(`Schedule history '${file}' has invalid fields.`);
|
|
311
|
+
return value.runs;
|
|
312
|
+
}
|
|
313
|
+
writeRun(schedule, run, event) {
|
|
314
|
+
const dir = scheduleDir(this.root, schedule.id, true, this.projectCwd);
|
|
315
|
+
writePrivateAtomicJson(path.join(dir, "runs", `${run.id}.json`), run);
|
|
316
|
+
const runs = [run, ...this.history(schedule.id).filter((item) => item.id !== run.id)].slice(0, MAX_HISTORY);
|
|
317
|
+
writePrivateAtomicJson(path.join(dir, "history.json"), { schemaVersion: 1, runs });
|
|
318
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
319
|
+
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 })}
|
|
320
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
321
|
+
}
|
|
322
|
+
appendEvent(schedule, event) {
|
|
323
|
+
const dir = scheduleDir(this.root, schedule.id, true, this.projectCwd);
|
|
324
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
325
|
+
fs.appendFileSync(path.join(dir, "events.jsonl"), `${JSON.stringify({ schemaVersion: 1, timestamp: new Date().toISOString(), event, scheduleId: schedule.id })}
|
|
326
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
129
327
|
}
|
|
130
|
-
}
|
|
131
|
-
function resolveMaxLatenessMs(config) {
|
|
132
|
-
const value = config.scheduledRuns?.maxLatenessMs;
|
|
133
|
-
return Number.isInteger(value) && value >= 0 ? value : DEFAULT_MAX_LATENESS_MS;
|
|
134
328
|
}
|
|
135
329
|
function resolveMaxPending(config) {
|
|
136
330
|
const value = config.scheduledRuns?.maxPending;
|
|
137
|
-
return Number.isInteger(value) && value >= 1 ? value : DEFAULT_MAX_PENDING;
|
|
138
|
-
}
|
|
139
|
-
function
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
function
|
|
143
|
-
if (
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
331
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : DEFAULT_MAX_PENDING;
|
|
332
|
+
}
|
|
333
|
+
function hasPendingScheduleWork(schedule) {
|
|
334
|
+
return schedule.activeRunId !== undefined || schedule.trigger.nextRunAt !== undefined;
|
|
335
|
+
}
|
|
336
|
+
function nextAfter(trigger, plannedAt, now) {
|
|
337
|
+
if (trigger.kind === "once")
|
|
338
|
+
return;
|
|
339
|
+
let next = plannedAt + trigger.everyMs;
|
|
340
|
+
while (next <= now)
|
|
341
|
+
next += trigger.everyMs;
|
|
342
|
+
return timestamp(next);
|
|
343
|
+
}
|
|
344
|
+
function nextRunAt(schedule) {
|
|
345
|
+
const value = schedule.trigger.nextRunAt;
|
|
346
|
+
if (!value)
|
|
347
|
+
return;
|
|
348
|
+
const parsed = Date.parse(value);
|
|
349
|
+
if (!Number.isFinite(parsed))
|
|
350
|
+
throw new Error(`Schedule '${schedule.id}' has invalid nextRunAt.`);
|
|
351
|
+
return parsed;
|
|
352
|
+
}
|
|
353
|
+
function duePlannedAt(schedule, now) {
|
|
354
|
+
const next = nextRunAt(schedule);
|
|
355
|
+
if (next === undefined || next > now || schedule.catchUp !== "latest" || schedule.trigger.kind !== "interval")
|
|
356
|
+
return next;
|
|
357
|
+
return next + Math.floor((now - next) / schedule.trigger.everyMs) * schedule.trigger.everyMs;
|
|
358
|
+
}
|
|
359
|
+
function textResult(text, schedules, runs, isError = false) {
|
|
157
360
|
return {
|
|
158
361
|
content: [{ type: "text", text }],
|
|
159
362
|
...isError ? { isError: true } : {},
|
|
160
|
-
details: { mode: "management", results: [] }
|
|
363
|
+
details: { mode: "management", results: [], schedules: { ...schedules ? { records: schedules } : {}, ...runs ? { runs } : {} } }
|
|
161
364
|
};
|
|
162
365
|
}
|
|
163
|
-
function
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
return
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
function sanitizeScheduledParams(params) {
|
|
175
|
-
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
176
|
-
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
177
|
-
const hasSingle = !hasChain && !hasTasks && Boolean(params.agent);
|
|
178
|
-
if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
|
|
179
|
-
return { error: "action='schedule' requires exactly one execution mode: agent, tasks, or chain." };
|
|
180
|
-
}
|
|
181
|
-
if (!params.schedule?.trim())
|
|
182
|
-
return { error: "action='schedule' requires schedule, such as '+10m' or a future ISO timestamp." };
|
|
366
|
+
function targetLabel(target) {
|
|
367
|
+
const preview = previewSimpleWorkflowRun(target.workflowScript);
|
|
368
|
+
return preview?.agent ? `workflowScript -> agent ${preview.agent}` : "workflowScript (dynamic)";
|
|
369
|
+
}
|
|
370
|
+
function sanitizeTarget(params) {
|
|
371
|
+
if (params.tasks || params.chain)
|
|
372
|
+
return { error: "Recurring schedules require workflowScript; legacy tasks and chain inputs are unsupported." };
|
|
373
|
+
if (params.agent !== undefined || params.task !== undefined)
|
|
374
|
+
return { error: `schedule.create requires workflowScript. Use workflowScript: "return runs.run('main', { agent, task })".` };
|
|
375
|
+
if (typeof params.workflowScript !== "string" || !params.workflowScript.trim())
|
|
376
|
+
return { error: "schedule.create requires a non-empty workflowScript." };
|
|
183
377
|
if (params.context === "fork")
|
|
184
|
-
return { error: "Scheduled
|
|
378
|
+
return { error: "Scheduled runs require fresh context." };
|
|
185
379
|
if (params.async === false)
|
|
186
|
-
return { error: "Scheduled
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
...
|
|
201
|
-
}
|
|
202
|
-
|
|
380
|
+
return { error: "Scheduled runs are always async." };
|
|
381
|
+
const acceptanceErrors = validateExecutionAcceptance(params);
|
|
382
|
+
if (acceptanceErrors.length)
|
|
383
|
+
return { error: acceptanceErrors.join(" ") };
|
|
384
|
+
return { target: { workflowScript: params.workflowScript.trim() } };
|
|
385
|
+
}
|
|
386
|
+
function executionParams(schedule) {
|
|
387
|
+
return {
|
|
388
|
+
...schedule.target,
|
|
389
|
+
async: true,
|
|
390
|
+
context: "fresh",
|
|
391
|
+
cwd: schedule.cwd,
|
|
392
|
+
mission: false,
|
|
393
|
+
scheduleOrigin: { id: schedule.id, ...schedule.name ? { name: schedule.name } : {} },
|
|
394
|
+
...schedule.timeoutMs === undefined ? {} : { timeoutMs: schedule.timeoutMs }
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function snapshotContext(ctx, cwd) {
|
|
398
|
+
const source = ctx.sessionManager;
|
|
399
|
+
const sessionId = source.getSessionId();
|
|
400
|
+
const sessionFile = source.getSessionFile();
|
|
401
|
+
const sessionManager = new Proxy(source, {
|
|
402
|
+
get(target, property) {
|
|
403
|
+
if (property === "getSessionId")
|
|
404
|
+
return () => sessionId;
|
|
405
|
+
if (property === "getSessionFile")
|
|
406
|
+
return () => sessionFile;
|
|
407
|
+
const value = Reflect.get(target, property, target);
|
|
408
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
return { ...ctx, cwd, sessionManager };
|
|
412
|
+
}
|
|
413
|
+
export function listScheduledRunSummaries(cwd, root) {
|
|
414
|
+
return new ScheduleStore(scheduledRunStorePath(cwd, undefined, root), root === undefined ? path.resolve(cwd) : undefined).list();
|
|
203
415
|
}
|
|
204
416
|
|
|
205
417
|
export class ScheduledRunManager {
|
|
206
418
|
store;
|
|
207
|
-
|
|
419
|
+
stores = new Map;
|
|
420
|
+
contexts = new Map;
|
|
208
421
|
timers = new Map;
|
|
209
|
-
|
|
422
|
+
observedAsyncIds = new Set;
|
|
210
423
|
now;
|
|
211
424
|
randomId;
|
|
212
425
|
timersApi;
|
|
213
426
|
deps;
|
|
214
427
|
constructor(deps) {
|
|
215
428
|
this.deps = deps;
|
|
216
|
-
this.storeRoot = deps.storeRoot ?? SCHEDULED_RUNS_DIR;
|
|
217
429
|
this.now = deps.now ?? Date.now;
|
|
218
430
|
this.randomId = deps.randomId ?? (() => randomUUID().slice(0, 8));
|
|
219
431
|
this.timersApi = deps.timers ?? globalThis;
|
|
220
432
|
}
|
|
221
433
|
bindSession(ctx) {
|
|
222
|
-
this.
|
|
223
|
-
this.ctx = ctx;
|
|
224
|
-
if (!scheduledRunsEnabled(this.deps.config)) {
|
|
225
|
-
this.store = undefined;
|
|
434
|
+
if (!scheduledRunsEnabled(this.deps.config))
|
|
226
435
|
return;
|
|
227
|
-
|
|
228
|
-
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
229
|
-
this.store = new ScheduledRunStore(scheduledRunStorePath(ctx.cwd, sessionId, this.storeRoot), ctx.cwd, sessionId);
|
|
230
|
-
this.rearmScheduledJobs();
|
|
436
|
+
this.selectProject(ctx.cwd, ctx);
|
|
231
437
|
}
|
|
232
438
|
stop() {
|
|
233
439
|
this.stopTimers();
|
|
234
440
|
this.store = undefined;
|
|
235
|
-
this.
|
|
441
|
+
this.stores.clear();
|
|
442
|
+
this.contexts.clear();
|
|
443
|
+
this.observedAsyncIds.clear();
|
|
236
444
|
}
|
|
237
445
|
async handleToolCall(params, ctx) {
|
|
238
|
-
this.ctx = ctx;
|
|
239
446
|
try {
|
|
240
|
-
if (!scheduledRunsEnabled(this.deps.config))
|
|
241
|
-
return textResult(
|
|
242
|
-
|
|
243
|
-
if (!this.store)
|
|
244
|
-
this.bindSession(ctx);
|
|
245
|
-
if (!this.store)
|
|
246
|
-
return textResult("Scheduled subagent store is unavailable for this session.", true);
|
|
447
|
+
if (!scheduledRunsEnabled(this.deps.config))
|
|
448
|
+
return textResult("Scheduled runs are disabled by scheduledRuns.enabled=false.", undefined, undefined, true);
|
|
449
|
+
this.selectProject(params.cwd ?? ctx.cwd, ctx);
|
|
247
450
|
switch (params.action) {
|
|
248
|
-
case "schedule":
|
|
249
|
-
return this.
|
|
250
|
-
case "schedule
|
|
251
|
-
return this.
|
|
252
|
-
case "schedule
|
|
253
|
-
return this.
|
|
254
|
-
case "schedule
|
|
255
|
-
return this.
|
|
451
|
+
case "schedule.create":
|
|
452
|
+
return this.create(params, ctx);
|
|
453
|
+
case "schedule.list":
|
|
454
|
+
return this.list();
|
|
455
|
+
case "schedule.show":
|
|
456
|
+
return this.show(params);
|
|
457
|
+
case "schedule.history":
|
|
458
|
+
return this.history(params);
|
|
459
|
+
case "schedule.pause":
|
|
460
|
+
return this.pause(params, true);
|
|
461
|
+
case "schedule.resume":
|
|
462
|
+
return this.pause(params, false);
|
|
463
|
+
case "schedule.run":
|
|
464
|
+
return await this.runManual(params);
|
|
465
|
+
case "schedule.run-due":
|
|
466
|
+
return await this.runDue();
|
|
467
|
+
case "schedule.delete":
|
|
468
|
+
return this.remove(params);
|
|
256
469
|
default:
|
|
257
|
-
return textResult(`Unknown
|
|
470
|
+
return textResult(`Unknown schedule action: ${params.action}`, undefined, undefined, true);
|
|
258
471
|
}
|
|
259
472
|
} catch (error) {
|
|
260
|
-
|
|
261
|
-
|
|
473
|
+
return textResult(error instanceof Error ? error.message : String(error), undefined, undefined, true);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
observedCompletionRunIds() {
|
|
477
|
+
return new Set(this.observedAsyncIds);
|
|
478
|
+
}
|
|
479
|
+
referencedAsyncRunIds() {
|
|
480
|
+
const runIds = new Set(this.observedAsyncIds);
|
|
481
|
+
for (const store of this.stores.values()) {
|
|
482
|
+
for (const scheduleId of store.ids()) {
|
|
483
|
+
for (const run of store.history(scheduleId)) {
|
|
484
|
+
if (run.asyncId)
|
|
485
|
+
runIds.add(run.asyncId);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return runIds;
|
|
490
|
+
}
|
|
491
|
+
handleAsyncCompletion(payload) {
|
|
492
|
+
if (!payload || typeof payload !== "object")
|
|
493
|
+
return;
|
|
494
|
+
const data = payload;
|
|
495
|
+
const asyncId = typeof data.runId === "string" ? data.runId : typeof data.id === "string" ? data.id : undefined;
|
|
496
|
+
if (!asyncId)
|
|
497
|
+
return;
|
|
498
|
+
for (const store of this.stores.values()) {
|
|
499
|
+
let ids;
|
|
500
|
+
try {
|
|
501
|
+
ids = store.ids();
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error(`Failed to inspect schedule store '${store.root}' during async completion:`, error);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
for (const id of ids) {
|
|
507
|
+
try {
|
|
508
|
+
const schedule = store.get(id);
|
|
509
|
+
const run = store.history(id).find((item) => item.asyncId === asyncId && item.state === "running");
|
|
510
|
+
if (!run)
|
|
511
|
+
continue;
|
|
512
|
+
this.finishRun(store, schedule, run, data.success === true, typeof data.summary === "string" ? data.summary : undefined);
|
|
513
|
+
return;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.error(`Failed to inspect schedule '${id}' in '${store.root}' during async completion:`, error);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
262
518
|
}
|
|
263
519
|
}
|
|
264
|
-
|
|
520
|
+
create(params, ctx) {
|
|
265
521
|
const store = this.requireStore();
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
return textResult(
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
522
|
+
const target = sanitizeTarget(params);
|
|
523
|
+
if (target.error)
|
|
524
|
+
return textResult(target.error, undefined, undefined, true);
|
|
525
|
+
const at = params.at?.trim();
|
|
526
|
+
const every = params.every?.trim();
|
|
527
|
+
if (Boolean(at) === Boolean(every))
|
|
528
|
+
return textResult("schedule.create requires exactly one trigger: at or every.", undefined, undefined, true);
|
|
529
|
+
if (params.overlap !== undefined && params.overlap !== "skip")
|
|
530
|
+
return textResult("This first recurring slice supports overlap='skip' only.", undefined, undefined, true);
|
|
531
|
+
if (params.catchUp !== undefined && params.catchUp !== "none" && params.catchUp !== "latest")
|
|
532
|
+
return textResult("catchUp must be 'none' or 'latest'.", undefined, undefined, true);
|
|
533
|
+
if (params.missionId !== undefined || params.mission !== undefined || params.missionUpdate !== undefined || params.missionStatus !== undefined || params.missionScope !== undefined)
|
|
534
|
+
return textResult("Mission attachment is deferred from this first schedule slice.", undefined, undefined, true);
|
|
535
|
+
if (params.on !== undefined || params.timezone !== undefined || every === "day" || every === "week" || every === "month" || every === "year")
|
|
536
|
+
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);
|
|
537
|
+
const sessionId = ctx.sessionManager.getSessionId() ?? "unknown";
|
|
538
|
+
if (this.deps.resolveCapabilityCeiling?.(sessionId))
|
|
539
|
+
return textResult("Cannot persist a schedule while a capability ceiling is active.", undefined, undefined, true);
|
|
540
|
+
const pendingCount = store.list().filter(hasPendingScheduleWork).length;
|
|
272
541
|
const maxPending = resolveMaxPending(this.deps.config);
|
|
273
542
|
if (pendingCount >= maxPending)
|
|
274
|
-
return textResult(`
|
|
275
|
-
const id = this.randomId();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const executionParams = sanitized.params;
|
|
543
|
+
return textResult(`Schedule limit reached (${maxPending}).`, undefined, undefined, true);
|
|
544
|
+
const id = validateScheduleId(params.id?.trim() || this.randomId());
|
|
545
|
+
if (store.ids().includes(id))
|
|
546
|
+
return textResult(`Schedule '${id}' already exists.`, undefined, undefined, true);
|
|
279
547
|
const now = this.now();
|
|
280
|
-
|
|
548
|
+
let trigger;
|
|
549
|
+
if (at) {
|
|
550
|
+
const planned = parseScheduledRunTime(at, now);
|
|
551
|
+
trigger = { kind: "once", at, nextRunAt: timestamp(planned) };
|
|
552
|
+
} else {
|
|
553
|
+
const everyMs = parseScheduleInterval(every);
|
|
554
|
+
trigger = { kind: "interval", every, everyMs, anchorAt: timestamp(now), nextRunAt: timestamp(now + everyMs) };
|
|
555
|
+
}
|
|
556
|
+
const schedule = {
|
|
557
|
+
schemaVersion: 1,
|
|
281
558
|
id,
|
|
282
|
-
name:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
559
|
+
name: params.name?.trim() || targetLabel(target.target),
|
|
560
|
+
cwd: path.resolve(params.cwd ?? ctx.cwd),
|
|
561
|
+
trigger,
|
|
562
|
+
target: target.target,
|
|
563
|
+
overlap: "skip",
|
|
564
|
+
catchUp: params.catchUp ?? "latest",
|
|
565
|
+
...params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs },
|
|
566
|
+
paused: false,
|
|
567
|
+
createdAt: timestamp(now),
|
|
568
|
+
updatedAt: timestamp(now)
|
|
291
569
|
};
|
|
292
|
-
store.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
`))
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const job = resolveJobById(this.requireStore().list(), requestedId);
|
|
328
|
-
const lines = [
|
|
329
|
-
`Scheduled run: ${job.id}`,
|
|
330
|
-
`Name: ${job.name}`,
|
|
331
|
-
`State: ${job.state}`,
|
|
332
|
-
`Schedule: ${job.schedule}`,
|
|
333
|
-
`Run at: ${new Date(job.runAt).toISOString()}`,
|
|
334
|
-
`Mode: ${jobMode(job.params)}`,
|
|
335
|
-
`CWD: ${shortenPath(job.cwd)}`,
|
|
336
|
-
`Created: ${new Date(job.createdAt).toISOString()}`,
|
|
337
|
-
`Updated: ${new Date(job.updatedAt).toISOString()}`,
|
|
338
|
-
job.lastRunId ? `Launched async run: ${job.lastRunId}` : undefined,
|
|
339
|
-
job.lastAsyncDir ? `Async dir: ${job.lastAsyncDir}` : undefined,
|
|
340
|
-
job.lastError ? `Error: ${job.lastError}` : undefined,
|
|
341
|
-
job.state === "scheduled" ? `Cancel: subagent({ action: "schedule-cancel", id: "${job.id}" })` : undefined,
|
|
342
|
-
job.lastRunId ? `Async status: subagent({ action: "status", id: "${job.lastRunId}" })` : undefined
|
|
343
|
-
].filter((line) => Boolean(line));
|
|
344
|
-
return textResult(lines.join(`
|
|
345
|
-
`));
|
|
346
|
-
}
|
|
347
|
-
cancelJob(params) {
|
|
348
|
-
const requestedId = params.id ?? params.runId;
|
|
349
|
-
if (!requestedId)
|
|
350
|
-
return textResult("action='schedule-cancel' requires id.", true);
|
|
570
|
+
store.write(schedule);
|
|
571
|
+
store.appendEvent(schedule, "schedule.created");
|
|
572
|
+
this.arm(schedule, store);
|
|
573
|
+
return textResult(`Created schedule ${id}.
|
|
574
|
+
Name: ${schedule.name}
|
|
575
|
+
Trigger: ${at ? `at ${at}` : `every ${every}`}
|
|
576
|
+
Next: ${schedule.trigger.nextRunAt}
|
|
577
|
+
Target: ${targetLabel(schedule.target)}`, [schedule]);
|
|
578
|
+
}
|
|
579
|
+
list() {
|
|
580
|
+
const schedules = this.requireStore().list().sort((a, b) => (a.trigger.nextRunAt ?? "").localeCompare(b.trigger.nextRunAt ?? ""));
|
|
581
|
+
if (!schedules.length)
|
|
582
|
+
return textResult("No project schedules.", []);
|
|
583
|
+
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.name}`)].join(`
|
|
584
|
+
`), schedules);
|
|
585
|
+
}
|
|
586
|
+
show(params) {
|
|
587
|
+
const schedule = this.resolve(params);
|
|
588
|
+
return textResult([`Schedule: ${schedule.id}`, `Name: ${schedule.name}`, `State: ${schedule.paused ? "paused" : schedule.activeRunId ? "running" : "scheduled"}`, `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(`
|
|
589
|
+
`), [schedule]);
|
|
590
|
+
}
|
|
591
|
+
history(params) {
|
|
592
|
+
const schedule = this.resolve(params);
|
|
593
|
+
const runs = this.requireStore().history(schedule.id);
|
|
594
|
+
return textResult(runs.length ? [`Schedule history: ${schedule.id}`, ...runs.map((run) => `- ${run.id} | ${run.state} | ${run.plannedAt}${run.asyncId ? ` | async ${run.asyncId}` : ""}`)].join(`
|
|
595
|
+
`) : `No runs recorded for schedule ${schedule.id}.`, [schedule], runs);
|
|
596
|
+
}
|
|
597
|
+
pause(params, paused) {
|
|
598
|
+
const schedule = this.resolve(params);
|
|
599
|
+
if (schedule.paused === paused)
|
|
600
|
+
return textResult(`Schedule ${schedule.id} is already ${paused ? "paused" : "active"}.`, [schedule]);
|
|
601
|
+
schedule.paused = paused;
|
|
602
|
+
schedule.updatedAt = timestamp(this.now());
|
|
603
|
+
this.requireStore().write(schedule);
|
|
604
|
+
this.requireStore().appendEvent(schedule, paused ? "schedule.paused" : "schedule.resumed");
|
|
351
605
|
const store = this.requireStore();
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const now = this.now();
|
|
358
|
-
this.clearTimer(job.id);
|
|
359
|
-
store.mutate((data) => {
|
|
360
|
-
const stored = data.jobs.find((candidate) => candidate.id === job.id);
|
|
361
|
-
if (!stored)
|
|
362
|
-
return;
|
|
363
|
-
stored.state = "canceled";
|
|
364
|
-
stored.canceledAt = now;
|
|
365
|
-
stored.updatedAt = now;
|
|
366
|
-
});
|
|
367
|
-
return textResult(`Canceled scheduled subagent run ${job.id}.`);
|
|
606
|
+
if (paused)
|
|
607
|
+
this.clearTimer(store, schedule.id);
|
|
608
|
+
else
|
|
609
|
+
this.restoreOne(store, schedule);
|
|
610
|
+
return textResult(`${paused ? "Paused" : "Resumed"} schedule ${schedule.id}.`, [schedule]);
|
|
368
611
|
}
|
|
369
|
-
|
|
612
|
+
async runManual(params) {
|
|
370
613
|
const store = this.requireStore();
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
614
|
+
const schedule = this.resolve(params);
|
|
615
|
+
const run = await this.launch(store, schedule, this.now(), "manual", false);
|
|
616
|
+
return textResult(`Manual schedule run ${run.id}: ${run.state}${run.asyncId ? ` (async ${run.asyncId})` : ""}.`, [store.get(schedule.id)], [run], run.state === "failed_launch");
|
|
617
|
+
}
|
|
618
|
+
async runDue() {
|
|
619
|
+
const store = this.requireStore();
|
|
620
|
+
const due = store.list().filter((schedule) => !schedule.paused && nextRunAt(schedule) !== undefined && nextRunAt(schedule) <= this.now());
|
|
621
|
+
const runs = [];
|
|
622
|
+
for (const schedule of due) {
|
|
623
|
+
const planned = duePlannedAt(schedule, this.now());
|
|
624
|
+
if (!schedule.activeRunId && schedule.catchUp === "none" && planned < this.now())
|
|
625
|
+
runs.push(this.recordMissed(store, schedule, planned, "run-due"));
|
|
626
|
+
else
|
|
627
|
+
runs.push(await this.launch(store, schedule, planned, "run-due", true));
|
|
628
|
+
}
|
|
629
|
+
return textResult(runs.length ? `Processed ${runs.length} due schedule(s).` : "No schedules are due.", store.list(), runs);
|
|
630
|
+
}
|
|
631
|
+
remove(params) {
|
|
632
|
+
const schedule = this.resolve(params);
|
|
633
|
+
if (schedule.activeRunId)
|
|
634
|
+
return textResult(`Schedule ${schedule.id} has active run ${schedule.activeRunId}; stop that run before deleting the schedule.`, [schedule], undefined, true);
|
|
635
|
+
const store = this.requireStore();
|
|
636
|
+
this.clearTimer(store, schedule.id);
|
|
637
|
+
store.appendEvent(schedule, "schedule.deleted");
|
|
638
|
+
store.delete(schedule.id);
|
|
639
|
+
return textResult(`Deleted schedule ${schedule.id}.`);
|
|
640
|
+
}
|
|
641
|
+
restore(store) {
|
|
642
|
+
for (const schedule of store.list())
|
|
643
|
+
this.restoreOne(store, schedule);
|
|
644
|
+
}
|
|
645
|
+
restoreOne(store, schedule, notBefore, rearm = true) {
|
|
646
|
+
if (schedule.activeRunId) {
|
|
647
|
+
const run = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);
|
|
648
|
+
if (run?.state === "running" && run.asyncId)
|
|
649
|
+
this.observedAsyncIds.add(run.asyncId);
|
|
650
|
+
const startedAt = run?.startedAt ? Date.parse(run.startedAt) : Number.NaN;
|
|
651
|
+
if (run?.state === "running" && run.asyncDir) {
|
|
652
|
+
try {
|
|
653
|
+
const status = readJson(path.join(run.asyncDir, "status.json"), "async status");
|
|
654
|
+
if (["complete", "failed", "stopped", "rejected"].includes(String(status.state)))
|
|
655
|
+
this.finishRun(store, schedule, run, status.state === "complete", typeof status.error === "string" ? status.error : undefined);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
if (error.code !== "ENOENT" && !(error instanceof Error && /ENOENT/.test(error.message)))
|
|
658
|
+
throw error;
|
|
383
659
|
}
|
|
384
|
-
}
|
|
660
|
+
}
|
|
661
|
+
if (schedule.activeRunId && (!run || run.state !== "running" || !run.asyncId && Number.isFinite(startedAt) && startedAt + STALE_LAUNCH_CLAIM_MS <= this.now())) {
|
|
662
|
+
if (run?.state === "running") {
|
|
663
|
+
run.state = "failed_launch";
|
|
664
|
+
run.completedAt = timestamp(this.now());
|
|
665
|
+
run.error = "Recovered a stale launch claim before an async run was attached.";
|
|
666
|
+
store.writeRun(schedule, run, "schedule.run.failed");
|
|
667
|
+
}
|
|
668
|
+
schedule.activeRunId = undefined;
|
|
669
|
+
schedule.updatedAt = timestamp(this.now());
|
|
670
|
+
store.write(schedule);
|
|
671
|
+
fs.rmSync(path.join(store.directory(schedule.id), "active.lock"), { force: true });
|
|
672
|
+
}
|
|
385
673
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
674
|
+
if (!rearm || schedule.paused)
|
|
675
|
+
return;
|
|
676
|
+
const next = nextRunAt(schedule);
|
|
677
|
+
if (next === undefined)
|
|
678
|
+
return;
|
|
679
|
+
if (!schedule.activeRunId && next < this.now() && schedule.catchUp === "none") {
|
|
680
|
+
try {
|
|
681
|
+
this.recordMissed(store, schedule, next, "timer");
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (notBefore !== undefined)
|
|
684
|
+
this.arm(schedule, store, notBefore);
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
389
687
|
}
|
|
688
|
+
this.arm(schedule, store, notBefore);
|
|
390
689
|
}
|
|
391
|
-
arm(
|
|
392
|
-
this.clearTimer(
|
|
393
|
-
|
|
690
|
+
arm(schedule, store, notBefore) {
|
|
691
|
+
this.clearTimer(store, schedule.id);
|
|
692
|
+
if (schedule.paused)
|
|
693
|
+
return;
|
|
694
|
+
const next = nextRunAt(schedule);
|
|
695
|
+
if (next === undefined)
|
|
696
|
+
return;
|
|
394
697
|
const timer = this.timersApi.setTimeout(() => {
|
|
395
|
-
this.fire(
|
|
396
|
-
|
|
698
|
+
this.fire(store, schedule.id).catch((error) => {
|
|
699
|
+
console.warn(`[dm-subagents] Scheduled run '${schedule.id}' failed to fire: ${error instanceof Error ? error.message : String(error)}`);
|
|
700
|
+
this.restoreAfterFireError(store, schedule.id);
|
|
701
|
+
});
|
|
702
|
+
}, Math.min(Math.max(0, next - this.now(), (notBefore ?? 0) - this.now()), MAX_TIMER_DELAY_MS));
|
|
397
703
|
timer.unref?.();
|
|
398
|
-
this.timers.set(
|
|
704
|
+
this.timers.set(this.timerKey(store, schedule.id), timer);
|
|
399
705
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
706
|
+
restoreAfterFireError(store, id) {
|
|
707
|
+
try {
|
|
708
|
+
const schedule = store.find(id);
|
|
709
|
+
if (!schedule)
|
|
710
|
+
return;
|
|
711
|
+
const now = this.now();
|
|
712
|
+
const planned = schedule.trigger.kind === "interval" ? duePlannedAt(schedule, now) : undefined;
|
|
713
|
+
const notBefore = planned !== undefined && planned <= now ? Date.parse(nextAfter(schedule.trigger, planned, now)) : undefined;
|
|
714
|
+
this.restoreOne(store, schedule, notBefore, schedule.trigger.kind === "interval");
|
|
715
|
+
} catch (error) {
|
|
716
|
+
console.warn(`[dm-subagents] Scheduled run '${id}' could not be restored after fire failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async fire(store, id) {
|
|
720
|
+
this.clearTimer(store, id);
|
|
721
|
+
const schedule = store.find(id);
|
|
722
|
+
if (!schedule)
|
|
405
723
|
return;
|
|
406
|
-
|
|
407
|
-
if (
|
|
724
|
+
const planned = duePlannedAt(schedule, this.now());
|
|
725
|
+
if (planned === undefined || schedule.paused)
|
|
408
726
|
return;
|
|
727
|
+
if (planned > this.now())
|
|
728
|
+
return this.arm(schedule, store);
|
|
729
|
+
await this.launch(store, schedule, planned, "timer", true);
|
|
730
|
+
}
|
|
731
|
+
async launch(store, schedule, planned, dueReason, advance) {
|
|
409
732
|
const now = this.now();
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
733
|
+
const run = { schemaVersion: 1, id: this.randomId(), scheduleId: schedule.id, plannedAt: timestamp(planned), dueReason, state: "running", startedAt: timestamp(now) };
|
|
734
|
+
if (schedule.activeRunId) {
|
|
735
|
+
run.state = "skipped";
|
|
736
|
+
run.completedAt = timestamp(now);
|
|
737
|
+
if (advance) {
|
|
738
|
+
schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);
|
|
739
|
+
schedule.updatedAt = timestamp(now);
|
|
740
|
+
store.write(schedule);
|
|
741
|
+
}
|
|
742
|
+
store.writeRun(schedule, run, "schedule.skipped_overlap");
|
|
743
|
+
this.arm(schedule, store);
|
|
744
|
+
return run;
|
|
413
745
|
}
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
746
|
+
const lockPath = path.join(store.directory(schedule.id, true), "active.lock");
|
|
747
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 448 });
|
|
748
|
+
let lock;
|
|
749
|
+
try {
|
|
750
|
+
lock = fs.openSync(lockPath, "wx", 384);
|
|
751
|
+
fs.writeFileSync(lock, run.id, "utf-8");
|
|
752
|
+
fs.closeSync(lock);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (error.code !== "EEXIST")
|
|
755
|
+
throw error;
|
|
756
|
+
run.state = "skipped";
|
|
757
|
+
run.completedAt = timestamp(now);
|
|
758
|
+
if (advance) {
|
|
759
|
+
schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);
|
|
760
|
+
schedule.updatedAt = timestamp(now);
|
|
761
|
+
store.write(schedule);
|
|
762
|
+
}
|
|
763
|
+
store.writeRun(schedule, run, "schedule.skipped_overlap");
|
|
764
|
+
this.arm(schedule, store);
|
|
765
|
+
return run;
|
|
425
766
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
});
|
|
434
|
-
job = store.get(jobId);
|
|
435
|
-
if (!job || job.state !== "running")
|
|
436
|
-
return;
|
|
437
|
-
const controller = new AbortController;
|
|
767
|
+
schedule.activeRunId = run.id;
|
|
768
|
+
schedule.lastRunId = run.id;
|
|
769
|
+
if (advance)
|
|
770
|
+
schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);
|
|
771
|
+
schedule.updatedAt = timestamp(now);
|
|
772
|
+
store.write(schedule);
|
|
773
|
+
store.writeRun(schedule, run, "schedule.run.started");
|
|
438
774
|
try {
|
|
439
|
-
const result = await this.deps.launch(
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
stored.state = "fired";
|
|
452
|
-
stored.lastRunId = launchRunId;
|
|
453
|
-
stored.lastAsyncDir = result.details?.asyncDir;
|
|
454
|
-
});
|
|
775
|
+
const result = await this.deps.launch(executionParams(schedule), this.requireContext(store), new AbortController().signal);
|
|
776
|
+
const asyncId = result.details?.asyncId ?? result.details?.runId;
|
|
777
|
+
if (result.isError || !asyncId)
|
|
778
|
+
throw new Error(result.content.find((item) => item.type === "text")?.text ?? "Scheduled launch failed.");
|
|
779
|
+
run.asyncId = asyncId;
|
|
780
|
+
run.asyncDir = result.details?.asyncDir;
|
|
781
|
+
this.observedAsyncIds.add(asyncId);
|
|
782
|
+
store.writeRun(schedule, run, "schedule.run.attached_async");
|
|
783
|
+
this.arm(schedule, store);
|
|
784
|
+
return run;
|
|
455
785
|
} catch (error) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
786
|
+
run.state = "failed_launch";
|
|
787
|
+
run.completedAt = timestamp(this.now());
|
|
788
|
+
run.error = error instanceof Error ? error.message : String(error);
|
|
789
|
+
schedule.activeRunId = undefined;
|
|
790
|
+
schedule.updatedAt = timestamp(this.now());
|
|
791
|
+
store.write(schedule);
|
|
792
|
+
store.writeRun(schedule, run, "schedule.run.failed");
|
|
793
|
+
fs.rmSync(lockPath, { force: true });
|
|
794
|
+
this.arm(schedule, store);
|
|
795
|
+
return run;
|
|
465
796
|
}
|
|
466
797
|
}
|
|
798
|
+
finishRun(store, schedule, run, success, error) {
|
|
799
|
+
const now = this.now();
|
|
800
|
+
const next = nextRunAt(schedule);
|
|
801
|
+
if (next !== undefined && next <= now) {
|
|
802
|
+
const planned = duePlannedAt(schedule, now);
|
|
803
|
+
const skipped = {
|
|
804
|
+
schemaVersion: 1,
|
|
805
|
+
id: this.randomId(),
|
|
806
|
+
scheduleId: schedule.id,
|
|
807
|
+
plannedAt: timestamp(planned),
|
|
808
|
+
dueReason: "timer",
|
|
809
|
+
state: "skipped",
|
|
810
|
+
completedAt: timestamp(now)
|
|
811
|
+
};
|
|
812
|
+
schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);
|
|
813
|
+
store.writeRun(schedule, skipped, "schedule.skipped_overlap");
|
|
814
|
+
}
|
|
815
|
+
if (run.asyncId)
|
|
816
|
+
this.observedAsyncIds.delete(run.asyncId);
|
|
817
|
+
run.state = success ? "completed" : "failed_run";
|
|
818
|
+
run.completedAt = timestamp(now);
|
|
819
|
+
if (!success && error)
|
|
820
|
+
run.error = error;
|
|
821
|
+
schedule.activeRunId = undefined;
|
|
822
|
+
schedule.updatedAt = timestamp(now);
|
|
823
|
+
store.write(schedule);
|
|
824
|
+
fs.rmSync(path.join(store.directory(schedule.id), "active.lock"), { force: true });
|
|
825
|
+
store.writeRun(schedule, run, success ? "schedule.run.completed" : "schedule.run.failed");
|
|
826
|
+
this.arm(schedule, store);
|
|
827
|
+
}
|
|
828
|
+
recordMissed(store, schedule, planned, dueReason) {
|
|
829
|
+
const run = {
|
|
830
|
+
schemaVersion: 1,
|
|
831
|
+
id: this.randomId(),
|
|
832
|
+
scheduleId: schedule.id,
|
|
833
|
+
plannedAt: timestamp(planned),
|
|
834
|
+
dueReason,
|
|
835
|
+
state: "missed",
|
|
836
|
+
completedAt: timestamp(this.now())
|
|
837
|
+
};
|
|
838
|
+
schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, this.now());
|
|
839
|
+
schedule.updatedAt = timestamp(this.now());
|
|
840
|
+
store.write(schedule);
|
|
841
|
+
store.writeRun(schedule, run, "schedule.missed");
|
|
842
|
+
return run;
|
|
843
|
+
}
|
|
844
|
+
selectProject(cwd, ctx) {
|
|
845
|
+
const projectCwd = path.resolve(cwd);
|
|
846
|
+
const root = scheduledRunStorePath(projectCwd, undefined, this.deps.storeRoot);
|
|
847
|
+
if (path.resolve(ctx.cwd) === projectCwd)
|
|
848
|
+
this.contexts.set(root, snapshotContext(ctx, projectCwd));
|
|
849
|
+
else if (!this.contexts.has(root))
|
|
850
|
+
throw new Error(`Cannot use project '${projectCwd}' until that project has been opened in this runtime.`);
|
|
851
|
+
let store = this.stores.get(root);
|
|
852
|
+
if (!store) {
|
|
853
|
+
store = new ScheduleStore(root, this.deps.storeRoot === undefined ? projectCwd : undefined);
|
|
854
|
+
this.stores.set(root, store);
|
|
855
|
+
this.restore(store);
|
|
856
|
+
}
|
|
857
|
+
this.store = store;
|
|
858
|
+
}
|
|
859
|
+
resolve(params) {
|
|
860
|
+
const id = params.id?.trim();
|
|
861
|
+
if (!id)
|
|
862
|
+
throw new Error(`${params.action} requires id.`);
|
|
863
|
+
return this.requireStore().get(id);
|
|
864
|
+
}
|
|
467
865
|
requireStore() {
|
|
468
866
|
if (!this.store)
|
|
469
|
-
throw new Error("
|
|
867
|
+
throw new Error("Schedule store is unavailable.");
|
|
470
868
|
return this.store;
|
|
471
869
|
}
|
|
472
|
-
|
|
473
|
-
const
|
|
870
|
+
requireContext(store) {
|
|
871
|
+
const ctx = this.contexts.get(store.root);
|
|
872
|
+
if (!ctx)
|
|
873
|
+
throw new Error("Schedule runtime context is unavailable.");
|
|
874
|
+
return ctx;
|
|
875
|
+
}
|
|
876
|
+
timerKey(store, id) {
|
|
877
|
+
return `${store.root}\x00${id}`;
|
|
878
|
+
}
|
|
879
|
+
clearTimer(store, id) {
|
|
880
|
+
const key = this.timerKey(store, id);
|
|
881
|
+
const timer = this.timers.get(key);
|
|
474
882
|
if (!timer)
|
|
475
883
|
return;
|
|
476
884
|
this.timersApi.clearTimeout(timer);
|
|
477
|
-
this.timers.delete(
|
|
885
|
+
this.timers.delete(key);
|
|
478
886
|
}
|
|
479
887
|
stopTimers() {
|
|
480
888
|
for (const timer of this.timers.values())
|