@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.
- package/CONTRIBUTING.md +2 -3
- package/README.md +25 -34
- package/dist/.templates/workspace/AGENTS.md +2 -2
- package/dist/boot.d.ts +5 -0
- package/dist/boot.d.ts.map +1 -1
- package/dist/boot.js +62 -87
- package/dist/boot.js.map +1 -1
- package/dist/cli/channels.d.ts +6 -23
- package/dist/cli/channels.d.ts.map +1 -1
- package/dist/cli/channels.js +15 -124
- package/dist/cli/channels.js.map +1 -1
- package/dist/cli/chat.d.ts +6 -0
- package/dist/cli/chat.d.ts.map +1 -0
- package/dist/cli/chat.js +49 -0
- package/dist/cli/chat.js.map +1 -0
- package/dist/cli/onboard.d.ts.map +1 -1
- package/dist/cli/onboard.js +2 -8
- package/dist/cli/onboard.js.map +1 -1
- package/dist/cli.d.ts +9 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +207 -290
- package/dist/cli.js.map +1 -1
- package/dist/core/bus.d.ts +32 -0
- package/dist/core/bus.d.ts.map +1 -0
- package/dist/core/bus.js +133 -0
- package/dist/core/bus.js.map +1 -0
- package/dist/core/cli.d.ts +38 -0
- package/dist/core/cli.d.ts.map +1 -0
- package/dist/core/cli.js +199 -0
- package/dist/core/cli.js.map +1 -0
- package/dist/core/errors.d.ts +21 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +30 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/loader.d.ts +31 -0
- package/dist/core/loader.d.ts.map +1 -0
- package/dist/core/loader.js +73 -0
- package/dist/core/loader.js.map +1 -0
- package/dist/core/local.d.ts +12 -0
- package/dist/core/local.d.ts.map +1 -0
- package/dist/core/local.js +23 -0
- package/dist/core/local.js.map +1 -0
- package/dist/core/rpc-server.d.ts +11 -0
- package/dist/core/rpc-server.d.ts.map +1 -0
- package/dist/core/rpc-server.js +69 -0
- package/dist/core/rpc-server.js.map +1 -0
- package/dist/core/services.d.ts +8 -0
- package/dist/core/services.d.ts.map +1 -0
- package/dist/core/services.js +33 -0
- package/dist/core/services.js.map +1 -0
- package/dist/core/types.d.ts +63 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/edge/mcp/index.d.ts +11 -11
- package/dist/edge/mcp/index.d.ts.map +1 -1
- package/dist/edge/mcp/index.js +24 -28
- package/dist/edge/mcp/index.js.map +1 -1
- package/dist/edge/webhooks/index.d.ts +8 -14
- package/dist/edge/webhooks/index.d.ts.map +1 -1
- package/dist/edge/webhooks/index.js +140 -194
- package/dist/edge/webhooks/index.js.map +1 -1
- package/dist/lib/logger.d.ts +2 -3
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +1 -1
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/paginate.d.ts +5 -1
- package/dist/lib/paginate.d.ts.map +1 -1
- package/dist/lib/retry.js +1 -1
- package/dist/lib/retry.js.map +1 -1
- package/dist/lib/util.d.ts +16 -0
- package/dist/lib/util.d.ts.map +1 -0
- package/dist/lib/util.js +63 -0
- package/dist/lib/util.js.map +1 -0
- package/dist/scripts/verify-core.d.ts +8 -0
- package/dist/scripts/verify-core.d.ts.map +1 -0
- package/dist/scripts/verify-core.js +191 -0
- package/dist/scripts/verify-core.js.map +1 -0
- package/dist/services/agent/index.d.ts +14 -18
- package/dist/services/agent/index.d.ts.map +1 -1
- package/dist/services/agent/index.js +469 -530
- package/dist/services/agent/index.js.map +1 -1
- package/dist/services/agent/tools.d.ts +3 -3
- package/dist/services/agent/tools.d.ts.map +1 -1
- package/dist/services/agent/tools.js +11 -19
- package/dist/services/agent/tools.js.map +1 -1
- package/dist/services/agent/types.d.ts +1 -1
- package/dist/services/agent/types.d.ts.map +1 -1
- package/dist/services/{channels → channel}/base-adapter.d.ts +5 -6
- package/dist/services/channel/base-adapter.d.ts.map +1 -0
- package/dist/services/channel/base-adapter.js.map +1 -0
- package/dist/services/channel/debounce.d.ts.map +1 -0
- package/dist/services/channel/debounce.js.map +1 -0
- package/dist/services/channel/dedupe.d.ts.map +1 -0
- package/dist/services/channel/dedupe.js.map +1 -0
- package/dist/services/channel/delivery.d.ts.map +1 -0
- package/dist/services/{channels → channel}/delivery.js +1 -1
- package/dist/services/channel/delivery.js.map +1 -0
- package/dist/services/{channels → channel}/index.d.ts +14 -17
- package/dist/services/channel/index.d.ts.map +1 -0
- package/dist/services/channel/index.js +354 -0
- package/dist/services/channel/index.js.map +1 -0
- package/dist/services/channel/link-expand.d.ts.map +1 -0
- package/dist/services/channel/link-expand.js.map +1 -0
- package/dist/services/channel/media-paths.d.ts.map +1 -0
- package/dist/services/channel/media-paths.js.map +1 -0
- package/dist/services/{channels → channel}/pipeline.d.ts +1 -1
- package/dist/services/channel/pipeline.d.ts.map +1 -0
- package/dist/services/channel/pipeline.js.map +1 -0
- package/dist/services/channel/provider-loader.d.ts.map +1 -0
- package/dist/services/channel/provider-loader.js.map +1 -0
- package/dist/services/channel/providers/telegram/adapter.d.ts.map +1 -0
- package/dist/services/{channels → channel}/providers/telegram/adapter.js +1 -1
- package/dist/services/channel/providers/telegram/adapter.js.map +1 -0
- package/dist/services/channel/providers/telegram/index.d.ts.map +1 -0
- package/dist/services/channel/providers/telegram/index.js.map +1 -0
- package/dist/services/channel/providers/telegram/normalizer.d.ts.map +1 -0
- package/dist/services/channel/providers/telegram/normalizer.js.map +1 -0
- package/dist/services/channel/providers/telegram/types.d.ts.map +1 -0
- package/dist/services/channel/providers/telegram/types.js.map +1 -0
- package/dist/services/channel/providers/whatsapp/adapter.d.ts.map +1 -0
- package/dist/services/channel/providers/whatsapp/adapter.js.map +1 -0
- package/dist/services/channel/providers/whatsapp/index.d.ts.map +1 -0
- package/dist/services/channel/providers/whatsapp/index.js.map +1 -0
- package/dist/services/channel/providers/whatsapp/normalizer.d.ts.map +1 -0
- package/dist/services/channel/providers/whatsapp/normalizer.js.map +1 -0
- package/dist/services/channel/providers/whatsapp/session.d.ts.map +1 -0
- package/dist/services/channel/providers/whatsapp/session.js.map +1 -0
- package/dist/services/channel/providers/whatsapp/types.d.ts.map +1 -0
- package/dist/services/channel/providers/whatsapp/types.js.map +1 -0
- package/dist/services/channel/reconnect.d.ts.map +1 -0
- package/dist/services/channel/reconnect.js.map +1 -0
- package/dist/services/channel/status-reactions.d.ts.map +1 -0
- package/dist/services/channel/status-reactions.js.map +1 -0
- package/dist/services/{channels → channel}/types.d.ts +1 -1
- package/dist/services/channel/types.d.ts.map +1 -0
- package/dist/services/channel/types.js.map +1 -0
- package/dist/services/channel/typing-state.d.ts.map +1 -0
- package/dist/services/channel/typing-state.js.map +1 -0
- package/dist/services/config/index.d.ts +45 -46
- package/dist/services/config/index.d.ts.map +1 -1
- package/dist/services/config/index.js +89 -182
- package/dist/services/config/index.js.map +1 -1
- package/dist/services/config/schemas/cron.d.ts +3 -3
- package/dist/services/config/schemas/providers.d.ts +12 -12
- package/dist/services/config/schemas/webhooks.d.ts +2 -2
- package/dist/services/cron/index.d.ts +16 -19
- package/dist/services/cron/index.d.ts.map +1 -1
- package/dist/services/cron/index.js +340 -396
- package/dist/services/cron/index.js.map +1 -1
- package/dist/services/log/index.d.ts +9 -8
- package/dist/services/log/index.d.ts.map +1 -1
- package/dist/services/log/index.js +71 -123
- package/dist/services/log/index.js.map +1 -1
- package/dist/services/{mcp-client → mcp}/index.d.ts +9 -11
- package/dist/services/mcp/index.d.ts.map +1 -0
- package/dist/services/{mcp-client → mcp}/index.js +19 -35
- package/dist/services/mcp/index.js.map +1 -0
- package/dist/services/media/index.d.ts +9 -13
- package/dist/services/media/index.d.ts.map +1 -1
- package/dist/services/media/index.js +53 -105
- package/dist/services/media/index.js.map +1 -1
- package/dist/services/memory/index.d.ts +12 -18
- package/dist/services/memory/index.d.ts.map +1 -1
- package/dist/services/memory/index.js +70 -132
- package/dist/services/memory/index.js.map +1 -1
- package/dist/services/web/index.d.ts +7 -7
- package/dist/services/web/index.d.ts.map +1 -1
- package/dist/services/web/index.js +41 -86
- package/dist/services/web/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/gateway/bus.d.ts +0 -50
- package/dist/gateway/bus.d.ts.map +0 -1
- package/dist/gateway/bus.js +0 -2
- package/dist/gateway/bus.js.map +0 -1
- package/dist/gateway/decorators.d.ts +0 -40
- package/dist/gateway/decorators.d.ts.map +0 -1
- package/dist/gateway/decorators.js +0 -43
- package/dist/gateway/decorators.js.map +0 -1
- package/dist/gateway/emitter.d.ts +0 -52
- package/dist/gateway/emitter.d.ts.map +0 -1
- package/dist/gateway/emitter.js +0 -304
- package/dist/gateway/emitter.js.map +0 -1
- package/dist/gateway/events.d.ts +0 -329
- package/dist/gateway/events.d.ts.map +0 -1
- package/dist/gateway/events.js +0 -7
- package/dist/gateway/events.js.map +0 -1
- package/dist/gateway/tcp-server.d.ts +0 -7
- package/dist/gateway/tcp-server.d.ts.map +0 -1
- package/dist/gateway/tcp-server.js +0 -118
- package/dist/gateway/tcp-server.js.map +0 -1
- package/dist/lib/id.d.ts +0 -3
- package/dist/lib/id.d.ts.map +0 -1
- package/dist/lib/id.js +0 -5
- package/dist/lib/id.js.map +0 -1
- package/dist/lib/sleep.d.ts +0 -6
- package/dist/lib/sleep.d.ts.map +0 -1
- package/dist/lib/sleep.js +0 -22
- package/dist/lib/sleep.js.map +0 -1
- package/dist/lib/strip-markdown.d.ts +0 -7
- package/dist/lib/strip-markdown.d.ts.map +0 -1
- package/dist/lib/strip-markdown.js +0 -32
- package/dist/lib/strip-markdown.js.map +0 -1
- package/dist/lib/timeout.d.ts +0 -6
- package/dist/lib/timeout.d.ts.map +0 -1
- package/dist/lib/timeout.js +0 -11
- package/dist/lib/timeout.js.map +0 -1
- package/dist/lib/truncate.d.ts +0 -11
- package/dist/lib/truncate.d.ts.map +0 -1
- package/dist/lib/truncate.js +0 -17
- package/dist/lib/truncate.js.map +0 -1
- package/dist/services/channels/base-adapter.d.ts.map +0 -1
- package/dist/services/channels/base-adapter.js.map +0 -1
- package/dist/services/channels/debounce.d.ts.map +0 -1
- package/dist/services/channels/debounce.js.map +0 -1
- package/dist/services/channels/dedupe.d.ts.map +0 -1
- package/dist/services/channels/dedupe.js.map +0 -1
- package/dist/services/channels/delivery.d.ts.map +0 -1
- package/dist/services/channels/delivery.js.map +0 -1
- package/dist/services/channels/index.d.ts.map +0 -1
- package/dist/services/channels/index.js +0 -413
- package/dist/services/channels/index.js.map +0 -1
- package/dist/services/channels/link-expand.d.ts.map +0 -1
- package/dist/services/channels/link-expand.js.map +0 -1
- package/dist/services/channels/media-paths.d.ts.map +0 -1
- package/dist/services/channels/media-paths.js.map +0 -1
- package/dist/services/channels/pipeline.d.ts.map +0 -1
- package/dist/services/channels/pipeline.js.map +0 -1
- package/dist/services/channels/provider-loader.d.ts.map +0 -1
- package/dist/services/channels/provider-loader.js.map +0 -1
- package/dist/services/channels/providers/telegram/adapter.d.ts.map +0 -1
- package/dist/services/channels/providers/telegram/adapter.js.map +0 -1
- package/dist/services/channels/providers/telegram/index.d.ts.map +0 -1
- package/dist/services/channels/providers/telegram/index.js.map +0 -1
- package/dist/services/channels/providers/telegram/normalizer.d.ts.map +0 -1
- package/dist/services/channels/providers/telegram/normalizer.js.map +0 -1
- package/dist/services/channels/providers/telegram/types.d.ts.map +0 -1
- package/dist/services/channels/providers/telegram/types.js.map +0 -1
- package/dist/services/channels/providers/whatsapp/adapter.d.ts.map +0 -1
- package/dist/services/channels/providers/whatsapp/adapter.js.map +0 -1
- package/dist/services/channels/providers/whatsapp/index.d.ts.map +0 -1
- package/dist/services/channels/providers/whatsapp/index.js.map +0 -1
- package/dist/services/channels/providers/whatsapp/normalizer.d.ts.map +0 -1
- package/dist/services/channels/providers/whatsapp/normalizer.js.map +0 -1
- package/dist/services/channels/providers/whatsapp/session.d.ts.map +0 -1
- package/dist/services/channels/providers/whatsapp/session.js.map +0 -1
- package/dist/services/channels/providers/whatsapp/types.d.ts.map +0 -1
- package/dist/services/channels/providers/whatsapp/types.js.map +0 -1
- package/dist/services/channels/reconnect.d.ts.map +0 -1
- package/dist/services/channels/reconnect.js.map +0 -1
- package/dist/services/channels/status-reactions.d.ts.map +0 -1
- package/dist/services/channels/status-reactions.js.map +0 -1
- package/dist/services/channels/types.d.ts.map +0 -1
- package/dist/services/channels/types.js.map +0 -1
- package/dist/services/channels/typing-state.d.ts.map +0 -1
- package/dist/services/channels/typing-state.js.map +0 -1
- package/dist/services/mcp-client/index.d.ts.map +0 -1
- package/dist/services/mcp-client/index.js.map +0 -1
- /package/dist/services/{channels → channel}/base-adapter.js +0 -0
- /package/dist/services/{channels → channel}/debounce.d.ts +0 -0
- /package/dist/services/{channels → channel}/debounce.js +0 -0
- /package/dist/services/{channels → channel}/dedupe.d.ts +0 -0
- /package/dist/services/{channels → channel}/dedupe.js +0 -0
- /package/dist/services/{channels → channel}/delivery.d.ts +0 -0
- /package/dist/services/{channels → channel}/link-expand.d.ts +0 -0
- /package/dist/services/{channels → channel}/link-expand.js +0 -0
- /package/dist/services/{channels → channel}/media-paths.d.ts +0 -0
- /package/dist/services/{channels → channel}/media-paths.js +0 -0
- /package/dist/services/{channels → channel}/pipeline.js +0 -0
- /package/dist/services/{channels → channel}/provider-loader.d.ts +0 -0
- /package/dist/services/{channels → channel}/provider-loader.js +0 -0
- /package/dist/services/{channels → channel}/providers/telegram/adapter.d.ts +0 -0
- /package/dist/services/{channels → channel}/providers/telegram/index.d.ts +0 -0
- /package/dist/services/{channels → channel}/providers/telegram/index.js +0 -0
- /package/dist/services/{channels → channel}/providers/telegram/normalizer.d.ts +0 -0
- /package/dist/services/{channels → channel}/providers/telegram/normalizer.js +0 -0
- /package/dist/services/{channels → channel}/providers/telegram/types.d.ts +0 -0
- /package/dist/services/{channels → channel}/providers/telegram/types.js +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/adapter.d.ts +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/adapter.js +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/index.d.ts +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/index.js +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/normalizer.d.ts +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/normalizer.js +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/session.d.ts +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/session.js +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/types.d.ts +0 -0
- /package/dist/services/{channels → channel}/providers/whatsapp/types.js +0 -0
- /package/dist/services/{channels → channel}/reconnect.d.ts +0 -0
- /package/dist/services/{channels → channel}/reconnect.js +0 -0
- /package/dist/services/{channels → channel}/status-reactions.d.ts +0 -0
- /package/dist/services/{channels → channel}/status-reactions.js +0 -0
- /package/dist/services/{channels → channel}/types.js +0 -0
- /package/dist/services/{channels → channel}/typing-state.d.ts +0 -0
- /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/
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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.
|
|
175
|
-
this.
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
if (
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return;
|
|
220
|
+
this.activeTasks.add(id);
|
|
221
|
+
try {
|
|
222
|
+
await this.executeTask(entry.task);
|
|
249
223
|
}
|
|
250
|
-
|
|
251
|
-
log.
|
|
252
|
-
return;
|
|
224
|
+
catch (err) {
|
|
225
|
+
log.error('task execution error', { id, error: err instanceof Error ? err.message : String(err) });
|
|
253
226
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
332
|
+
return tasks;
|
|
333
|
+
}
|
|
334
|
+
async writeTaskToDisk(task) {
|
|
335
|
+
if (!task?.id) {
|
|
336
|
+
throw new Error('Cannot write task without id');
|
|
323
337
|
}
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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
|
|
400
|
-
|
|
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
|
-
|
|
410
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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.
|
|
395
|
+
const content = await fs.readFile(path.join(workspaceDir, 'HEARTBEAT.md'), 'utf-8');
|
|
396
|
+
if (isHeartbeatContentEffectivelyEmpty(content))
|
|
397
|
+
return false;
|
|
418
398
|
}
|
|
419
|
-
catch
|
|
420
|
-
|
|
421
|
-
throw err;
|
|
422
|
-
}
|
|
423
|
-
// File doesn't exist, that's fine
|
|
399
|
+
catch {
|
|
400
|
+
return false; // missing file
|
|
424
401
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|