@chozzz/vargos 3.1.5 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/CONTRIBUTING.md +2 -3
  2. package/README.md +25 -34
  3. package/dist/.templates/workspace/AGENTS.md +2 -2
  4. package/dist/boot.d.ts +5 -0
  5. package/dist/boot.d.ts.map +1 -1
  6. package/dist/boot.js +62 -87
  7. package/dist/boot.js.map +1 -1
  8. package/dist/cli/channels.d.ts +6 -23
  9. package/dist/cli/channels.d.ts.map +1 -1
  10. package/dist/cli/channels.js +15 -124
  11. package/dist/cli/channels.js.map +1 -1
  12. package/dist/cli/chat.d.ts +6 -0
  13. package/dist/cli/chat.d.ts.map +1 -0
  14. package/dist/cli/chat.js +49 -0
  15. package/dist/cli/chat.js.map +1 -0
  16. package/dist/cli/onboard.d.ts.map +1 -1
  17. package/dist/cli/onboard.js +2 -8
  18. package/dist/cli/onboard.js.map +1 -1
  19. package/dist/cli.d.ts +9 -7
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +207 -290
  22. package/dist/cli.js.map +1 -1
  23. package/dist/core/bus.d.ts +32 -0
  24. package/dist/core/bus.d.ts.map +1 -0
  25. package/dist/core/bus.js +133 -0
  26. package/dist/core/bus.js.map +1 -0
  27. package/dist/core/cli.d.ts +38 -0
  28. package/dist/core/cli.d.ts.map +1 -0
  29. package/dist/core/cli.js +199 -0
  30. package/dist/core/cli.js.map +1 -0
  31. package/dist/core/errors.d.ts +21 -0
  32. package/dist/core/errors.d.ts.map +1 -0
  33. package/dist/core/errors.js +30 -0
  34. package/dist/core/errors.js.map +1 -0
  35. package/dist/core/loader.d.ts +31 -0
  36. package/dist/core/loader.d.ts.map +1 -0
  37. package/dist/core/loader.js +73 -0
  38. package/dist/core/loader.js.map +1 -0
  39. package/dist/core/local.d.ts +12 -0
  40. package/dist/core/local.d.ts.map +1 -0
  41. package/dist/core/local.js +23 -0
  42. package/dist/core/local.js.map +1 -0
  43. package/dist/core/rpc-server.d.ts +11 -0
  44. package/dist/core/rpc-server.d.ts.map +1 -0
  45. package/dist/core/rpc-server.js +69 -0
  46. package/dist/core/rpc-server.js.map +1 -0
  47. package/dist/core/services.d.ts +8 -0
  48. package/dist/core/services.d.ts.map +1 -0
  49. package/dist/core/services.js +33 -0
  50. package/dist/core/services.js.map +1 -0
  51. package/dist/core/types.d.ts +63 -0
  52. package/dist/core/types.d.ts.map +1 -0
  53. package/dist/core/types.js +2 -0
  54. package/dist/core/types.js.map +1 -0
  55. package/dist/edge/mcp/index.d.ts +11 -11
  56. package/dist/edge/mcp/index.d.ts.map +1 -1
  57. package/dist/edge/mcp/index.js +24 -28
  58. package/dist/edge/mcp/index.js.map +1 -1
  59. package/dist/edge/webhooks/index.d.ts +8 -14
  60. package/dist/edge/webhooks/index.d.ts.map +1 -1
  61. package/dist/edge/webhooks/index.js +140 -194
  62. package/dist/edge/webhooks/index.js.map +1 -1
  63. package/dist/lib/logger.d.ts +2 -3
  64. package/dist/lib/logger.d.ts.map +1 -1
  65. package/dist/lib/logger.js +1 -1
  66. package/dist/lib/logger.js.map +1 -1
  67. package/dist/lib/paginate.d.ts +5 -1
  68. package/dist/lib/paginate.d.ts.map +1 -1
  69. package/dist/lib/retry.js +1 -1
  70. package/dist/lib/retry.js.map +1 -1
  71. package/dist/lib/util.d.ts +16 -0
  72. package/dist/lib/util.d.ts.map +1 -0
  73. package/dist/lib/util.js +63 -0
  74. package/dist/lib/util.js.map +1 -0
  75. package/dist/scripts/verify-core.d.ts +8 -0
  76. package/dist/scripts/verify-core.d.ts.map +1 -0
  77. package/dist/scripts/verify-core.js +191 -0
  78. package/dist/scripts/verify-core.js.map +1 -0
  79. package/dist/services/agent/index.d.ts +14 -18
  80. package/dist/services/agent/index.d.ts.map +1 -1
  81. package/dist/services/agent/index.js +469 -530
  82. package/dist/services/agent/index.js.map +1 -1
  83. package/dist/services/agent/tools.d.ts +3 -3
  84. package/dist/services/agent/tools.d.ts.map +1 -1
  85. package/dist/services/agent/tools.js +11 -19
  86. package/dist/services/agent/tools.js.map +1 -1
  87. package/dist/services/agent/types.d.ts +1 -1
  88. package/dist/services/agent/types.d.ts.map +1 -1
  89. package/dist/services/{channels → channel}/base-adapter.d.ts +5 -6
  90. package/dist/services/channel/base-adapter.d.ts.map +1 -0
  91. package/dist/services/channel/base-adapter.js.map +1 -0
  92. package/dist/services/channel/debounce.d.ts.map +1 -0
  93. package/dist/services/channel/debounce.js.map +1 -0
  94. package/dist/services/channel/dedupe.d.ts.map +1 -0
  95. package/dist/services/channel/dedupe.js.map +1 -0
  96. package/dist/services/channel/delivery.d.ts.map +1 -0
  97. package/dist/services/{channels → channel}/delivery.js +1 -1
  98. package/dist/services/channel/delivery.js.map +1 -0
  99. package/dist/services/{channels → channel}/index.d.ts +14 -17
  100. package/dist/services/channel/index.d.ts.map +1 -0
  101. package/dist/services/channel/index.js +354 -0
  102. package/dist/services/channel/index.js.map +1 -0
  103. package/dist/services/channel/link-expand.d.ts.map +1 -0
  104. package/dist/services/channel/link-expand.js.map +1 -0
  105. package/dist/services/channel/media-paths.d.ts.map +1 -0
  106. package/dist/services/channel/media-paths.js.map +1 -0
  107. package/dist/services/{channels → channel}/pipeline.d.ts +1 -1
  108. package/dist/services/channel/pipeline.d.ts.map +1 -0
  109. package/dist/services/channel/pipeline.js.map +1 -0
  110. package/dist/services/channel/provider-loader.d.ts.map +1 -0
  111. package/dist/services/channel/provider-loader.js.map +1 -0
  112. package/dist/services/channel/providers/telegram/adapter.d.ts.map +1 -0
  113. package/dist/services/{channels → channel}/providers/telegram/adapter.js +1 -1
  114. package/dist/services/channel/providers/telegram/adapter.js.map +1 -0
  115. package/dist/services/channel/providers/telegram/index.d.ts.map +1 -0
  116. package/dist/services/channel/providers/telegram/index.js.map +1 -0
  117. package/dist/services/channel/providers/telegram/normalizer.d.ts.map +1 -0
  118. package/dist/services/channel/providers/telegram/normalizer.js.map +1 -0
  119. package/dist/services/channel/providers/telegram/types.d.ts.map +1 -0
  120. package/dist/services/channel/providers/telegram/types.js.map +1 -0
  121. package/dist/services/channel/providers/whatsapp/adapter.d.ts.map +1 -0
  122. package/dist/services/channel/providers/whatsapp/adapter.js.map +1 -0
  123. package/dist/services/channel/providers/whatsapp/index.d.ts.map +1 -0
  124. package/dist/services/channel/providers/whatsapp/index.js.map +1 -0
  125. package/dist/services/channel/providers/whatsapp/normalizer.d.ts.map +1 -0
  126. package/dist/services/channel/providers/whatsapp/normalizer.js.map +1 -0
  127. package/dist/services/channel/providers/whatsapp/session.d.ts.map +1 -0
  128. package/dist/services/channel/providers/whatsapp/session.js.map +1 -0
  129. package/dist/services/channel/providers/whatsapp/types.d.ts.map +1 -0
  130. package/dist/services/channel/providers/whatsapp/types.js.map +1 -0
  131. package/dist/services/channel/reconnect.d.ts.map +1 -0
  132. package/dist/services/channel/reconnect.js.map +1 -0
  133. package/dist/services/channel/status-reactions.d.ts.map +1 -0
  134. package/dist/services/channel/status-reactions.js.map +1 -0
  135. package/dist/services/{channels → channel}/types.d.ts +1 -1
  136. package/dist/services/channel/types.d.ts.map +1 -0
  137. package/dist/services/channel/types.js.map +1 -0
  138. package/dist/services/channel/typing-state.d.ts.map +1 -0
  139. package/dist/services/channel/typing-state.js.map +1 -0
  140. package/dist/services/config/index.d.ts +45 -46
  141. package/dist/services/config/index.d.ts.map +1 -1
  142. package/dist/services/config/index.js +89 -182
  143. package/dist/services/config/index.js.map +1 -1
  144. package/dist/services/config/schemas/cron.d.ts +3 -3
  145. package/dist/services/config/schemas/providers.d.ts +12 -12
  146. package/dist/services/config/schemas/webhooks.d.ts +2 -2
  147. package/dist/services/cron/index.d.ts +16 -19
  148. package/dist/services/cron/index.d.ts.map +1 -1
  149. package/dist/services/cron/index.js +340 -396
  150. package/dist/services/cron/index.js.map +1 -1
  151. package/dist/services/log/index.d.ts +9 -8
  152. package/dist/services/log/index.d.ts.map +1 -1
  153. package/dist/services/log/index.js +71 -123
  154. package/dist/services/log/index.js.map +1 -1
  155. package/dist/services/{mcp-client → mcp}/index.d.ts +9 -11
  156. package/dist/services/mcp/index.d.ts.map +1 -0
  157. package/dist/services/{mcp-client → mcp}/index.js +19 -35
  158. package/dist/services/mcp/index.js.map +1 -0
  159. package/dist/services/media/index.d.ts +9 -13
  160. package/dist/services/media/index.d.ts.map +1 -1
  161. package/dist/services/media/index.js +53 -105
  162. package/dist/services/media/index.js.map +1 -1
  163. package/dist/services/memory/index.d.ts +12 -18
  164. package/dist/services/memory/index.d.ts.map +1 -1
  165. package/dist/services/memory/index.js +70 -132
  166. package/dist/services/memory/index.js.map +1 -1
  167. package/dist/services/web/index.d.ts +7 -7
  168. package/dist/services/web/index.d.ts.map +1 -1
  169. package/dist/services/web/index.js +41 -86
  170. package/dist/services/web/index.js.map +1 -1
  171. package/package.json +3 -2
  172. package/dist/gateway/bus.d.ts +0 -50
  173. package/dist/gateway/bus.d.ts.map +0 -1
  174. package/dist/gateway/bus.js +0 -2
  175. package/dist/gateway/bus.js.map +0 -1
  176. package/dist/gateway/decorators.d.ts +0 -40
  177. package/dist/gateway/decorators.d.ts.map +0 -1
  178. package/dist/gateway/decorators.js +0 -43
  179. package/dist/gateway/decorators.js.map +0 -1
  180. package/dist/gateway/emitter.d.ts +0 -52
  181. package/dist/gateway/emitter.d.ts.map +0 -1
  182. package/dist/gateway/emitter.js +0 -304
  183. package/dist/gateway/emitter.js.map +0 -1
  184. package/dist/gateway/events.d.ts +0 -329
  185. package/dist/gateway/events.d.ts.map +0 -1
  186. package/dist/gateway/events.js +0 -7
  187. package/dist/gateway/events.js.map +0 -1
  188. package/dist/gateway/tcp-server.d.ts +0 -7
  189. package/dist/gateway/tcp-server.d.ts.map +0 -1
  190. package/dist/gateway/tcp-server.js +0 -118
  191. package/dist/gateway/tcp-server.js.map +0 -1
  192. package/dist/lib/id.d.ts +0 -3
  193. package/dist/lib/id.d.ts.map +0 -1
  194. package/dist/lib/id.js +0 -5
  195. package/dist/lib/id.js.map +0 -1
  196. package/dist/lib/sleep.d.ts +0 -6
  197. package/dist/lib/sleep.d.ts.map +0 -1
  198. package/dist/lib/sleep.js +0 -22
  199. package/dist/lib/sleep.js.map +0 -1
  200. package/dist/lib/strip-markdown.d.ts +0 -7
  201. package/dist/lib/strip-markdown.d.ts.map +0 -1
  202. package/dist/lib/strip-markdown.js +0 -32
  203. package/dist/lib/strip-markdown.js.map +0 -1
  204. package/dist/lib/timeout.d.ts +0 -6
  205. package/dist/lib/timeout.d.ts.map +0 -1
  206. package/dist/lib/timeout.js +0 -11
  207. package/dist/lib/timeout.js.map +0 -1
  208. package/dist/lib/truncate.d.ts +0 -11
  209. package/dist/lib/truncate.d.ts.map +0 -1
  210. package/dist/lib/truncate.js +0 -17
  211. package/dist/lib/truncate.js.map +0 -1
  212. package/dist/services/channels/base-adapter.d.ts.map +0 -1
  213. package/dist/services/channels/base-adapter.js.map +0 -1
  214. package/dist/services/channels/debounce.d.ts.map +0 -1
  215. package/dist/services/channels/debounce.js.map +0 -1
  216. package/dist/services/channels/dedupe.d.ts.map +0 -1
  217. package/dist/services/channels/dedupe.js.map +0 -1
  218. package/dist/services/channels/delivery.d.ts.map +0 -1
  219. package/dist/services/channels/delivery.js.map +0 -1
  220. package/dist/services/channels/index.d.ts.map +0 -1
  221. package/dist/services/channels/index.js +0 -413
  222. package/dist/services/channels/index.js.map +0 -1
  223. package/dist/services/channels/link-expand.d.ts.map +0 -1
  224. package/dist/services/channels/link-expand.js.map +0 -1
  225. package/dist/services/channels/media-paths.d.ts.map +0 -1
  226. package/dist/services/channels/media-paths.js.map +0 -1
  227. package/dist/services/channels/pipeline.d.ts.map +0 -1
  228. package/dist/services/channels/pipeline.js.map +0 -1
  229. package/dist/services/channels/provider-loader.d.ts.map +0 -1
  230. package/dist/services/channels/provider-loader.js.map +0 -1
  231. package/dist/services/channels/providers/telegram/adapter.d.ts.map +0 -1
  232. package/dist/services/channels/providers/telegram/adapter.js.map +0 -1
  233. package/dist/services/channels/providers/telegram/index.d.ts.map +0 -1
  234. package/dist/services/channels/providers/telegram/index.js.map +0 -1
  235. package/dist/services/channels/providers/telegram/normalizer.d.ts.map +0 -1
  236. package/dist/services/channels/providers/telegram/normalizer.js.map +0 -1
  237. package/dist/services/channels/providers/telegram/types.d.ts.map +0 -1
  238. package/dist/services/channels/providers/telegram/types.js.map +0 -1
  239. package/dist/services/channels/providers/whatsapp/adapter.d.ts.map +0 -1
  240. package/dist/services/channels/providers/whatsapp/adapter.js.map +0 -1
  241. package/dist/services/channels/providers/whatsapp/index.d.ts.map +0 -1
  242. package/dist/services/channels/providers/whatsapp/index.js.map +0 -1
  243. package/dist/services/channels/providers/whatsapp/normalizer.d.ts.map +0 -1
  244. package/dist/services/channels/providers/whatsapp/normalizer.js.map +0 -1
  245. package/dist/services/channels/providers/whatsapp/session.d.ts.map +0 -1
  246. package/dist/services/channels/providers/whatsapp/session.js.map +0 -1
  247. package/dist/services/channels/providers/whatsapp/types.d.ts.map +0 -1
  248. package/dist/services/channels/providers/whatsapp/types.js.map +0 -1
  249. package/dist/services/channels/reconnect.d.ts.map +0 -1
  250. package/dist/services/channels/reconnect.js.map +0 -1
  251. package/dist/services/channels/status-reactions.d.ts.map +0 -1
  252. package/dist/services/channels/status-reactions.js.map +0 -1
  253. package/dist/services/channels/types.d.ts.map +0 -1
  254. package/dist/services/channels/types.js.map +0 -1
  255. package/dist/services/channels/typing-state.d.ts.map +0 -1
  256. package/dist/services/channels/typing-state.js.map +0 -1
  257. package/dist/services/mcp-client/index.d.ts.map +0 -1
  258. package/dist/services/mcp-client/index.js.map +0 -1
  259. /package/dist/services/{channels → channel}/base-adapter.js +0 -0
  260. /package/dist/services/{channels → channel}/debounce.d.ts +0 -0
  261. /package/dist/services/{channels → channel}/debounce.js +0 -0
  262. /package/dist/services/{channels → channel}/dedupe.d.ts +0 -0
  263. /package/dist/services/{channels → channel}/dedupe.js +0 -0
  264. /package/dist/services/{channels → channel}/delivery.d.ts +0 -0
  265. /package/dist/services/{channels → channel}/link-expand.d.ts +0 -0
  266. /package/dist/services/{channels → channel}/link-expand.js +0 -0
  267. /package/dist/services/{channels → channel}/media-paths.d.ts +0 -0
  268. /package/dist/services/{channels → channel}/media-paths.js +0 -0
  269. /package/dist/services/{channels → channel}/pipeline.js +0 -0
  270. /package/dist/services/{channels → channel}/provider-loader.d.ts +0 -0
  271. /package/dist/services/{channels → channel}/provider-loader.js +0 -0
  272. /package/dist/services/{channels → channel}/providers/telegram/adapter.d.ts +0 -0
  273. /package/dist/services/{channels → channel}/providers/telegram/index.d.ts +0 -0
  274. /package/dist/services/{channels → channel}/providers/telegram/index.js +0 -0
  275. /package/dist/services/{channels → channel}/providers/telegram/normalizer.d.ts +0 -0
  276. /package/dist/services/{channels → channel}/providers/telegram/normalizer.js +0 -0
  277. /package/dist/services/{channels → channel}/providers/telegram/types.d.ts +0 -0
  278. /package/dist/services/{channels → channel}/providers/telegram/types.js +0 -0
  279. /package/dist/services/{channels → channel}/providers/whatsapp/adapter.d.ts +0 -0
  280. /package/dist/services/{channels → channel}/providers/whatsapp/adapter.js +0 -0
  281. /package/dist/services/{channels → channel}/providers/whatsapp/index.d.ts +0 -0
  282. /package/dist/services/{channels → channel}/providers/whatsapp/index.js +0 -0
  283. /package/dist/services/{channels → channel}/providers/whatsapp/normalizer.d.ts +0 -0
  284. /package/dist/services/{channels → channel}/providers/whatsapp/normalizer.js +0 -0
  285. /package/dist/services/{channels → channel}/providers/whatsapp/session.d.ts +0 -0
  286. /package/dist/services/{channels → channel}/providers/whatsapp/session.js +0 -0
  287. /package/dist/services/{channels → channel}/providers/whatsapp/types.d.ts +0 -0
  288. /package/dist/services/{channels → channel}/providers/whatsapp/types.js +0 -0
  289. /package/dist/services/{channels → channel}/reconnect.d.ts +0 -0
  290. /package/dist/services/{channels → channel}/reconnect.js +0 -0
  291. /package/dist/services/{channels → channel}/status-reactions.d.ts +0 -0
  292. /package/dist/services/{channels → channel}/status-reactions.js +0 -0
  293. /package/dist/services/{channels → channel}/types.js +0 -0
  294. /package/dist/services/{channels → channel}/typing-state.d.ts +0 -0
  295. /package/dist/services/{channels → channel}/typing-state.js +0 -0
