@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.
Files changed (197) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +70 -123
  3. package/extensions/dm-9router-ext/src/index.js +410 -5
  4. package/extensions/dm-caveman/extensions/caveman.js +283 -12
  5. package/extensions/dm-cliproxy/index.js +182 -2
  6. package/extensions/dm-cliproxy/scripts/check-config-migration.js +66 -8
  7. package/extensions/dm-cliproxy/src/apply.js +228 -1
  8. package/extensions/dm-cliproxy/src/cache.js +42 -1
  9. package/extensions/dm-cliproxy/src/commands.js +50 -2
  10. package/extensions/dm-cliproxy/src/compat.js +81 -1
  11. package/extensions/dm-cliproxy/src/config.js +192 -2
  12. package/extensions/dm-cliproxy/src/conflicts.js +46 -1
  13. package/extensions/dm-cliproxy/src/fetch-models.js +190 -1
  14. package/extensions/dm-cliproxy/src/fetch-usage.js +41 -1
  15. package/extensions/dm-cliproxy/src/log.js +23 -1
  16. package/extensions/dm-cliproxy/src/status-quota.js +77 -1
  17. package/extensions/dm-cliproxy/src/ui-frame.js +50 -1
  18. package/extensions/dm-cliproxy/src/ui-hub/hub.js +199 -2
  19. package/extensions/dm-cliproxy/src/ui-hub/index.js +26 -2
  20. package/extensions/dm-cliproxy/src/ui-hub/shell.js +46 -1
  21. package/extensions/dm-cliproxy/src/ui-hub/view-diagnostics.js +69 -1
  22. package/extensions/dm-cliproxy/src/ui-hub/view-models.js +379 -1
  23. package/extensions/dm-cliproxy/src/ui-hub/view-usage.js +106 -1
  24. package/extensions/dm-cliproxy/src/ui-picker/catalog.js +41 -1
  25. package/extensions/dm-cliproxy/src/ui-picker/mutate.js +115 -1
  26. package/extensions/dm-cliproxy/src/ui-picker/prompt-confirm.js +35 -1
  27. package/extensions/dm-cliproxy/src/ui-picker/prompt-name.js +62 -1
  28. package/extensions/dm-cliproxy/src/ui-picker/providers.js +45 -1
  29. package/extensions/dm-cliproxy/src/ui-picker/render-text.js +34 -1
  30. package/extensions/dm-cliproxy/src/ui-picker/rows.js +57 -1
  31. package/extensions/dm-cliproxy/src/ui-setup.js +260 -2
  32. package/extensions/dm-cliproxy/src/ui-usage.js +151 -1
  33. package/extensions/dm-cliproxy/src/usage-shared-cache.js +97 -1
  34. package/extensions/dm-context/src/context.js +144 -1
  35. package/extensions/dm-context/src/index.js +339 -7
  36. package/extensions/dm-context/src/utils.js +9 -1
  37. package/extensions/dm-cua/bin/browser-cua.mjs +73 -8
  38. package/extensions/dm-cua/index.js +75 -6
  39. package/extensions/dm-cua/src/browser-cua-lib.mjs +490 -6
  40. package/extensions/dm-cua/src/browser-install.mjs +331 -2
  41. package/extensions/dm-fff/src/index.js +688 -12
  42. package/extensions/dm-fff/src/query.js +60 -1
  43. package/extensions/dm-goal/src/goal.js +894 -23
  44. package/extensions/dm-image2/index.js +103 -9
  45. package/extensions/dm-image2/src/image-lib.mjs +1275 -8
  46. package/extensions/dm-subagents/install.mjs +62 -8
  47. package/extensions/dm-subagents/src/agents/agent-management.js +1190 -36
  48. package/extensions/dm-subagents/src/agents/agent-memory.js +216 -6
  49. package/extensions/dm-subagents/src/agents/agent-scope.js +5 -1
  50. package/extensions/dm-subagents/src/agents/agent-selection.js +20 -1
  51. package/extensions/dm-subagents/src/agents/agent-serializer.js +120 -5
  52. package/extensions/dm-subagents/src/agents/agents.js +1139 -11
  53. package/extensions/dm-subagents/src/agents/chain-serializer.js +299 -11
  54. package/extensions/dm-subagents/src/agents/frontmatter.js +65 -6
  55. package/extensions/dm-subagents/src/agents/identity.js +29 -1
  56. package/extensions/dm-subagents/src/agents/proactive-skills.js +141 -1
  57. package/extensions/dm-subagents/src/agents/skills.js +614 -6
  58. package/extensions/dm-subagents/src/extension/config.js +35 -2
  59. package/extensions/dm-subagents/src/extension/control-notices.js +69 -4
  60. package/extensions/dm-subagents/src/extension/doctor.js +172 -15
  61. package/extensions/dm-subagents/src/extension/fanout-child.js +158 -231
  62. package/extensions/dm-subagents/src/extension/index.js +521 -359
  63. package/extensions/dm-subagents/src/extension/rpc.js +266 -7
  64. package/extensions/dm-subagents/src/extension/schemas.js +275 -1
  65. package/extensions/dm-subagents/src/extension/tool-description.js +111 -6
  66. package/extensions/dm-subagents/src/intercom/intercom-bridge.js +126 -4
  67. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +452 -5
  68. package/extensions/dm-subagents/src/intercom/result-intercom.js +319 -3
  69. package/extensions/dm-subagents/src/profiles/profiles.js +458 -3
  70. package/extensions/dm-subagents/src/runs/background/async-execution.js +834 -40
  71. package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +435 -14
  72. package/extensions/dm-subagents/src/runs/background/async-resume.js +334 -8
  73. package/extensions/dm-subagents/src/runs/background/async-status.js +313 -12
  74. package/extensions/dm-subagents/src/runs/background/chain-append.js +245 -2
  75. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +136 -1
  76. package/extensions/dm-subagents/src/runs/background/completion-batcher.js +94 -1
  77. package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +54 -1
  78. package/extensions/dm-subagents/src/runs/background/control-channel.js +190 -1
  79. package/extensions/dm-subagents/src/runs/background/fleet-view.js +483 -17
  80. package/extensions/dm-subagents/src/runs/background/notify.js +129 -3
  81. package/extensions/dm-subagents/src/runs/background/parallel-groups.js +34 -1
  82. package/extensions/dm-subagents/src/runs/background/result-watcher.js +236 -6
  83. package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +76 -4
  84. package/extensions/dm-subagents/src/runs/background/run-status.js +427 -23
  85. package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +487 -4
  86. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +306 -9
  87. package/extensions/dm-subagents/src/runs/background/subagent-runner.js +2849 -73
  88. package/extensions/dm-subagents/src/runs/background/top-level-async.js +5 -1
  89. package/extensions/dm-subagents/src/runs/background/wait.js +206 -11
  90. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +1013 -12
  91. package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +980 -101
  92. package/extensions/dm-subagents/src/runs/foreground/execution.js +1165 -45
  93. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +3157 -222
  94. package/extensions/dm-subagents/src/runs/shared/acceptance.js +835 -3
  95. package/extensions/dm-subagents/src/runs/shared/chain-outputs.js +104 -1
  96. package/extensions/dm-subagents/src/runs/shared/completion-guard.js +116 -3
  97. package/extensions/dm-subagents/src/runs/shared/dm-args.js +208 -1
  98. package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +90 -1
  99. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +282 -1
  100. package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +148 -1
  101. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +305 -1
  102. package/extensions/dm-subagents/src/runs/shared/model-fallback.js +194 -1
  103. package/extensions/dm-subagents/src/runs/shared/model-scope.js +65 -1
  104. package/extensions/dm-subagents/src/runs/shared/nested-events.js +851 -8
  105. package/extensions/dm-subagents/src/runs/shared/nested-path.js +41 -1
  106. package/extensions/dm-subagents/src/runs/shared/nested-render.js +105 -1
  107. package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +81 -4
  108. package/extensions/dm-subagents/src/runs/shared/run-history.js +51 -4
  109. package/extensions/dm-subagents/src/runs/shared/single-output.js +149 -8
  110. package/extensions/dm-subagents/src/runs/shared/structured-output.js +58 -1
  111. package/extensions/dm-subagents/src/runs/shared/subagent-control.js +166 -5
  112. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +329 -13
  113. package/extensions/dm-subagents/src/runs/shared/tool-budget.js +73 -1
  114. package/extensions/dm-subagents/src/runs/shared/turn-budget.js +47 -4
  115. package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +196 -1
  116. package/extensions/dm-subagents/src/runs/shared/worktree.js +435 -3
  117. package/extensions/dm-subagents/src/shared/artifacts.js +92 -2
  118. package/extensions/dm-subagents/src/shared/atomic-json.js +55 -1
  119. package/extensions/dm-subagents/src/shared/child-transcript.js +167 -5
  120. package/extensions/dm-subagents/src/shared/file-coalescer.js +25 -1
  121. package/extensions/dm-subagents/src/shared/fork-context.js +147 -4
  122. package/extensions/dm-subagents/src/shared/formatters.js +98 -7
  123. package/extensions/dm-subagents/src/shared/jsonl-writer.js +56 -2
  124. package/extensions/dm-subagents/src/shared/model-info.js +62 -1
  125. package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.js +68 -1
  126. package/extensions/dm-subagents/src/shared/session-identity.js +6 -1
  127. package/extensions/dm-subagents/src/shared/session-tokens.js +39 -2
  128. package/extensions/dm-subagents/src/shared/settings.js +198 -11
  129. package/extensions/dm-subagents/src/shared/status-format.js +53 -1
  130. package/extensions/dm-subagents/src/shared/types.js +184 -6
  131. package/extensions/dm-subagents/src/shared/utils.js +462 -2
  132. package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +288 -1
  133. package/extensions/dm-subagents/src/slash/prompt-workflows.js +297 -7
  134. package/extensions/dm-subagents/src/slash/slash-bridge.js +118 -1
  135. package/extensions/dm-subagents/src/slash/slash-commands.js +1287 -31
  136. package/extensions/dm-subagents/src/slash/slash-live-state.js +240 -4
  137. package/extensions/dm-subagents/src/tui/render-helpers.js +64 -1
  138. package/extensions/dm-subagents/src/tui/render.js +1542 -4
  139. package/extensions/dm-usage/index.js +1294 -9
  140. package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +40 -9
  141. package/extensions/greedysearch-dm/bin/cdp-headless.mjs +5 -2
  142. package/extensions/greedysearch-dm/bin/cdp-visible.mjs +5 -2
  143. package/extensions/greedysearch-dm/bin/cdp.mjs +896 -30
  144. package/extensions/greedysearch-dm/bin/gschrome.mjs +30 -2
  145. package/extensions/greedysearch-dm/bin/kill-visible.mjs +7 -2
  146. package/extensions/greedysearch-dm/bin/launch-visible.mjs +13 -2
  147. package/extensions/greedysearch-dm/bin/launch.mjs +282 -10
  148. package/extensions/greedysearch-dm/bin/mcp.mjs +386 -361
  149. package/extensions/greedysearch-dm/bin/search.mjs +620 -540
  150. package/extensions/greedysearch-dm/bin/visible.mjs +22 -2
  151. package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +329 -579
  152. package/extensions/greedysearch-dm/extractors/chatgpt.mjs +301 -583
  153. package/extensions/greedysearch-dm/extractors/common.mjs +408 -32
  154. package/extensions/greedysearch-dm/extractors/consensus.mjs +376 -365
  155. package/extensions/greedysearch-dm/extractors/consent.mjs +303 -14
  156. package/extensions/greedysearch-dm/extractors/gemini.mjs +228 -592
  157. package/extensions/greedysearch-dm/extractors/google-ai.mjs +78 -499
  158. package/extensions/greedysearch-dm/extractors/logically.mjs +270 -347
  159. package/extensions/greedysearch-dm/extractors/perplexity.mjs +243 -581
  160. package/extensions/greedysearch-dm/extractors/selectors.mjs +32 -1
  161. package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +130 -317
  162. package/extensions/greedysearch-dm/index.js +123 -23
  163. package/extensions/greedysearch-dm/src/fetcher.mjs +576 -2
  164. package/extensions/greedysearch-dm/src/formatters/results.js +95 -10
  165. package/extensions/greedysearch-dm/src/formatters/sources.js +57 -1
  166. package/extensions/greedysearch-dm/src/formatters/synthesis.js +49 -1
  167. package/extensions/greedysearch-dm/src/github.mjs +222 -7
  168. package/extensions/greedysearch-dm/src/reddit.mjs +145 -14
  169. package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +340 -9
  170. package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +112 -4
  171. package/extensions/greedysearch-dm/src/search/chrome.mjs +486 -285
  172. package/extensions/greedysearch-dm/src/search/constants.mjs +109 -8
  173. package/extensions/greedysearch-dm/src/search/defaults.mjs +10 -1
  174. package/extensions/greedysearch-dm/src/search/engines.mjs +79 -9
  175. package/extensions/greedysearch-dm/src/search/fetch-source.mjs +441 -349
  176. package/extensions/greedysearch-dm/src/search/file-sources.mjs +29 -7
  177. package/extensions/greedysearch-dm/src/search/minimize.mjs +86 -1
  178. package/extensions/greedysearch-dm/src/search/output.mjs +51 -5
  179. package/extensions/greedysearch-dm/src/search/paths.mjs +48 -1
  180. package/extensions/greedysearch-dm/src/search/pdf.mjs +63 -2
  181. package/extensions/greedysearch-dm/src/search/port-pid.mjs +69 -1
  182. package/extensions/greedysearch-dm/src/search/progress.mjs +109 -2
  183. package/extensions/greedysearch-dm/src/search/query.mjs +21 -1
  184. package/extensions/greedysearch-dm/src/search/recovery.mjs +49 -1
  185. package/extensions/greedysearch-dm/src/search/research.mjs +2227 -458
  186. package/extensions/greedysearch-dm/src/search/scale-aware.mjs +61 -11
  187. package/extensions/greedysearch-dm/src/search/simple-research.mjs +396 -805
  188. package/extensions/greedysearch-dm/src/search/sources.mjs +412 -1
  189. package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +127 -12
  190. package/extensions/greedysearch-dm/src/search/synthesis.mjs +202 -12
  191. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +209 -23
  192. package/extensions/greedysearch-dm/src/tools/shared.js +226 -10
  193. package/extensions/greedysearch-dm/src/utils/content.mjs +35 -4
  194. package/extensions/greedysearch-dm/src/utils/helpers.js +22 -1
  195. package/extensions/greedysearch-dm/src/utils/node-runtime.mjs +10 -1
  196. package/extensions/greedysearch-dm/src/utils/system-cmds.mjs +61 -1
  197. package/package.json +1 -1
