@duckmind/dm-windows-x64 0.60.6 → 0.60.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 +70 -123
- package/extensions/dm-9router-ext/src/index.js +410 -5
- package/extensions/dm-caveman/extensions/caveman.js +283 -12
- package/extensions/dm-cliproxy/index.js +182 -2
- package/extensions/dm-cliproxy/scripts/check-config-migration.js +66 -8
- package/extensions/dm-cliproxy/src/apply.js +228 -1
- package/extensions/dm-cliproxy/src/cache.js +42 -1
- package/extensions/dm-cliproxy/src/commands.js +50 -2
- package/extensions/dm-cliproxy/src/compat.js +81 -1
- package/extensions/dm-cliproxy/src/config.js +192 -2
- package/extensions/dm-cliproxy/src/conflicts.js +46 -1
- package/extensions/dm-cliproxy/src/fetch-models.js +190 -1
- package/extensions/dm-cliproxy/src/fetch-usage.js +41 -1
- package/extensions/dm-cliproxy/src/log.js +23 -1
- package/extensions/dm-cliproxy/src/status-quota.js +77 -1
- package/extensions/dm-cliproxy/src/ui-frame.js +50 -1
- package/extensions/dm-cliproxy/src/ui-hub/hub.js +199 -2
- package/extensions/dm-cliproxy/src/ui-hub/index.js +26 -2
- package/extensions/dm-cliproxy/src/ui-hub/shell.js +46 -1
- package/extensions/dm-cliproxy/src/ui-hub/view-diagnostics.js +69 -1
- package/extensions/dm-cliproxy/src/ui-hub/view-models.js +379 -1
- package/extensions/dm-cliproxy/src/ui-hub/view-usage.js +106 -1
- package/extensions/dm-cliproxy/src/ui-picker/catalog.js +41 -1
- package/extensions/dm-cliproxy/src/ui-picker/mutate.js +115 -1
- package/extensions/dm-cliproxy/src/ui-picker/prompt-confirm.js +35 -1
- package/extensions/dm-cliproxy/src/ui-picker/prompt-name.js +62 -1
- package/extensions/dm-cliproxy/src/ui-picker/providers.js +45 -1
- package/extensions/dm-cliproxy/src/ui-picker/render-text.js +34 -1
- package/extensions/dm-cliproxy/src/ui-picker/rows.js +57 -1
- package/extensions/dm-cliproxy/src/ui-setup.js +260 -2
- package/extensions/dm-cliproxy/src/ui-usage.js +151 -1
- package/extensions/dm-cliproxy/src/usage-shared-cache.js +97 -1
- package/extensions/dm-context/src/context.js +144 -1
- package/extensions/dm-context/src/index.js +339 -7
- package/extensions/dm-context/src/utils.js +9 -1
- package/extensions/dm-cua/bin/browser-cua.mjs +73 -8
- package/extensions/dm-cua/index.js +75 -6
- package/extensions/dm-cua/src/browser-cua-lib.mjs +490 -6
- package/extensions/dm-cua/src/browser-install.mjs +331 -2
- package/extensions/dm-fff/src/index.js +688 -12
- package/extensions/dm-fff/src/query.js +60 -1
- package/extensions/dm-goal/src/goal.js +894 -23
- package/extensions/dm-image2/index.js +103 -9
- package/extensions/dm-image2/src/image-lib.mjs +1275 -8
- package/extensions/dm-subagents/install.mjs +62 -8
- package/extensions/dm-subagents/src/agents/agent-management.js +1190 -36
- package/extensions/dm-subagents/src/agents/agent-memory.js +216 -6
- package/extensions/dm-subagents/src/agents/agent-scope.js +5 -1
- package/extensions/dm-subagents/src/agents/agent-selection.js +20 -1
- package/extensions/dm-subagents/src/agents/agent-serializer.js +120 -5
- package/extensions/dm-subagents/src/agents/agents.js +1139 -11
- package/extensions/dm-subagents/src/agents/chain-serializer.js +299 -11
- package/extensions/dm-subagents/src/agents/frontmatter.js +65 -6
- package/extensions/dm-subagents/src/agents/identity.js +29 -1
- package/extensions/dm-subagents/src/agents/proactive-skills.js +141 -1
- package/extensions/dm-subagents/src/agents/skills.js +614 -6
- package/extensions/dm-subagents/src/extension/config.js +35 -2
- package/extensions/dm-subagents/src/extension/control-notices.js +69 -4
- package/extensions/dm-subagents/src/extension/doctor.js +172 -15
- package/extensions/dm-subagents/src/extension/fanout-child.js +158 -231
- package/extensions/dm-subagents/src/extension/index.js +521 -359
- package/extensions/dm-subagents/src/extension/rpc.js +266 -7
- package/extensions/dm-subagents/src/extension/schemas.js +275 -1
- package/extensions/dm-subagents/src/extension/tool-description.js +111 -6
- package/extensions/dm-subagents/src/intercom/intercom-bridge.js +126 -4
- package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +452 -5
- package/extensions/dm-subagents/src/intercom/result-intercom.js +319 -3
- package/extensions/dm-subagents/src/profiles/profiles.js +458 -3
- package/extensions/dm-subagents/src/runs/background/async-execution.js +834 -40
- package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +435 -14
- package/extensions/dm-subagents/src/runs/background/async-resume.js +334 -8
- package/extensions/dm-subagents/src/runs/background/async-status.js +313 -12
- package/extensions/dm-subagents/src/runs/background/chain-append.js +245 -2
- package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +136 -1
- package/extensions/dm-subagents/src/runs/background/completion-batcher.js +94 -1
- package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +54 -1
- package/extensions/dm-subagents/src/runs/background/control-channel.js +190 -1
- package/extensions/dm-subagents/src/runs/background/fleet-view.js +483 -17
- package/extensions/dm-subagents/src/runs/background/notify.js +129 -3
- package/extensions/dm-subagents/src/runs/background/parallel-groups.js +34 -1
- package/extensions/dm-subagents/src/runs/background/result-watcher.js +236 -6
- package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +76 -4
- package/extensions/dm-subagents/src/runs/background/run-status.js +427 -23
- package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +487 -4
- package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +306 -9
- package/extensions/dm-subagents/src/runs/background/subagent-runner.js +2849 -73
- package/extensions/dm-subagents/src/runs/background/top-level-async.js +5 -1
- package/extensions/dm-subagents/src/runs/background/wait.js +206 -11
- package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +1013 -12
- package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +980 -101
- package/extensions/dm-subagents/src/runs/foreground/execution.js +1165 -45
- package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +3157 -222
- package/extensions/dm-subagents/src/runs/shared/acceptance.js +835 -3
- package/extensions/dm-subagents/src/runs/shared/chain-outputs.js +104 -1
- package/extensions/dm-subagents/src/runs/shared/completion-guard.js +116 -3
- package/extensions/dm-subagents/src/runs/shared/dm-args.js +208 -1
- package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +90 -1
- package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +282 -1
- package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +148 -1
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +305 -1
- package/extensions/dm-subagents/src/runs/shared/model-fallback.js +194 -1
- package/extensions/dm-subagents/src/runs/shared/model-scope.js +65 -1
- package/extensions/dm-subagents/src/runs/shared/nested-events.js +851 -8
- package/extensions/dm-subagents/src/runs/shared/nested-path.js +41 -1
- package/extensions/dm-subagents/src/runs/shared/nested-render.js +105 -1
- package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +81 -4
- package/extensions/dm-subagents/src/runs/shared/run-history.js +51 -4
- package/extensions/dm-subagents/src/runs/shared/single-output.js +149 -8
- package/extensions/dm-subagents/src/runs/shared/structured-output.js +58 -1
- package/extensions/dm-subagents/src/runs/shared/subagent-control.js +166 -5
- package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +329 -13
- package/extensions/dm-subagents/src/runs/shared/tool-budget.js +73 -1
- package/extensions/dm-subagents/src/runs/shared/turn-budget.js +47 -4
- package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +196 -1
- package/extensions/dm-subagents/src/runs/shared/worktree.js +435 -3
- package/extensions/dm-subagents/src/shared/artifacts.js +92 -2
- package/extensions/dm-subagents/src/shared/atomic-json.js +55 -1
- package/extensions/dm-subagents/src/shared/child-transcript.js +167 -5
- package/extensions/dm-subagents/src/shared/file-coalescer.js +25 -1
- package/extensions/dm-subagents/src/shared/fork-context.js +147 -4
- package/extensions/dm-subagents/src/shared/formatters.js +98 -7
- package/extensions/dm-subagents/src/shared/jsonl-writer.js +56 -2
- package/extensions/dm-subagents/src/shared/model-info.js +62 -1
- package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.js +68 -1
- package/extensions/dm-subagents/src/shared/session-identity.js +6 -1
- package/extensions/dm-subagents/src/shared/session-tokens.js +39 -2
- package/extensions/dm-subagents/src/shared/settings.js +198 -11
- package/extensions/dm-subagents/src/shared/status-format.js +53 -1
- package/extensions/dm-subagents/src/shared/types.js +184 -6
- package/extensions/dm-subagents/src/shared/utils.js +462 -2
- package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +288 -1
- package/extensions/dm-subagents/src/slash/prompt-workflows.js +297 -7
- package/extensions/dm-subagents/src/slash/slash-bridge.js +118 -1
- package/extensions/dm-subagents/src/slash/slash-commands.js +1287 -31
- package/extensions/dm-subagents/src/slash/slash-live-state.js +240 -4
- package/extensions/dm-subagents/src/tui/render-helpers.js +64 -1
- package/extensions/dm-subagents/src/tui/render.js +1542 -4
- package/extensions/dm-usage/index.js +1294 -9
- package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +40 -9
- package/extensions/greedysearch-dm/bin/cdp-headless.mjs +5 -2
- package/extensions/greedysearch-dm/bin/cdp-visible.mjs +5 -2
- package/extensions/greedysearch-dm/bin/cdp.mjs +896 -30
- package/extensions/greedysearch-dm/bin/gschrome.mjs +30 -2
- package/extensions/greedysearch-dm/bin/kill-visible.mjs +7 -2
- package/extensions/greedysearch-dm/bin/launch-visible.mjs +13 -2
- package/extensions/greedysearch-dm/bin/launch.mjs +282 -10
- package/extensions/greedysearch-dm/bin/mcp.mjs +386 -361
- package/extensions/greedysearch-dm/bin/search.mjs +620 -540
- package/extensions/greedysearch-dm/bin/visible.mjs +22 -2
- package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +329 -579
- package/extensions/greedysearch-dm/extractors/chatgpt.mjs +301 -583
- package/extensions/greedysearch-dm/extractors/common.mjs +408 -32
- package/extensions/greedysearch-dm/extractors/consensus.mjs +376 -365
- package/extensions/greedysearch-dm/extractors/consent.mjs +303 -14
- package/extensions/greedysearch-dm/extractors/gemini.mjs +228 -592
- package/extensions/greedysearch-dm/extractors/google-ai.mjs +78 -499
- package/extensions/greedysearch-dm/extractors/logically.mjs +270 -347
- package/extensions/greedysearch-dm/extractors/perplexity.mjs +243 -581
- package/extensions/greedysearch-dm/extractors/selectors.mjs +32 -1
- package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +130 -317
- package/extensions/greedysearch-dm/index.js +123 -23
- package/extensions/greedysearch-dm/src/fetcher.mjs +576 -2
- package/extensions/greedysearch-dm/src/formatters/results.js +95 -10
- package/extensions/greedysearch-dm/src/formatters/sources.js +57 -1
- package/extensions/greedysearch-dm/src/formatters/synthesis.js +49 -1
- package/extensions/greedysearch-dm/src/github.mjs +222 -7
- package/extensions/greedysearch-dm/src/reddit.mjs +145 -14
- package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +340 -9
- package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +112 -4
- package/extensions/greedysearch-dm/src/search/chrome.mjs +486 -285
- package/extensions/greedysearch-dm/src/search/constants.mjs +109 -8
- package/extensions/greedysearch-dm/src/search/defaults.mjs +10 -1
- package/extensions/greedysearch-dm/src/search/engines.mjs +79 -9
- package/extensions/greedysearch-dm/src/search/fetch-source.mjs +441 -349
- package/extensions/greedysearch-dm/src/search/file-sources.mjs +29 -7
- package/extensions/greedysearch-dm/src/search/minimize.mjs +86 -1
- package/extensions/greedysearch-dm/src/search/output.mjs +51 -5
- package/extensions/greedysearch-dm/src/search/paths.mjs +48 -1
- package/extensions/greedysearch-dm/src/search/pdf.mjs +63 -2
- package/extensions/greedysearch-dm/src/search/port-pid.mjs +69 -1
- package/extensions/greedysearch-dm/src/search/progress.mjs +109 -2
- package/extensions/greedysearch-dm/src/search/query.mjs +21 -1
- package/extensions/greedysearch-dm/src/search/recovery.mjs +49 -1
- package/extensions/greedysearch-dm/src/search/research.mjs +2227 -458
- package/extensions/greedysearch-dm/src/search/scale-aware.mjs +61 -11
- package/extensions/greedysearch-dm/src/search/simple-research.mjs +396 -805
- package/extensions/greedysearch-dm/src/search/sources.mjs +412 -1
- package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +127 -12
- package/extensions/greedysearch-dm/src/search/synthesis.mjs +202 -12
- package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +209 -23
- package/extensions/greedysearch-dm/src/tools/shared.js +226 -10
- package/extensions/greedysearch-dm/src/utils/content.mjs +35 -4
- package/extensions/greedysearch-dm/src/utils/helpers.js +22 -1
- package/extensions/greedysearch-dm/src/utils/node-runtime.mjs +10 -1
- package/extensions/greedysearch-dm/src/utils/system-cmds.mjs +61 -1
- package/package.json +1 -1
|
@@ -1,4 +1,487 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { writeAtomicJson } from "../../shared/atomic-json.js";
|
|
5
|
+
import { formatDuration, shortenPath } from "../../shared/formatters.js";
|
|
6
|
+
import { resolveCurrentSessionId } from "../../shared/session-identity.js";
|
|
7
|
+
import {
|
|
8
|
+
TEMP_ROOT_DIR
|
|
9
|
+
} from "../../shared/types.js";
|
|
10
|
+
export const SCHEDULED_RUNS_DIR = path.join(TEMP_ROOT_DIR, "scheduled-subagent-runs");
|
|
11
|
+
export const SCHEDULED_RUN_ACTIONS = ["schedule", "schedule-list", "schedule-status", "schedule-cancel"];
|
|
12
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
13
|
+
const DEFAULT_MAX_LATENESS_MS = 5 * 60 * 1000;
|
|
14
|
+
const DEFAULT_MAX_PENDING = 20;
|
|
15
|
+
export function isScheduledRunAction(action) {
|
|
16
|
+
return typeof action === "string" && SCHEDULED_RUN_ACTIONS.includes(action);
|
|
17
|
+
}
|
|
18
|
+
export function scheduledRunsEnabled(config) {
|
|
19
|
+
return config.scheduledRuns?.enabled === true;
|
|
20
|
+
}
|
|
21
|
+
export function scheduledRunStorePath(cwd, sessionId, root = SCHEDULED_RUNS_DIR) {
|
|
22
|
+
const digest = createHash("sha256").update(`${path.resolve(cwd)}\x00${sessionId}`).digest("hex").slice(0, 20);
|
|
23
|
+
return path.join(root, `${digest}.json`);
|
|
24
|
+
}
|
|
25
|
+
export function parseScheduledRunTime(schedule, now = Date.now()) {
|
|
26
|
+
const trimmed = schedule.trim();
|
|
27
|
+
const relative = trimmed.match(/^\+(\d+)(s|m|h|d)$/);
|
|
28
|
+
if (relative) {
|
|
29
|
+
const amount = Number(relative[1]);
|
|
30
|
+
if (!Number.isSafeInteger(amount) || amount < 1)
|
|
31
|
+
throw new Error(`Invalid schedule "${schedule}". Relative schedules must be positive, such as "+10m".`);
|
|
32
|
+
const unitMs = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[relative[2]];
|
|
33
|
+
const runAt = now + amount * unitMs;
|
|
34
|
+
if (!Number.isSafeInteger(runAt) || Number.isNaN(new Date(runAt).getTime()))
|
|
35
|
+
throw new Error(`Invalid schedule "${schedule}". Relative delay is too large.`);
|
|
36
|
+
return runAt;
|
|
37
|
+
}
|
|
38
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
|
|
39
|
+
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})$/);
|
|
40
|
+
if (!iso)
|
|
41
|
+
throw new Error(`Invalid schedule "${schedule}". Absolute ISO timestamps must include a timezone, such as "2030-01-01T09:00:00Z".`);
|
|
42
|
+
const year = Number(iso[1]);
|
|
43
|
+
const month = Number(iso[2]);
|
|
44
|
+
const day = Number(iso[3]);
|
|
45
|
+
const hour = Number(iso[4]);
|
|
46
|
+
const minute = Number(iso[5]);
|
|
47
|
+
const second = iso[6] === undefined ? 0 : Number(iso[6]);
|
|
48
|
+
const offset = iso[7];
|
|
49
|
+
const offsetHour = offset === "Z" ? 0 : Number(offset.slice(1, 3));
|
|
50
|
+
const offsetMinute = offset === "Z" ? 0 : Number(offset.slice(4, 6));
|
|
51
|
+
const daysInMonth = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
|
|
52
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) {
|
|
53
|
+
throw new Error(`Invalid schedule "${schedule}". Use a valid future ISO timestamp.`);
|
|
54
|
+
}
|
|
55
|
+
const parsed = new Date(trimmed).getTime();
|
|
56
|
+
if (!Number.isNaN(parsed)) {
|
|
57
|
+
if (parsed <= now)
|
|
58
|
+
throw new Error(`Scheduled time ${new Date(parsed).toISOString()} is in the past.`);
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Invalid schedule "${schedule}". Use a one-shot relative delay like "+10m" or a future ISO timestamp with timezone.`);
|
|
63
|
+
}
|
|
64
|
+
function readStoreData(filePath, cwd, sessionId) {
|
|
65
|
+
if (!fs.existsSync(filePath))
|
|
66
|
+
return { version: 1, cwd, sessionId, jobs: [] };
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
throw new Error(`Failed to parse scheduled subagent store '${filePath}': ${message}`, { cause: error instanceof Error ? error : undefined });
|
|
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);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
version: 1,
|
|
103
|
+
cwd: typeof data.cwd === "string" ? data.cwd : cwd,
|
|
104
|
+
sessionId: typeof data.sessionId === "string" ? data.sessionId : sessionId,
|
|
105
|
+
jobs
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class ScheduledRunStore {
|
|
110
|
+
filePath;
|
|
111
|
+
cwd;
|
|
112
|
+
sessionId;
|
|
113
|
+
constructor(filePath, cwd, sessionId) {
|
|
114
|
+
this.filePath = filePath;
|
|
115
|
+
this.cwd = cwd;
|
|
116
|
+
this.sessionId = sessionId;
|
|
117
|
+
}
|
|
118
|
+
list() {
|
|
119
|
+
return readStoreData(this.filePath, this.cwd, this.sessionId).jobs;
|
|
120
|
+
}
|
|
121
|
+
get(id) {
|
|
122
|
+
return this.list().find((job) => job.id === id);
|
|
123
|
+
}
|
|
124
|
+
mutate(fn) {
|
|
125
|
+
const data = readStoreData(this.filePath, this.cwd, this.sessionId);
|
|
126
|
+
const result = fn(data);
|
|
127
|
+
writeAtomicJson(this.filePath, data);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function resolveMaxLatenessMs(config) {
|
|
132
|
+
const value = config.scheduledRuns?.maxLatenessMs;
|
|
133
|
+
return Number.isInteger(value) && value >= 0 ? value : DEFAULT_MAX_LATENESS_MS;
|
|
134
|
+
}
|
|
135
|
+
function resolveMaxPending(config) {
|
|
136
|
+
const value = config.scheduledRuns?.maxPending;
|
|
137
|
+
return Number.isInteger(value) && value >= 1 ? value : DEFAULT_MAX_PENDING;
|
|
138
|
+
}
|
|
139
|
+
function terminalState(state) {
|
|
140
|
+
return state === "fired" || state === "canceled" || state === "missed" || state === "failed";
|
|
141
|
+
}
|
|
142
|
+
function jobMode(params) {
|
|
143
|
+
if ((params.chain?.length ?? 0) > 0)
|
|
144
|
+
return "chain";
|
|
145
|
+
if ((params.tasks?.length ?? 0) > 0)
|
|
146
|
+
return "parallel";
|
|
147
|
+
return "single";
|
|
148
|
+
}
|
|
149
|
+
function describeScheduledTarget(params) {
|
|
150
|
+
if ((params.chain?.length ?? 0) > 0)
|
|
151
|
+
return `chain (${params.chain.length})`;
|
|
152
|
+
if ((params.tasks?.length ?? 0) > 0)
|
|
153
|
+
return `parallel (${params.tasks.length})`;
|
|
154
|
+
return params.agent ? `agent ${params.agent}` : "subagent run";
|
|
155
|
+
}
|
|
156
|
+
function textResult(text, isError = false) {
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text }],
|
|
159
|
+
...isError ? { isError: true } : {},
|
|
160
|
+
details: { mode: "management", results: [] }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function resolveJobById(jobs, requestedId) {
|
|
164
|
+
const exact = jobs.find((job) => job.id === requestedId);
|
|
165
|
+
if (exact)
|
|
166
|
+
return exact;
|
|
167
|
+
const matches = jobs.filter((job) => job.id.startsWith(requestedId));
|
|
168
|
+
if (matches.length === 1)
|
|
169
|
+
return matches[0];
|
|
170
|
+
if (matches.length > 1)
|
|
171
|
+
throw new Error(`Ambiguous scheduled run id prefix '${requestedId}' matched: ${matches.map((job) => job.id).join(", ")}. Provide a longer id.`);
|
|
172
|
+
throw new Error(`Scheduled run '${requestedId}' not found.`);
|
|
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." };
|
|
183
|
+
if (params.context === "fork")
|
|
184
|
+
return { error: "Scheduled subagent runs require fresh context. Forked parent-session context is not safe at fire time." };
|
|
185
|
+
if (params.async === false)
|
|
186
|
+
return { error: "Scheduled subagent runs are always async; omit async or set async: true." };
|
|
187
|
+
if (params.clarify === true)
|
|
188
|
+
return { error: "Scheduled subagent runs cannot open clarify UI; omit clarify or set clarify: false." };
|
|
189
|
+
const {
|
|
190
|
+
action: _action,
|
|
191
|
+
id: _id,
|
|
192
|
+
runId: _runId,
|
|
193
|
+
dir: _dir,
|
|
194
|
+
index: _index,
|
|
195
|
+
message: _message,
|
|
196
|
+
chainName: _chainName,
|
|
197
|
+
config: _config,
|
|
198
|
+
schedule: _schedule,
|
|
199
|
+
scheduleName: _scheduleName,
|
|
200
|
+
...executionParams
|
|
201
|
+
} = params;
|
|
202
|
+
return { params: { ...executionParams, async: true, clarify: false, context: "fresh" } };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export class ScheduledRunManager {
|
|
206
|
+
store;
|
|
207
|
+
ctx;
|
|
208
|
+
timers = new Map;
|
|
209
|
+
storeRoot;
|
|
210
|
+
now;
|
|
211
|
+
randomId;
|
|
212
|
+
timersApi;
|
|
213
|
+
deps;
|
|
214
|
+
constructor(deps) {
|
|
215
|
+
this.deps = deps;
|
|
216
|
+
this.storeRoot = deps.storeRoot ?? SCHEDULED_RUNS_DIR;
|
|
217
|
+
this.now = deps.now ?? Date.now;
|
|
218
|
+
this.randomId = deps.randomId ?? (() => randomUUID().slice(0, 8));
|
|
219
|
+
this.timersApi = deps.timers ?? globalThis;
|
|
220
|
+
}
|
|
221
|
+
bindSession(ctx) {
|
|
222
|
+
this.stopTimers();
|
|
223
|
+
this.ctx = ctx;
|
|
224
|
+
if (!scheduledRunsEnabled(this.deps.config)) {
|
|
225
|
+
this.store = undefined;
|
|
226
|
+
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();
|
|
231
|
+
}
|
|
232
|
+
stop() {
|
|
233
|
+
this.stopTimers();
|
|
234
|
+
this.store = undefined;
|
|
235
|
+
this.ctx = undefined;
|
|
236
|
+
}
|
|
237
|
+
async handleToolCall(params, ctx) {
|
|
238
|
+
this.ctx = ctx;
|
|
239
|
+
try {
|
|
240
|
+
if (!scheduledRunsEnabled(this.deps.config)) {
|
|
241
|
+
return textResult('Scheduled subagent runs are disabled. Set { "scheduledRuns": { "enabled": true } } in ~/.dm/agent/extensions/subagent/config.json, then reload Pi. Schedule only explicit delayed runs the user asked for.', true);
|
|
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);
|
|
247
|
+
switch (params.action) {
|
|
248
|
+
case "schedule":
|
|
249
|
+
return this.createJob(params, ctx);
|
|
250
|
+
case "schedule-list":
|
|
251
|
+
return this.listJobs();
|
|
252
|
+
case "schedule-status":
|
|
253
|
+
return this.statusJob(params);
|
|
254
|
+
case "schedule-cancel":
|
|
255
|
+
return this.cancelJob(params);
|
|
256
|
+
default:
|
|
257
|
+
return textResult(`Unknown scheduled-run action: ${params.action}`, true);
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
261
|
+
return textResult(message, true);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
createJob(params, ctx) {
|
|
265
|
+
const store = this.requireStore();
|
|
266
|
+
const sanitized = sanitizeScheduledParams(params);
|
|
267
|
+
if (sanitized.error)
|
|
268
|
+
return textResult(sanitized.error, true);
|
|
269
|
+
const scheduleInput = params.schedule.trim();
|
|
270
|
+
const runAt = parseScheduledRunTime(scheduleInput, this.now());
|
|
271
|
+
const pendingCount = store.list().filter((job) => job.state === "scheduled" || job.state === "running").length;
|
|
272
|
+
const maxPending = resolveMaxPending(this.deps.config);
|
|
273
|
+
if (pendingCount >= maxPending)
|
|
274
|
+
return textResult(`Scheduled subagent limit reached (${pendingCount}/${maxPending} pending or running). Cancel an existing scheduled run before adding another.`, true);
|
|
275
|
+
const id = this.randomId();
|
|
276
|
+
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
277
|
+
const scheduleName = params.scheduleName?.trim();
|
|
278
|
+
const executionParams = sanitized.params;
|
|
279
|
+
const now = this.now();
|
|
280
|
+
const job = {
|
|
281
|
+
id,
|
|
282
|
+
name: scheduleName || describeScheduledTarget(executionParams),
|
|
283
|
+
schedule: scheduleInput,
|
|
284
|
+
runAt,
|
|
285
|
+
state: "scheduled",
|
|
286
|
+
createdAt: now,
|
|
287
|
+
updatedAt: now,
|
|
288
|
+
cwd: ctx.cwd,
|
|
289
|
+
sessionId,
|
|
290
|
+
params: executionParams
|
|
291
|
+
};
|
|
292
|
+
store.mutate((data) => {
|
|
293
|
+
data.jobs.push(job);
|
|
294
|
+
});
|
|
295
|
+
this.arm(job);
|
|
296
|
+
return textResult([
|
|
297
|
+
`Scheduled subagent run ${job.id}.`,
|
|
298
|
+
`Name: ${job.name}`,
|
|
299
|
+
`When: ${new Date(job.runAt).toISOString()}`,
|
|
300
|
+
`Mode: ${jobMode(executionParams)}`,
|
|
301
|
+
`Context: fresh (scheduled runs never fork parent-session context)`,
|
|
302
|
+
`Status: subagent({ action: "schedule-status", id: "${job.id}" })`,
|
|
303
|
+
`Cancel before it fires: subagent({ action: "schedule-cancel", id: "${job.id}" })`
|
|
304
|
+
].join(`
|
|
305
|
+
`));
|
|
306
|
+
}
|
|
307
|
+
listJobs() {
|
|
308
|
+
const jobs = this.requireStore().list().sort((left, right) => left.runAt - right.runAt);
|
|
309
|
+
if (jobs.length === 0)
|
|
310
|
+
return textResult("No scheduled subagent runs for this session.");
|
|
311
|
+
const lines = [`Scheduled subagent runs: ${jobs.length}`, ""];
|
|
312
|
+
for (const job of jobs) {
|
|
313
|
+
const parts = [job.id, job.state, new Date(job.runAt).toISOString(), job.name];
|
|
314
|
+
if (job.lastRunId)
|
|
315
|
+
parts.push(`run ${job.lastRunId}`);
|
|
316
|
+
if (job.lastError)
|
|
317
|
+
parts.push(`error: ${job.lastError}`);
|
|
318
|
+
lines.push(`- ${parts.join(" | ")}`);
|
|
319
|
+
}
|
|
320
|
+
return textResult(lines.join(`
|
|
321
|
+
`));
|
|
322
|
+
}
|
|
323
|
+
statusJob(params) {
|
|
324
|
+
const requestedId = params.id ?? params.runId;
|
|
325
|
+
if (!requestedId)
|
|
326
|
+
return textResult("action='schedule-status' requires id.", true);
|
|
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);
|
|
351
|
+
const store = this.requireStore();
|
|
352
|
+
const job = resolveJobById(store.list(), requestedId);
|
|
353
|
+
if (job.state === "running")
|
|
354
|
+
return textResult(`Scheduled run ${job.id} already launched async run ${job.lastRunId ?? "unknown"}; interrupt that async run instead.`, true);
|
|
355
|
+
if (terminalState(job.state))
|
|
356
|
+
return textResult(`Scheduled run ${job.id} is already ${job.state}.`, true);
|
|
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}.`);
|
|
368
|
+
}
|
|
369
|
+
rearmScheduledJobs() {
|
|
370
|
+
const store = this.requireStore();
|
|
371
|
+
const now = this.now();
|
|
372
|
+
const maxLatenessMs = resolveMaxLatenessMs(this.deps.config);
|
|
373
|
+
const dueToMiss = store.list().filter((job) => job.state === "scheduled" && job.runAt + maxLatenessMs < now);
|
|
374
|
+
if (dueToMiss.length > 0) {
|
|
375
|
+
store.mutate((data) => {
|
|
376
|
+
for (const missed of dueToMiss) {
|
|
377
|
+
const job = data.jobs.find((candidate) => candidate.id === missed.id);
|
|
378
|
+
if (!job || job.state !== "scheduled")
|
|
379
|
+
continue;
|
|
380
|
+
job.state = "missed";
|
|
381
|
+
job.updatedAt = now;
|
|
382
|
+
job.lastError = `Missed scheduled time by more than ${formatDuration(maxLatenessMs)} while DM was not available.`;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
for (const job of store.list()) {
|
|
387
|
+
if (job.state === "scheduled")
|
|
388
|
+
this.arm(job);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
arm(job) {
|
|
392
|
+
this.clearTimer(job.id);
|
|
393
|
+
const delayMs = Math.max(0, job.runAt - this.now());
|
|
394
|
+
const timer = this.timersApi.setTimeout(() => {
|
|
395
|
+
this.fire(job.id);
|
|
396
|
+
}, Math.min(delayMs, MAX_TIMER_DELAY_MS));
|
|
397
|
+
timer.unref?.();
|
|
398
|
+
this.timers.set(job.id, timer);
|
|
399
|
+
}
|
|
400
|
+
async fire(jobId) {
|
|
401
|
+
this.clearTimer(jobId);
|
|
402
|
+
const store = this.store;
|
|
403
|
+
const ctx = this.ctx;
|
|
404
|
+
if (!store || !ctx)
|
|
405
|
+
return;
|
|
406
|
+
let job = store.get(jobId);
|
|
407
|
+
if (!job || job.state !== "scheduled")
|
|
408
|
+
return;
|
|
409
|
+
const now = this.now();
|
|
410
|
+
if (now < job.runAt) {
|
|
411
|
+
this.arm(job);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const maxLatenessMs = resolveMaxLatenessMs(this.deps.config);
|
|
415
|
+
if (job.runAt + maxLatenessMs < now) {
|
|
416
|
+
store.mutate((data) => {
|
|
417
|
+
const stored = data.jobs.find((candidate) => candidate.id === jobId);
|
|
418
|
+
if (!stored || stored.state !== "scheduled")
|
|
419
|
+
return;
|
|
420
|
+
stored.state = "missed";
|
|
421
|
+
stored.updatedAt = now;
|
|
422
|
+
stored.lastError = `Missed scheduled time by more than ${formatDuration(maxLatenessMs)}.`;
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
store.mutate((data) => {
|
|
427
|
+
const stored = data.jobs.find((candidate) => candidate.id === jobId);
|
|
428
|
+
if (!stored || stored.state !== "scheduled")
|
|
429
|
+
return;
|
|
430
|
+
stored.state = "running";
|
|
431
|
+
stored.firedAt = now;
|
|
432
|
+
stored.updatedAt = now;
|
|
433
|
+
});
|
|
434
|
+
job = store.get(jobId);
|
|
435
|
+
if (!job || job.state !== "running")
|
|
436
|
+
return;
|
|
437
|
+
const controller = new AbortController;
|
|
438
|
+
try {
|
|
439
|
+
const result = await this.deps.launch(job.params, ctx, controller.signal);
|
|
440
|
+
const launchRunId = result.details?.asyncId ?? result.details?.runId;
|
|
441
|
+
store.mutate((data) => {
|
|
442
|
+
const stored = data.jobs.find((candidate) => candidate.id === jobId);
|
|
443
|
+
if (!stored)
|
|
444
|
+
return;
|
|
445
|
+
stored.updatedAt = this.now();
|
|
446
|
+
if (result.isError || !launchRunId) {
|
|
447
|
+
stored.state = "failed";
|
|
448
|
+
stored.lastError = result.content.find((item) => item.type === "text")?.text ?? "Scheduled subagent launch failed.";
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
stored.state = "fired";
|
|
452
|
+
stored.lastRunId = launchRunId;
|
|
453
|
+
stored.lastAsyncDir = result.details?.asyncDir;
|
|
454
|
+
});
|
|
455
|
+
} catch (error) {
|
|
456
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
457
|
+
store.mutate((data) => {
|
|
458
|
+
const stored = data.jobs.find((candidate) => candidate.id === jobId);
|
|
459
|
+
if (!stored)
|
|
460
|
+
return;
|
|
461
|
+
stored.state = "failed";
|
|
462
|
+
stored.lastError = message;
|
|
463
|
+
stored.updatedAt = this.now();
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
requireStore() {
|
|
468
|
+
if (!this.store)
|
|
469
|
+
throw new Error("Scheduled subagent store is not bound to a session.");
|
|
470
|
+
return this.store;
|
|
471
|
+
}
|
|
472
|
+
clearTimer(jobId) {
|
|
473
|
+
const timer = this.timers.get(jobId);
|
|
474
|
+
if (!timer)
|
|
475
|
+
return;
|
|
476
|
+
this.timersApi.clearTimeout(timer);
|
|
477
|
+
this.timers.delete(jobId);
|
|
478
|
+
}
|
|
479
|
+
stopTimers() {
|
|
480
|
+
for (const timer of this.timers.values())
|
|
481
|
+
this.timersApi.clearTimeout(timer);
|
|
482
|
+
this.timers.clear();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
export function createScheduledRunManager(deps) {
|
|
486
|
+
return new ScheduledRunManager(deps);
|
|
487
|
+
}
|