@@ -11,456 +11,400 @@
11
11
  * Subagent deferral: if subagents are still running, the agent runtime may
12
12
  * defer delivery until the parent run completes.
13
13
  */
14
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
15
- var useValue = arguments.length > 2;
16
- for (var i = 0; i < initializers.length; i++) {
17
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
18
- }
19
- return useValue ? value : void 0;
20
- };
21
- var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
22
- function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
23
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
24
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
25
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
26
- var _, done = false;
27
- for (var i = decorators.length - 1; i >= 0; i--) {
28
- var context = {};
29
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
30
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
31
- context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
32
- var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
33
- if (kind === "accessor") {
34
- if (result === void 0) continue;
35
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
36
- if (_ = accept(result.get)) descriptor.get = _;
37
- if (_ = accept(result.set)) descriptor.set = _;
38
- if (_ = accept(result.init)) initializers.unshift(_);
39
- }
40
- else if (_ = accept(result)) {
41
- if (kind === "field") initializers.unshift(_);
42
- else descriptor[key] = _;
43
- }
44
- }
45
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
46
- done = true;
47
- };
48
14
  import { CronJob } from 'cron';
49
15
  import { promises as fs } from 'node:fs';
50
16
  import path from 'node:path';
51
17
  import { z } from 'zod';