@@ -1,4 +1,487 @@
1
- import{createHash as o,randomUUID as i}from"node:crypto";import*as M from"node:fs";import*as T from"node:path";import*as g from"node:fs";import*as F from"node:path";var h=[10,25,50,100,200,500,1000,2000,4000],m=new Set(["EACCES","EBUSY","EPERM"]),I=typeof SharedArrayBuffer<"u"?new SharedArrayBuffer(4):void 0,j=I?new Int32Array(I):void 0;function c(Q){if(Q<=0)return;if(j)try{Atomics.wait(j,0,0,Q);return}catch{}let Z=Date.now()+Q;while(Date.now()<Z);}function u(Q){let Z=Q?.code;return typeof Z==="string"&&m.has(Z)}function p(Q,Z,$,Y,X){for(let V=0;;V++)try{Q.renameSync(Z,$);return}catch(W){let J=Y[V];if(J===void 0||!u(W))throw W;X(J)}}function d(Q={}){let Z=Q.fs??g,$=Q.now??Date.now,Y=Q.pid??process.pid,X=Q.random??Math.random,W=Q.retryRenameErrors??process.platform==="win32"?Q.retryDelaysMs??h:[],J=Q.wait??c;return(B,q)=>{Z.mkdirSync(F.dirname(B),{recursive:!0});let G=F.join(F.dirname(B),`.${F.basename(B)}.${Y}.${$()}.${X().toString(36).slice(2)}.tmp`);try{Z.writeFileSync(G,JSON.stringify(q,null,2),"utf-8"),p(Z,G,B,W,J)}finally{Z.rmSync(G,{force:!0})}}}var w=d();import*as L from"node:os";import*as O from"node:path";function E(Q){return Q.trim().replace(/[^A-Za-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"unknown"}function n(Q){let Z=Q?.env??process.env,$=Q&&Object.hasOwn(Q,"getuid")?Q.getuid:process.getuid?.bind(process);if(typeof $==="function")return`uid-${$()}`;for(let W of["USERNAME","USER","LOGNAME"]){let J=Z[W];if(J)return`user-${E(J)}`}let Y=Q&&Object.hasOwn(Q,"userInfo")?Q.userInfo:L.userInfo;try{let W=Y?.().username;if(W)return`user-${E(W)}`}catch{}let X=Z.USERPROFILE??Z.HOME;if(X)return`home-${E(X)}`;let V=Q&&Object.hasOwn(Q,"homedir")?Q.homedir:L.homedir;try{let W=V?.();if(W)return`home-${E(W)}`}catch{}return"shared"}var _=O.join(L.tmpdir(),`dm-subagents-${n()}`),LQ=O.join(_,"async-subagent-results"),OQ=O.join(_,"async-subagent-runs"),l=O.join(_,"chain-runs"),_Q=O.join(_,"artifacts");function C(Q){if(Q<1000)return`${Q}ms`;if(Q<60000)return`${(Q/1000).toFixed(1)}s`;return`${Math.floor(Q/60000)}m${Math.floor(Q%60000/1000)}s`}function S(Q){let Z=process.env.HOME;if(Z&&Q.startsWith(Z))return`~${Q.slice(Z.length)}`;return Q}function R(Q){let Z=Q.getSessionFile()??Q.getSessionId();if(!Z)throw Error("Current session identity is unavailable.");return Z}var P=T.join(_,"scheduled-subagent-runs"),r=["schedule","schedule-list","schedule-status","schedule-cancel"],a=2147483647,s=300000,t=20;function mQ(Q){return typeof Q==="string"&&r.includes(Q)}function D(Q){return Q.scheduledRuns?.enabled===!0}function e(Q,Z,$=P){let Y=o("sha256").update(`${T.resolve(Q)}\x00${Z}`).digest("hex").slice(0,20);return T.join($,`${Y}.json`)}function QQ(Q,Z=Date.now()){let $=Q.trim(),Y=$.match(/^\+(\d+)(s|m|h|d)$/);if(Y){let X=Number(Y[1]);if(!Number.isSafeInteger(X)||X<1)throw Error(`Invalid schedule "${Q}". Relative schedules must be positive, such as "+10m".`);let V={s:1000,m:60000,h:3600000,d:86400000}[Y[2]],W=Z+X*V;if(!Number.isSafeInteger(W)||Number.isNaN(new Date(W).getTime()))throw Error(`Invalid schedule "${Q}". Relative delay is too large.`);return W}if(/^\d{4}-\d{2}-\d{2}T/.test($)){let X=$.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/);if(!X)throw Error(`Invalid schedule "${Q}". Absolute ISO timestamps must include a timezone, such as "2030-01-01T09:00:00Z".`);let V=Number(X[1]),W=Number(X[2]),J=Number(X[3]),B=Number(X[4]),q=Number(X[5]),G=X[6]===void 0?0:Number(X[6]),K=X[7],A=K==="Z"?0:Number(K.slice(1,3)),H=K==="Z"?0:Number(K.slice(4,6)),z=W>=1&&W<=12?new Date(Date.UTC(V,W,0)).getUTCDate():0;if(W<1||W>12||J<1||J>z||B>23||q>59||G>59||A>23||H>59)throw Error(`Invalid schedule "${Q}". Use a valid future ISO timestamp.`);let N=new Date($).getTime();if(!Number.isNaN(N)){if(N<=Z)throw Error(`Scheduled time ${new Date(N).toISOString()} is in the past.`);return N}}throw Error(`Invalid schedule "${Q}". Use a one-shot relative delay like "+10m" or a future ISO timestamp with timezone.`)}function y(Q,Z,$){if(!M.existsSync(Q))return{version:1,cwd:Z,sessionId:$,jobs:[]};let Y;try{Y=JSON.parse(M.readFileSync(Q,"utf-8"))}catch(J){let B=J instanceof Error?J.message:String(J);throw Error(`Failed to parse scheduled subagent store '${Q}': ${B}`,{cause:J instanceof Error?J:void 0})}if(!Y||typeof Y!=="object"||Array.isArray(Y))throw Error(`Scheduled subagent store '${Q}' must be a JSON object.`);let X=Y;if(X.version!==1)throw Error(`Unsupported scheduled subagent store version in '${Q}'.`);if(!Array.isArray(X.jobs))throw Error(`Scheduled subagent store '${Q}' must contain a jobs array.`);let V=[],W=new Set(["scheduled","running","fired","canceled","missed","failed"]);for(let[J,B]of X.jobs.entries()){if(!B||typeof B!=="object"||Array.isArray(B))throw Error(`Scheduled subagent store '${Q}' job ${J} must be an object.`);let q=B;if(typeof q.id!=="string"||typeof q.name!=="string"||typeof q.schedule!=="string"||typeof q.cwd!=="string"||typeof q.sessionId!=="string")throw Error(`Scheduled subagent store '${Q}' job ${J} has invalid string fields.`);if([q.runAt,q.createdAt,q.updatedAt].some((K)=>typeof K!=="number"||!Number.isFinite(K)||Number.isNaN(new Date(K).getTime())))throw Error(`Scheduled subagent store '${Q}' job ${J} has invalid timestamps.`);if(!q.state||!W.has(q.state))throw Error(`Scheduled subagent store '${Q}' job ${J} has invalid state.`);if(!q.params||typeof q.params!=="object"||Array.isArray(q.params))throw Error(`Scheduled subagent store '${Q}' job ${J} has invalid params.`);V.push(q)}return{version:1,cwd:typeof X.cwd==="string"?X.cwd:Z,sessionId:typeof X.sessionId==="string"?X.sessionId:$,jobs:V}}class f{filePath;cwd;sessionId;constructor(Q,Z,$){this.filePath=Q,this.cwd=Z,this.sessionId=$}list(){return y(this.filePath,this.cwd,this.sessionId).jobs}get(Q){return this.list().find((Z)=>Z.id===Q)}mutate(Q){let Z=y(this.filePath,this.cwd,this.sessionId),$=Q(Z);return w(this.filePath,Z),$}}function x(Q){let Z=Q.scheduledRuns?.maxLatenessMs;return Number.isInteger(Z)&&Z>=0?Z:s}function ZQ(Q){let Z=Q.scheduledRuns?.maxPending;return Number.isInteger(Z)&&Z>=1?Z:t}function $Q(Q){return Q==="fired"||Q==="canceled"||Q==="missed"||Q==="failed"}function b(Q){if((Q.chain?.length??0)>0)return"chain";if((Q.tasks?.length??0)>0)return"parallel";return"single"}function YQ(Q){if((Q.chain?.length??0)>0)return`chain (${Q.chain.length})`;if((Q.tasks?.length??0)>0)return`parallel (${Q.tasks.length})`;return Q.agent?`agent ${Q.agent}`:"subagent run"}function U(Q,Z=!1){return{content:[{type:"text",text:Q}],...Z?{isError:!0}:{},details:{mode:"management",results:[]}}}function v(Q,Z){let $=Q.find((X)=>X.id===Z);if($)return $;let Y=Q.filter((X)=>X.id.startsWith(Z));if(Y.length===1)return Y[0];if(Y.length>1)throw Error(`Ambiguous scheduled run id prefix '${Z}' matched: ${Y.map((X)=>X.id).join(", ")}. Provide a longer id.`);throw Error(`Scheduled run '${Z}' not found.`)}function XQ(Q){let Z=(Q.chain?.length??0)>0,$=(Q.tasks?.length??0)>0,Y=!Z&&!$&&Boolean(Q.agent);if(Number(Z)+Number($)+Number(Y)!==1)return{error:"action='schedule' requires exactly one execution mode: agent, tasks, or chain."};if(!Q.schedule?.trim())return{error:"action='schedule' requires schedule, such as '+10m' or a future ISO timestamp."};if(Q.context==="fork")return{error:"Scheduled subagent runs require fresh context. Forked parent-session context is not safe at fire time."};if(Q.async===!1)return{error:"Scheduled subagent runs are always async; omit async or set async: true."};if(Q.clarify===!0)return{error:"Scheduled subagent runs cannot open clarify UI; omit clarify or set clarify: false."};let{action:X,id:V,runId:W,dir:J,index:B,message:q,chainName:G,config:K,schedule:A,scheduleName:H,...z}=Q;return{params:{...z,async:!0,clarify:!1,context:"fresh"}}}class k{store;ctx;timers=new Map;storeRoot;now;randomId;timersApi;deps;constructor(Q){this.deps=Q,this.storeRoot=Q.storeRoot??P,this.now=Q.now??Date.now,this.randomId=Q.randomId??(()=>i().slice(0,8)),this.timersApi=Q.timers??globalThis}bindSession(Q){if(this.stopTimers(),this.ctx=Q,!D(this.deps.config)){this.store=void 0;return}let Z=R(Q.sessionManager);this.store=new f(e(Q.cwd,Z,this.storeRoot),Q.cwd,Z),this.rearmScheduledJobs()}stop(){this.stopTimers(),this.store=void 0,this.ctx=void 0}async handleToolCall(Q,Z){this.ctx=Z;try{if(!D(this.deps.config))return U('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.',!0);if(!this.store)this.bindSession(Z);if(!this.store)return U("Scheduled subagent store is unavailable for this session.",!0);switch(Q.action){case"schedule":return this.createJob(Q,Z);case"schedule-list":return this.listJobs();case"schedule-status":return this.statusJob(Q);case"schedule-cancel":return this.cancelJob(Q);default:return U(`Unknown scheduled-run action: ${Q.action}`,!0)}}catch($){let Y=$ instanceof Error?$.message:String($);return U(Y,!0)}}createJob(Q,Z){let $=this.requireStore(),Y=XQ(Q);if(Y.error)return U(Y.error,!0);let X=Q.schedule.trim(),V=QQ(X,this.now()),W=$.list().filter((z)=>z.state==="scheduled"||z.state==="running").length,J=ZQ(this.deps.config);if(W>=J)return U(`Scheduled subagent limit reached (${W}/${J} pending or running). Cancel an existing scheduled run before adding another.`,!0);let B=this.randomId(),q=R(Z.sessionManager),G=Q.scheduleName?.trim(),K=Y.params,A=this.now(),H={id:B,name:G||YQ(K),schedule:X,runAt:V,state:"scheduled",createdAt:A,updatedAt:A,cwd:Z.cwd,sessionId:q,params:K};return $.mutate((z)=>{z.jobs.push(H)}),this.arm(H),U([`Scheduled subagent run ${H.id}.`,`Name: ${H.name}`,`When: ${new Date(H.runAt).toISOString()}`,`Mode: ${b(K)}`,"Context: fresh (scheduled runs never fork parent-session context)",`Status: subagent({ action: "schedule-status", id: "${H.id}" })`,`Cancel before it fires: subagent({ action: "schedule-cancel", id: "${H.id}" })`].join(`
2
- `))}listJobs(){let Q=this.requireStore().list().sort(($,Y)=>$.runAt-Y.runAt);if(Q.length===0)return U("No scheduled subagent runs for this session.");let Z=[`Scheduled subagent runs: ${Q.length}`,""];for(let $ of Q){let Y=[$.id,$.state,new Date($.runAt).toISOString(),$.name];if($.lastRunId)Y.push(`run ${$.lastRunId}`);if($.lastError)Y.push(`error: ${$.lastError}`);Z.push(`- ${Y.join(" | ")}`)}return U(Z.join(`
3
- `))}statusJob(Q){let Z=Q.id??Q.runId;if(!Z)return U("action='schedule-status' requires id.",!0);let $=v(this.requireStore().list(),Z),Y=[`Scheduled run: ${$.id}`,`Name: ${$.name}`,`State: ${$.state}`,`Schedule: ${$.schedule}`,`Run at: ${new Date($.runAt).toISOString()}`,`Mode: ${b($.params)}`,`CWD: ${S($.cwd)}`,`Created: ${new Date($.createdAt).toISOString()}`,`Updated: ${new Date($.updatedAt).toISOString()}`,$.lastRunId?`Launched async run: ${$.lastRunId}`:void 0,$.lastAsyncDir?`Async dir: ${$.lastAsyncDir}`:void 0,$.lastError?`Error: ${$.lastError}`:void 0,$.state==="scheduled"?`Cancel: subagent({ action: "schedule-cancel", id: "${$.id}" })`:void 0,$.lastRunId?`Async status: subagent({ action: "status", id: "${$.lastRunId}" })`:void 0].filter((X)=>Boolean(X));return U(Y.join(`
4
- `))}cancelJob(Q){let Z=Q.id??Q.runId;if(!Z)return U("action='schedule-cancel' requires id.",!0);let $=this.requireStore(),Y=v($.list(),Z);if(Y.state==="running")return U(`Scheduled run ${Y.id} already launched async run ${Y.lastRunId??"unknown"}; interrupt that async run instead.`,!0);if($Q(Y.state))return U(`Scheduled run ${Y.id} is already ${Y.state}.`,!0);let X=this.now();return this.clearTimer(Y.id),$.mutate((V)=>{let W=V.jobs.find((J)=>J.id===Y.id);if(!W)return;W.state="canceled",W.canceledAt=X,W.updatedAt=X}),U(`Canceled scheduled subagent run ${Y.id}.`)}rearmScheduledJobs(){let Q=this.requireStore(),Z=this.now(),$=x(this.deps.config),Y=Q.list().filter((X)=>X.state==="scheduled"&&X.runAt+$<Z);if(Y.length>0)Q.mutate((X)=>{for(let V of Y){let W=X.jobs.find((J)=>J.id===V.id);if(!W||W.state!=="scheduled")continue;W.state="missed",W.updatedAt=Z,W.lastError=`Missed scheduled time by more than ${C($)} while DM was not available.`}});for(let X of Q.list())if(X.state==="scheduled")this.arm(X)}arm(Q){this.clearTimer(Q.id);let Z=Math.max(0,Q.runAt-this.now()),$=this.timersApi.setTimeout(()=>{this.fire(Q.id)},Math.min(Z,a));$.unref?.(),this.timers.set(Q.id,$)}async fire(Q){this.clearTimer(Q);let Z=this.store,$=this.ctx;if(!Z||!$)return;let Y=Z.get(Q);if(!Y||Y.state!=="scheduled")return;let X=this.now();if(X<Y.runAt){this.arm(Y);return}let V=x(this.deps.config);if(Y.runAt+V<X){Z.mutate((J)=>{let B=J.jobs.find((q)=>q.id===Q);if(!B||B.state!=="scheduled")return;B.state="missed",B.updatedAt=X,B.lastError=`Missed scheduled time by more than ${C(V)}.`});return}if(Z.mutate((J)=>{let B=J.jobs.find((q)=>q.id===Q);if(!B||B.state!=="scheduled")return;B.state="running",B.firedAt=X,B.updatedAt=X}),Y=Z.get(Q),!Y||Y.state!=="running")return;let W=new AbortController;try{let J=await this.deps.launch(Y.params,$,W.signal),B=J.details?.asyncId??J.details?.runId;Z.mutate((q)=>{let G=q.jobs.find((K)=>K.id===Q);if(!G)return;if(G.updatedAt=this.now(),J.isError||!B){G.state="failed",G.lastError=J.content.find((K)=>K.type==="text")?.text??"Scheduled subagent launch failed.";return}G.state="fired",G.lastRunId=B,G.lastAsyncDir=J.details?.asyncDir})}catch(J){let B=J instanceof Error?J.message:String(J);Z.mutate((q)=>{let G=q.jobs.find((K)=>K.id===Q);if(!G)return;G.state="failed",G.lastError=B,G.updatedAt=this.now()})}}requireStore(){if(!this.store)throw Error("Scheduled subagent store is not bound to a session.");return this.store}clearTimer(Q){let Z=this.timers.get(Q);if(!Z)return;this.timersApi.clearTimeout(Z),this.timers.delete(Q)}stopTimers(){for(let Q of this.timers.values())this.timersApi.clearTimeout(Q);this.timers.clear()}}function cQ(Q){return new k(Q)}export{D as scheduledRunsEnabled,e as scheduledRunStorePath,QQ as parseScheduledRunTime,mQ as isScheduledRunAction,cQ as createScheduledRunManager,k as ScheduledRunManager,r as SCHEDULED_RUN_ACTIONS,P as SCHEDULED_RUNS_DIR};
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
+ }