52
- import { register } from '../../gateway/decorators.js';
53
18
  import { CronTaskSchema } from '../../services/config/schemas/cron.js';
54
19
  import { createLogger } from '../../lib/logger.js';
55
20
  import { toMessage } from '../../lib/error.js';
21
+ import { formatZodIssues } from '../../core/errors.js';
56
22
  import { getDataPaths } from '../../lib/paths.js';
57
- import { generateId } from '../../lib/id.js';
23
+ import { generateId } from '../../lib/util.js';
58
24
  import { paginate } from '../../lib/paginate.js';
59
25
  import { cronSessionKey, parseSessionKey } from '../../lib/session-key.js';
60
26
  import { parseFrontmatter, serializeFrontmatter } from '../../lib/frontmatter.js';
61
27
  import { isWithinActiveHours, isHeartbeatContentEffectivelyEmpty, stripHeartbeatToken, } from './heartbeat.js';
62
28
  const log = createLogger('cron');
63
29
  // ── CronService ───────────────────────────────────────────────────────────────
64
- let CronService = (() => {
65
- let _instanceExtraInitializers = [];
66
- let _search_decorators;
67
- let _add_decorators;
68
- let _remove_decorators;
69
- let _update_decorators;
70
- let _run_decorators;
71
- return class CronService {
72
- static {
73
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
74
- _search_decorators = [register('cron.search', {
75
- description: 'Search scheduled cron tasks.',
76
- schema: z.object({ query: z.string().optional(), page: z.number().default(1), limit: z.number().default(20) }),
77
- })];
78
- _add_decorators = [register('cron.add', {
79
- description: 'Add a new scheduled cron task.',
80
- schema: z.object({
81
- name: z.string(),
82
- schedule: z.string(),
83
- task: z.string(),
84
- notify: z.array(z.string()).optional(),
85
- }),
86
- })];
87
- _remove_decorators = [register('cron.remove', {
88
- description: 'Remove a scheduled cron task.',
89
- schema: z.object({ id: z.string() }),
90
- })];
91
- _update_decorators = [register('cron.update', {
92
- description: 'Update a scheduled cron task.',
93
- schema: z.object({
94
- id: z.string(),
95
- name: z.string().optional(),
96
- schedule: z.string().optional(),
97
- task: z.string().optional(),
98
- enabled: z.boolean().optional(),
99
- notify: z.array(z.string()).optional(),
100
- }),
101
- })];
102
- _run_decorators = [register('cron.run', {
103
- description: 'Manually trigger a cron task immediately.',
104
- schema: z.object({ id: z.string() }),
105
- })];
106
- __esDecorate(this, null, _search_decorators, { kind: "method", name: "search", static: false, private: false, access: { has: obj => "search" in obj, get: obj => obj.search }, metadata: _metadata }, null, _instanceExtraInitializers);
107
- __esDecorate(this, null, _add_decorators, { kind: "method", name: "add", static: false, private: false, access: { has: obj => "add" in obj, get: obj => obj.add }, metadata: _metadata }, null, _instanceExtraInitializers);
108
- __esDecorate(this, null, _remove_decorators, { kind: "method", name: "remove", static: false, private: false, access: { has: obj => "remove" in obj, get: obj => obj.remove }, metadata: _metadata }, null, _instanceExtraInitializers);
109
- __esDecorate(this, null, _update_decorators, { kind: "method", name: "update", static: false, private: false, access: { has: obj => "update" in obj, get: obj => obj.update }, metadata: _metadata }, null, _instanceExtraInitializers);
110
- __esDecorate(this, null, _run_decorators, { kind: "method", name: "run", static: false, private: false, access: { has: obj => "run" in obj, get: obj => obj.run }, metadata: _metadata }, null, _instanceExtraInitializers);
111
- if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
112
- }
113
- bus = __runInitializers(this, _instanceExtraInitializers);
114
- config;
115
- jobs = new Map();
116
- ephemeralIds = new Set();
117
- activeTasks = new Set();
118
- beforeFireHooks = new Map();
119
- unsubscribeCompleted;
120
- cronDir;
121
- constructor(bus, config, cronDir) {
122
- this.bus = bus;
123
- this.config = config;
124
- this.cronDir = cronDir ?? getDataPaths().cronDir;
125
- }
126
- async start() {
127
- // Load tasks from disk
128
- const diskTasks = await this.loadTasksFromDisk();
129
- for (const task of diskTasks) {
130
- this.addJob(task);
131
- }
132
- // Register heartbeat if task exists (loaded from disk)
133
- if (this.jobs.has('heartbeat')) {
134
- this.registerHeartbeat();
135
- }
136
- this.startAll();
137
- this.unsubscribeCompleted = this.bus.on('agent.onCompleted', (payload) => this.onAgentCompleted(payload));
138
- }
139
- stop() {
140
- this.stopAll();
141
- this.unsubscribeCompleted?.();
142
- }
143
- // ── Callable handlers ─────────────────────────────────────────────────────
144
- async search(params) {
145
- const { query } = params;
146
- const all = Array.from(this.jobs.values())
147
- .filter(e => !this.ephemeralIds.has(e.task.id))
148
- .map(e => e.task);
149
- const filtered = query
150
- ? all.filter(t => t.name.includes(query) || t.id.includes(query) || t.task.includes(query))
151
- : all;
152
- // page/limit are omitted by untyped JSON-RPC callers — paginate() defaults them to 1/20.
153
- return paginate(filtered, params.page, params.limit);
154
- }
155
- async add(params) {
156
- const id = generateId('cron');
157
- if (this.jobs.has(id)) {
158
- throw new Error(`Cron task already exists: ${id}`);
159
- }
160
- const task = { ...params, id, enabled: true };
161
- // Write to disk
162
- await this.writeTaskToDisk(task);
163
- // Register in-memory
30
+ export class CronService {
31
+ name = 'cron';
32
+ jobs = new Map();
33
+ ephemeralIds = new Set();
34
+ activeTasks = new Set();
35
+ beforeFireHooks = new Map();
36
+ bus;
37
+ cronDir;
38
+ constructor(cronDir) {
39
+ this.cronDir = cronDir ?? getDataPaths().cronDir;
40
+ }
41
+ async init(bus) {
42
+ this.bus = bus;
43
+ this.registerMethods(bus);
44
+ const diskTasks = await this.loadTasksFromDisk();
45
+ for (const task of diskTasks)
164
46
  this.addJob(task);
165
- this.jobs.get(task.id).job.start();
166
- log.info(`task added: ${task.name} (${task.id})`);
47
+ if (this.jobs.has('heartbeat'))
48
+ this.registerHeartbeat();
49
+ this.startAll();
50
+ bus.on('agent.onCompleted', (p) => this.onAgentCompleted(p));
51
+ log.info('cron service started');
52
+ }
53
+ /** Stop every CronJob timer — without this, a reload would leave timers firing (D2). */
54
+ dispose() {
55
+ this.stopAll();
56
+ }
57
+ registerMethods(bus) {
58
+ bus.register('cron.search', {
59
+ description: 'Search scheduled cron tasks.',
60
+ schema: z.object({ query: z.string().optional(), page: z.number().default(1), limit: z.number().default(20) }),
61
+ cli: { positional: ['query'] },
62
+ }, (p) => this.search(p));
63
+ bus.register('cron.add', {
64
+ description: 'Add a new scheduled cron task.',
65
+ schema: z.object({
66
+ name: z.string(),
67
+ schedule: z.string(),
68
+ task: z.string(),
69
+ notify: z.array(z.string()).optional(),
70
+ }),
71
+ cli: { positional: ['name', 'schedule', 'task'] },
72
+ }, (p) => this.add(p));
73
+ bus.register('cron.remove', {
74
+ description: 'Remove a scheduled cron task.',
75
+ schema: z.object({ id: z.string() }),
76
+ cli: { positional: ['id'] },
77
+ }, (p) => this.remove(p));
78
+ bus.register('cron.update', {
79
+ description: 'Update a scheduled cron task.',
80
+ schema: z.object({
81
+ id: z.string(),
82
+ name: z.string().optional(),
83
+ schedule: z.string().optional(),
84
+ task: z.string().optional(),
85
+ enabled: z.boolean().optional(),
86
+ notify: z.array(z.string()).optional(),
87
+ }),
88
+ cli: { positional: ['id'] },
89
+ }, (p) => this.update(p));
90
+ bus.register('cron.run', {
91
+ description: 'Manually trigger a cron task immediately.',
92
+ schema: z.object({ id: z.string() }),
93
+ cli: { positional: ['id'] },
94
+ }, (p) => this.run(p));
95
+ }
96
+ // ── Callable handlers ─────────────────────────────────────────────────────
97
+ async search(params) {
98
+ const { query } = params;
99
+ const all = Array.from(this.jobs.values())
100
+ .filter(e => !this.ephemeralIds.has(e.task.id))
101
+ .map(e => e.task);
102
+ const filtered = query
103
+ ? all.filter(t => t.name.includes(query) || t.id.includes(query) || t.task.includes(query))
104
+ : all;
105
+ // page/limit are omitted by untyped JSON-RPC callers — paginate() defaults them to 1/20.
106
+ return paginate(filtered, params.page, params.limit);
107
+ }
108
+ async add(params) {
109
+ const id = generateId('cron');
110
+ if (this.jobs.has(id)) {
111
+ throw new Error(`Cron task already exists: ${id}`);
167
112
  }
168
- async remove(params) {
169
- const entry = this.jobs.get(params.id);
170
- if (!entry)
171
- return;
172
- const isEphemeral = this.ephemeralIds.has(params.id);
113
+ const task = { ...params, id, enabled: true };
114
+ // Write to disk
115
+ await this.writeTaskToDisk(task);
116
+ // Register in-memory
117
+ this.addJob(task);
118
+ this.jobs.get(task.id).job.start();
119
+ log.info(`task added: ${task.name} (${task.id})`);
120
+ }
121
+ async remove(params) {
122
+ const entry = this.jobs.get(params.id);
123
+ if (!entry)
124
+ return;
125
+ const isEphemeral = this.ephemeralIds.has(params.id);
126
+ entry.job.stop();
127
+ this.jobs.delete(params.id);
128
+ this.ephemeralIds.delete(params.id);
129
+ this.activeTasks.delete(params.id);
130
+ // Delete from disk (only persistent tasks)
131
+ if (!isEphemeral) {
132
+ await this.deleteTaskFromDisk(params.id);
133
+ }
134
+ log.info(`task removed: ${params.id}`);
135
+ }
136
+ async update(params) {
137
+ const entry = this.jobs.get(params.id);
138
+ if (!entry)
139
+ throw new Error(`No task with id: ${params.id}`);
140
+ const updates = Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined));
141
+ const updated = { ...entry.task, ...updates };
142
+ if (params.schedule && params.schedule !== entry.task.schedule) {
173
143
  entry.job.stop();
174
- this.jobs.delete(params.id);
175
- this.ephemeralIds.delete(params.id);
176
- this.activeTasks.delete(params.id);
177
- // Delete from disk (only persistent tasks)
178
- if (!isEphemeral) {
179
- await this.deleteTaskFromDisk(params.id);
180
- }
181
- log.info(`task removed: ${params.id}`);
144
+ const job = new CronJob(updated.schedule, () => this.fire(params.id), null, updated.enabled, 'UTC');
145
+ this.jobs.set(params.id, { task: updated, job });
182
146
  }
183
- async update(params) {
184
- const entry = this.jobs.get(params.id);
185
- if (!entry)
186
- throw new Error(`No task with id: ${params.id}`);
187
- const updates = Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined));
188
- const updated = { ...entry.task, ...updates };
189
- if (params.schedule && params.schedule !== entry.task.schedule) {
147
+ else {
148
+ entry.task = updated;
149
+ if (params.enabled === false)
190
150
  entry.job.stop();
191
- const job = new CronJob(updated.schedule, () => this.fire(params.id), null, updated.enabled, 'UTC');
192
- this.jobs.set(params.id, { task: updated, job });
151
+ else if (params.enabled === true)
152
+ entry.job.start();
153
+ }
154
+ // Write to disk (only persistent tasks)
155
+ const isEphemeral = this.ephemeralIds.has(params.id);
156
+ if (!isEphemeral) {
157
+ await this.writeTaskToDisk(updated);
158
+ }
159
+ log.info(`task updated: ${params.id}`);
160
+ }
161
+ async run(params) {
162
+ const entry = this.jobs.get(params.id);
163
+ if (!entry)
164
+ throw new Error(`No task with id: ${params.id}`);
165
+ // Fire without awaiting — long-running tasks must not block the RPC socket
166
+ this.executeTask(entry.task).catch(err => log.error(`manual run failed: ${params.id}: ${toMessage(err)}`));
167
+ }
168
+ // ── Internal scheduling ───────────────────────────────────────────────────
169
+ addJob(task, opts) {
170
+ const job = new CronJob(task.schedule, () => this.fire(task.id), null, false);
171
+ this.jobs.set(task.id, { task, job });
172
+ if (opts?.ephemeral)
173
+ this.ephemeralIds.add(task.id);
174
+ }
175
+ startAll() {
176
+ let count = 0;
177
+ for (const { task, job } of this.jobs.values()) {
178
+ if (task.enabled) {
179
+ log.debug(`starting job: ${task.id} (${task.schedule})`);
180
+ job.start();
181
+ count++;
193
182
  }
194
183
  else {
195
- entry.task = updated;
196
- if (params.enabled === false)
197
- entry.job.stop();
198
- else if (params.enabled === true)
199
- entry.job.start();
184
+ log.debug(`skipping disabled job: ${task.id}`);
200
185
  }
201
- // Write to disk (only persistent tasks)
202
- const isEphemeral = this.ephemeralIds.has(params.id);
203
- if (!isEphemeral) {
204
- await this.writeTaskToDisk(updated);
205
- }
206
- log.info(`task updated: ${params.id}`);
207
186
  }
208
- async run(params) {
209
- const entry = this.jobs.get(params.id);
210
- if (!entry)
211
- throw new Error(`No task with id: ${params.id}`);
212
- // Fire without awaiting — long-running tasks must not block the RPC socket
213
- this.executeTask(entry.task).catch(err => log.error(`manual run failed: ${params.id}: ${toMessage(err)}`));
187
+ log.info(`${count} jobs started`);
188
+ }
189
+ stopAll() {
190
+ for (const { job } of this.jobs.values())
191
+ job.stop();
192
+ }
193
+ fire(id) {
194
+ if (this.activeTasks.has(id)) {
195
+ log.info(`skipping fire — task still active: ${id}`);
196
+ return;
214
197
  }
215
- // ── Internal scheduling ───────────────────────────────────────────────────
216
- addJob(task, opts) {
217
- const job = new CronJob(task.schedule, () => this.fire(task.id), null, false);
218
- this.jobs.set(task.id, { task, job });
219
- if (opts?.ephemeral)
220
- this.ephemeralIds.add(task.id);
198
+ const entry = this.jobs.get(id);
199
+ if (!entry) {
200
+ log.warn(`fire() called for unknown task: ${id}`);
201
+ return;
221
202
  }
222
- startAll() {
223
- let count = 0;
224
- for (const { task, job } of this.jobs.values()) {
225
- if (task.enabled) {
226
- log.debug(`starting job: ${task.id} (${task.schedule})`);
227
- job.start();
228
- count++;
229
- }
230
- else {
231
- log.debug(`skipping disabled job: ${task.id}`);
232
- }
233
- }
234
- log.info(`${count} jobs started`);
203
+ if (!entry.task.enabled) {
204
+ log.debug(`task disabled, not firing: ${id}`);
205
+ return;
235
206
  }
236
- stopAll() {
237
- for (const { job } of this.jobs.values())
238
- job.stop();
207
+ // Check activeHours for all tasks (not just heartbeat)
208
+ if (entry.task.activeHours &&
209
+ !isWithinActiveHours(entry.task.activeHours, entry.task.activeHoursTimezone)) {
210
+ log.debug(`task outside active hours, not firing: ${id}`);
211
+ return;
239
212
  }
240
- fire(id) {
241
- if (this.activeTasks.has(id)) {
242
- log.info(`skipping fire task still active: ${id}`);
213
+ const hook = this.beforeFireHooks.get(id);
214
+ const check = hook ? hook() : Promise.resolve(true);
215
+ check.then(async (shouldFire) => {
216
+ if (!shouldFire) {
217
+ log.debug(`hook check returned false for ${id}, not firing`);
243
218
  return;
244
219
  }
245
- const entry = this.jobs.get(id);
246
- if (!entry) {
247
- log.warn(`fire() called for unknown task: ${id}`);
248
- return;
220
+ this.activeTasks.add(id);
221
+ try {
222
+ await this.executeTask(entry.task);
249
223
  }
250
- if (!entry.task.enabled) {
251
- log.debug(`task disabled, not firing: ${id}`);
252
- return;
224
+ catch (err) {
225
+ log.error('task execution error', { id, error: err instanceof Error ? err.message : String(err) });
253
226
  }
254
- // Check activeHours for all tasks (not just heartbeat)
255
- if (entry.task.activeHours &&
256
- !isWithinActiveHours(entry.task.activeHours, entry.task.activeHoursTimezone)) {
257
- log.debug(`task outside active hours, not firing: ${id}`);
258
- return;
227
+ }).catch(err => log.error(`hook check error: ${id}: ${err}`));
228
+ }
229
+ onAgentCompleted(payload) {
230
+ const parsed = parseSessionKey(payload.sessionKey);
231
+ if (parsed.type !== 'cron')
232
+ return;
233
+ // Strip date suffix to recover taskId (e.g. "daily-backup:2026-03-29" → "daily-backup")
234
+ const taskId = parsed.id.replace(/:\d{4}-\d{2}-\d{2}$/, '');
235
+ if (this.activeTasks.delete(taskId)) {
236
+ log.debug(`concurrency lock released: ${taskId}`);
237
+ }
238
+ }
239
+ // ── Task execution ────────────────────────────────────────────────────────
240
+ async executeTask(task) {
241
+ const sessionKey = cronSessionKey(task.id);
242
+ log.info(`⏰ ${task.name} (${task.id})`);
243
+ const result = await this.bus.call('agent.execute', {
244
+ sessionKey,
245
+ task: task.task,
246
+ ...(task.model && { model: task.model }),
247
+ });
248
+ if (!result.response)
249
+ return;
250
+ const cleaned = stripHeartbeatToken(result.response);
251
+ if (cleaned === null) {
252
+ log.debug(`heartbeat no-op: ${task.id}`);
253
+ return;
254
+ }
255
+ if (!task.notify?.length)
256
+ return;
257
+ // Heartbeat: plain send (omit fromSessionKey so channel.send skips history injection).
258
+ // Other tasks: pass our cron sessionKey so the target session records the cross-session push.
259
+ const isHeartbeat = task.id === 'heartbeat';
260
+ await Promise.all(task.notify.map(target => this.bus.call('channel.send', {
261
+ sessionKey: target,
262
+ text: cleaned,
263
+ ...(isHeartbeat ? {} : { fromSessionKey: sessionKey }),
264
+ }).catch(err => log.error(`notify send to ${target}: ${toMessage(err)}`))));
265
+ }
266
+ // ── File I/O ──────────────────────────────────────────────────────────────
267
+ parseMarkdownTask(content) {
268
+ const result = parseFrontmatter(content);
269
+ if (!result)
270
+ return null;
271
+ return { frontmatter: result.meta, body: result.body };
272
+ }
273
+ serializeMarkdownTask(task) {
274
+ const { task: taskPrompt, ...metadata } = task;
275
+ return serializeFrontmatter(metadata, taskPrompt);
276
+ }
277
+ async loadTasksFromDisk() {
278
+ const tasks = [];
279
+ try {
280
+ const files = await fs.readdir(this.cronDir);
281
+ const mdFiles = files.filter(f => f.endsWith('.md'));
282
+ if (mdFiles.length === 0) {
283
+ log.debug(`no tasks found in ${this.cronDir}`);
284
+ return tasks;
259
285
  }
260
- const hook = this.beforeFireHooks.get(id);
261
- const check = hook ? hook() : Promise.resolve(true);
262
- check.then(async (shouldFire) => {
263
- if (!shouldFire) {
264
- log.debug(`hook check returned false for ${id}, not firing`);
265
- return;
266
- }
267
- this.activeTasks.add(id);
286
+ for (const filename of mdFiles) {
268
287
  try {
269
- await this.executeTask(entry.task);
288
+ const filepath = path.join(this.cronDir, filename);
289
+ const content = await fs.readFile(filepath, 'utf-8');
290
+ const parsed = this.parseMarkdownTask(content);
291
+ if (!parsed) {
292
+ log.warn(`${filename}: missing or invalid YAML frontmatter (expected --- ... ---)}`);
293
+ continue;
294
+ }
295
+ // Build task object
296
+ const task = {
297
+ id: String(parsed.frontmatter.id ?? ''),
298
+ name: String(parsed.frontmatter.title || parsed.frontmatter.name || parsed.frontmatter.id || ''),
299
+ schedule: String(parsed.frontmatter.schedule ?? ''),
300
+ task: parsed.body || '',
301
+ enabled: parsed.frontmatter.enabled === true,
302
+ notify: Array.isArray(parsed.frontmatter.notify) ? parsed.frontmatter.notify.map(String) : undefined,
303
+ activeHours: Array.isArray(parsed.frontmatter.activeHours) ? parsed.frontmatter.activeHours.slice(0, 2) : undefined,
304
+ activeHoursTimezone: parsed.frontmatter.activeHoursTimezone ? String(parsed.frontmatter.activeHoursTimezone) : undefined,
305
+ };
306
+ // Validate against schema
307
+ const validation = CronTaskSchema.safeParse(task);
308
+ if (!validation.success) {
309
+ log.error(`${filename}: schema validation failed — ${formatZodIssues(validation.error)}`);
310
+ continue;
311
+ }
312
+ tasks.push(validation.data);
313
+ log.debug(`loaded task: ${task.id}`);
314
+ // Mark heartbeat as ephemeral
315
+ if (task.id === 'heartbeat') {
316
+ this.ephemeralIds.add(task.id);
317
+ }
270
318
  }
271
319
  catch (err) {
272
- log.error('task execution error', { id, error: err instanceof Error ? err.message : String(err) });
320
+ log.warn(`${filename}: ${toMessage(err)}`);
273
321
  }
274
- }).catch(err => log.error(`hook check error: ${id}: ${err}`));
275
- }
276
- onAgentCompleted(payload) {
277
- const parsed = parseSessionKey(payload.sessionKey);
278
- if (parsed.type !== 'cron')
279
- return;
280
- // Strip date suffix to recover taskId (e.g. "daily-backup:2026-03-29" → "daily-backup")
281
- const taskId = parsed.id.replace(/:\d{4}-\d{2}-\d{2}$/, '');
282
- if (this.activeTasks.delete(taskId)) {
283
- log.debug(`concurrency lock released: ${taskId}`);
284
322
  }
285
323
  }
286
- // ── Task execution ────────────────────────────────────────────────────────
287
- async executeTask(task) {
288
- const sessionKey = cronSessionKey(task.id);
289
- log.info(`⏰ ${task.name} (${task.id})`);
290
- const result = await this.bus.call('agent.execute', {
291
- sessionKey,
292
- task: task.task,
293
- ...(task.model && { model: task.model }),
294
- });
295
- if (!result.response)
296
- return;
297
- const cleaned = stripHeartbeatToken(result.response);
298
- if (cleaned === null) {
299
- log.debug(`heartbeat no-op: ${task.id}`);
300
- return;
324
+ catch (err) {
325
+ if (err.code === 'ENOENT') {
326
+ log.debug(`cron directory does not exist yet: ${this.cronDir}`);
327
+ }
328
+ else {
329
+ log.warn(`failed to read cron directory: ${toMessage(err)}`);
301
330
  }
302
- if (!task.notify?.length)
303
- return;
304
- // Heartbeat: plain send (omit fromSessionKey so channel.send skips history injection).
305
- // Other tasks: pass our cron sessionKey so the target session records the cross-session push.
306
- const isHeartbeat = task.id === 'heartbeat';
307
- await Promise.all(task.notify.map(target => this.bus.call('channel.send', {
308
- sessionKey: target,
309
- text: cleaned,
310
- ...(isHeartbeat ? {} : { fromSessionKey: sessionKey }),
311
- }).catch(err => log.error(`notify send to ${target}: ${toMessage(err)}`))));
312
- }
313
- // ── File I/O ──────────────────────────────────────────────────────────────
314
- parseMarkdownTask(content) {
315
- const result = parseFrontmatter(content);
316
- if (!result)
317
- return null;
318
- return { frontmatter: result.meta, body: result.body };
319
331
  }
320
- serializeMarkdownTask(task) {
321
- const { task: taskPrompt, ...metadata } = task;
322
- return serializeFrontmatter(metadata, taskPrompt);
332
+ return tasks;
333
+ }
334
+ async writeTaskToDisk(task) {
335
+ if (!task?.id) {
336
+ throw new Error('Cannot write task without id');
323
337
  }
324
- async loadTasksFromDisk() {
325
- const tasks = [];
338
+ try {
339
+ await fs.mkdir(this.cronDir, { recursive: true });
340
+ const filepath = path.join(this.cronDir, `${task.id}.md`);
341
+ const tmpPath = `${filepath}.tmp`;
326
342
  try {
327
- const files = await fs.readdir(this.cronDir);
328
- const mdFiles = files.filter(f => f.endsWith('.md'));
329
- if (mdFiles.length === 0) {
330
- log.debug(`no tasks found in ${this.cronDir}`);
331
- return tasks;
332
- }
333
- for (const filename of mdFiles) {
334
- try {
335
- const filepath = path.join(this.cronDir, filename);
336
- const content = await fs.readFile(filepath, 'utf-8');
337
- const parsed = this.parseMarkdownTask(content);
338
- if (!parsed) {
339
- log.warn(`${filename}: missing or invalid YAML frontmatter (expected --- ... ---)}`);
340
- continue;
341
- }
342
- // Build task object
343
- const task = {
344
- id: String(parsed.frontmatter.id ?? ''),
345
- name: String(parsed.frontmatter.title || parsed.frontmatter.name || parsed.frontmatter.id || ''),
346
- schedule: String(parsed.frontmatter.schedule ?? ''),
347
- task: parsed.body || '',
348
- enabled: parsed.frontmatter.enabled === true,
349
- notify: Array.isArray(parsed.frontmatter.notify) ? parsed.frontmatter.notify.map(String) : undefined,
350
- activeHours: Array.isArray(parsed.frontmatter.activeHours) ? parsed.frontmatter.activeHours.slice(0, 2) : undefined,
351
- activeHoursTimezone: parsed.frontmatter.activeHoursTimezone ? String(parsed.frontmatter.activeHoursTimezone) : undefined,
352
- };
353
- // Validate against schema
354
- const validation = CronTaskSchema.safeParse(task);
355
- if (!validation.success) {
356
- const errors = validation.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join('; ');
357
- log.error(`${filename}: schema validation failed — ${errors}`);
358
- continue;
359
- }
360
- tasks.push(validation.data);
361
- log.debug(`loaded task: ${task.id}`);
362
- // Mark heartbeat as ephemeral
363
- if (task.id === 'heartbeat') {
364
- this.ephemeralIds.add(task.id);
365
- }
366
- }
367
- catch (err) {
368
- log.warn(`${filename}: ${toMessage(err)}`);
369
- }
343
+ const content = this.serializeMarkdownTask(task);
344
+ if (!content) {
345
+ throw new Error('Failed to serialize task');
370
346
  }
347
+ await fs.writeFile(tmpPath, content, 'utf-8');
348
+ await fs.rename(tmpPath, filepath);
349
+ log.debug(`wrote task to disk: ${task.id}`);
371
350
  }
372
351
  catch (err) {
373
- if (err.code === 'ENOENT') {
374
- log.debug(`cron directory does not exist yet: ${this.cronDir}`);
375
- }
376
- else {
377
- log.warn(`failed to read cron directory: ${toMessage(err)}`);
378
- }
379
- }
380
- return tasks;
381
- }
382
- async writeTaskToDisk(task) {
383
- if (!task?.id) {
384
- throw new Error('Cannot write task without id');
385
- }
386
- try {
387
- await fs.mkdir(this.cronDir, { recursive: true });
388
- const filepath = path.join(this.cronDir, `${task.id}.md`);
389
- const tmpPath = `${filepath}.tmp`;
390
352
  try {
391
- const content = this.serializeMarkdownTask(task);
392
- if (!content) {
393
- throw new Error('Failed to serialize task');
394
- }
395
- await fs.writeFile(tmpPath, content, 'utf-8');
396
- await fs.rename(tmpPath, filepath);
397
- log.debug(`wrote task to disk: ${task.id}`);
353
+ await fs.unlink(tmpPath);
398
354
  }
399
- catch (err) {
400
- try {
401
- await fs.unlink(tmpPath);
402
- }
403
- catch {
404
- // Ignore cleanup errors
405
- }
406
- throw err;
355
+ catch {
356
+ // Ignore cleanup errors
407
357
  }
358
+ throw err;
408
359
  }
409
- catch (err) {
410
- log.error(`failed to write task ${task.id}: ${toMessage(err)}`);
360
+ }
361
+ catch (err) {
362
+ log.error(`failed to write task ${task.id}: ${toMessage(err)}`);
363
+ throw err;
364
+ }
365
+ }
366
+ async deleteTaskFromDisk(taskId) {
367
+ const filepath = path.join(this.cronDir, `${taskId}.md`);
368
+ try {
369
+ await fs.unlink(filepath);
370
+ }
371
+ catch (err) {
372
+ if (!(err instanceof Error && 'code' in err && err.code === 'ENOENT')) {
411
373
  throw err;
412
374
  }
375
+ // File doesn't exist, that's fine
413
376
  }
414
- async deleteTaskFromDisk(taskId) {
415
- const filepath = path.join(this.cronDir, `${taskId}.md`);
377
+ }
378
+ // ── Heartbeat ─────────────────────────────────────────────────────────────
379
+ registerHeartbeat() {
380
+ const entry = this.jobs.get('heartbeat');
381
+ if (!entry) {
382
+ log.warn('heartbeat task not found in cron tasks');
383
+ return;
384
+ }
385
+ const { workspaceDir } = getDataPaths();
386
+ const activeHours = entry.task.activeHours;
387
+ const activeHoursTimezone = entry.task.activeHoursTimezone;
388
+ this.beforeFireHooks.set('heartbeat', async () => {
389
+ if (!isWithinActiveHours(activeHours, activeHoursTimezone))
390
+ return false;
391
+ const { activeRuns } = await this.bus.call('agent.status', {});
392
+ if (activeRuns.length > 0)
393
+ return false;
416
394
  try {
417
- await fs.unlink(filepath);
395
+ const content = await fs.readFile(path.join(workspaceDir, 'HEARTBEAT.md'), 'utf-8');
396
+ if (isHeartbeatContentEffectivelyEmpty(content))
397
+ return false;
418
398
  }
419
- catch (err) {
420
- if (!(err instanceof Error && 'code' in err && err.code === 'ENOENT')) {
421
- throw err;
422
- }
423
- // File doesn't exist, that's fine
399
+ catch {
400
+ return false; // missing file
424
401
  }
425
- }
426
- // ── Heartbeat ─────────────────────────────────────────────────────────────
427
- registerHeartbeat() {
428
- const entry = this.jobs.get('heartbeat');
429
- if (!entry) {
430
- log.warn('heartbeat task not found in cron tasks');
431
- return;
432
- }
433
- const { workspaceDir } = getDataPaths();
434
- const activeHours = entry.task.activeHours;
435
- const activeHoursTimezone = entry.task.activeHoursTimezone;
436
- this.beforeFireHooks.set('heartbeat', async () => {
437
- if (!isWithinActiveHours(activeHours, activeHoursTimezone))
438
- return false;
439
- const { activeRuns } = await this.bus.call('agent.status', {});
440
- if (activeRuns.length > 0)
441
- return false;
442
- try {
443
- const content = await fs.readFile(path.join(workspaceDir, 'HEARTBEAT.md'), 'utf-8');
444
- if (isHeartbeatContentEffectivelyEmpty(content))
445
- return false;
446
- }
447
- catch {
448
- return false; // missing file
449
- }
450
- return true;
451
- });
452
- log.info('heartbeat registered');
453
- }
454
- };
455
- })();
456
- export { CronService };
457
- // ── Boot ───────────────────────────────────────────────────────────────────────
458
- export async function boot(bus) {
459
- const config = await bus.call('config.get', {});
460
- const svc = new CronService(bus, config);
461
- await svc.start();
462
- bus.bootstrap(svc);
463
- log.info('cron service started');
464
- return { stop: () => svc.stop() };
402
+ return true;
403
+ });
404
+ log.info('heartbeat registered');
405
+ }
406
+ }
407
+ export function createService() {
408
+ return new CronService();
465
409
  }
466
410
  //# sourceMappingURL=index.js.map