negotium 0.1.8 → 0.1.10
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/README.md +24 -0
- package/dist/canonical-mcp-bridge.js +14 -0
- package/dist/canonical-mcp-bridge.js.map +9 -0
- package/dist/{chunk-n17yxpa8.js → chunk-1jdws5ns.js} +3 -3
- package/dist/chunk-1msje2zf.js +23 -0
- package/dist/chunk-1msje2zf.js.map +10 -0
- package/dist/{chunk-0ay6cjdy.js → chunk-1vth6t3y.js} +10 -10
- package/dist/{chunk-0ay6cjdy.js.map → chunk-1vth6t3y.js.map} +1 -1
- package/dist/{chunk-gxma4c70.js → chunk-2p2hds67.js} +22 -17
- package/dist/{chunk-gxma4c70.js.map → chunk-2p2hds67.js.map} +2 -2
- package/dist/{chunk-j2behpy7.js → chunk-39czd64s.js} +9 -9
- package/dist/{chunk-5ahkew9c.js → chunk-4f43r1nd.js} +8 -108
- package/dist/{chunk-5ahkew9c.js.map → chunk-4f43r1nd.js.map} +6 -9
- package/dist/{chunk-hkdqhm34.js → chunk-4yvkqxmm.js} +1 -1
- package/dist/{chunk-ghs2j1fr.js → chunk-5rmbpqy9.js} +18 -15
- package/dist/{chunk-ghs2j1fr.js.map → chunk-5rmbpqy9.js.map} +2 -2
- package/dist/chunk-9krt5z40.js +113 -0
- package/dist/chunk-9krt5z40.js.map +12 -0
- package/dist/{chunk-hpa0cpzh.js → chunk-9webgemt.js} +24 -19
- package/dist/{chunk-hpa0cpzh.js.map → chunk-9webgemt.js.map} +2 -2
- package/dist/{chunk-9p0ww3tk.js → chunk-a3ypq5pp.js} +5 -3
- package/dist/{chunk-9p0ww3tk.js.map → chunk-a3ypq5pp.js.map} +2 -2
- package/dist/{chunk-e4407g8x.js → chunk-abme0zjm.js} +23 -18
- package/dist/{chunk-e4407g8x.js.map → chunk-abme0zjm.js.map} +2 -2
- package/dist/{chunk-t9zcg8f1.js → chunk-cp37k2wg.js} +24 -12
- package/dist/chunk-cp37k2wg.js.map +13 -0
- package/dist/{chunk-fka9eqe1.js → chunk-cphmr4g4.js} +24 -18
- package/dist/chunk-cphmr4g4.js.map +11 -0
- package/dist/{chunk-w13jjf4a.js → chunk-dj7cpy9k.js} +21 -16
- package/dist/{chunk-w13jjf4a.js.map → chunk-dj7cpy9k.js.map} +2 -2
- package/dist/{chunk-7xqj7aex.js → chunk-f5xgprg1.js} +2 -20
- package/dist/{chunk-7xqj7aex.js.map → chunk-f5xgprg1.js.map} +3 -4
- package/dist/{chunk-xfq68rch.js → chunk-f97qybwk.js} +3 -2
- package/dist/{chunk-xfq68rch.js.map → chunk-f97qybwk.js.map} +2 -2
- package/dist/{chunk-g82jygez.js → chunk-fcskj4xy.js} +6 -6
- package/dist/chunk-gdb8ywgf.js +981 -0
- package/dist/chunk-gdb8ywgf.js.map +16 -0
- package/dist/{chunk-q5bcpf23.js → chunk-grn9fxdn.js} +63 -55
- package/dist/{chunk-q5bcpf23.js.map → chunk-grn9fxdn.js.map} +3 -3
- package/dist/{chunk-6kctn20e.js → chunk-gsk494xn.js} +29 -24
- package/dist/{chunk-6kctn20e.js.map → chunk-gsk494xn.js.map} +2 -2
- package/dist/{chunk-gmy11efb.js → chunk-hsq7j9jk.js} +3 -2
- package/dist/{chunk-gmy11efb.js.map → chunk-hsq7j9jk.js.map} +2 -2
- package/dist/chunk-j0kjscb7.js +53 -0
- package/dist/chunk-j0kjscb7.js.map +10 -0
- package/dist/chunk-jkencnkw.js +2385 -0
- package/dist/chunk-jkencnkw.js.map +26 -0
- package/dist/chunk-k9z47mab.js.map +1 -1
- package/dist/{chunk-exd0v74a.js → chunk-ka8rv918.js} +155 -101
- package/dist/{chunk-exd0v74a.js.map → chunk-ka8rv918.js.map} +4 -4
- package/dist/{chunk-a92a7nrk.js → chunk-mamgm6gb.js} +1 -1
- package/dist/chunk-mhty3nna.js +150 -0
- package/dist/chunk-mhty3nna.js.map +11 -0
- package/dist/chunk-pkdmypdd.js +147 -0
- package/dist/chunk-pkdmypdd.js.map +11 -0
- package/dist/{chunk-p3cr2ejg.js → chunk-q00djwra.js} +39 -31
- package/dist/{chunk-p3cr2ejg.js.map → chunk-q00djwra.js.map} +3 -3
- package/dist/{chunk-ns3ke88n.js → chunk-rj7w44t0.js} +21 -16
- package/dist/{chunk-ns3ke88n.js.map → chunk-rj7w44t0.js.map} +2 -2
- package/dist/{chunk-cds1qxsf.js → chunk-rx58pnss.js} +162 -97
- package/dist/chunk-rx58pnss.js.map +18 -0
- package/dist/{chunk-sm6fh5c5.js → chunk-sxf7krrx.js} +20 -15
- package/dist/{chunk-sm6fh5c5.js.map → chunk-sxf7krrx.js.map} +2 -2
- package/dist/{chunk-naa09ags.js → chunk-tdxmdy6s.js} +28 -22
- package/dist/{chunk-naa09ags.js.map → chunk-tdxmdy6s.js.map} +3 -3
- package/dist/{chunk-fgrme9tr.js → chunk-waf61p58.js} +373 -2643
- package/dist/chunk-waf61p58.js.map +29 -0
- package/dist/{chunk-rafc55bs.js → chunk-wfhghtdz.js} +21 -16
- package/dist/{chunk-rafc55bs.js.map → chunk-wfhghtdz.js.map} +2 -2
- package/dist/{chunk-jzkw2f01.js → chunk-wgrk1d2n.js} +23 -18
- package/dist/{chunk-jzkw2f01.js.map → chunk-wgrk1d2n.js.map} +2 -2
- package/dist/{chunk-jkqq5ef6.js → chunk-y3de7fp3.js} +22 -17
- package/dist/{chunk-jkqq5ef6.js.map → chunk-y3de7fp3.js.map} +2 -2
- package/dist/{chunk-rbrxjvbj.js → chunk-yx2dvsye.js} +93 -36
- package/dist/chunk-yx2dvsye.js.map +24 -0
- package/dist/{chunk-jc0at0tc.js → chunk-z2xy5n2q.js} +3 -2
- package/dist/{chunk-jc0at0tc.js.map → chunk-z2xy5n2q.js.map} +2 -2
- package/dist/{chunk-e5pbggsh.js → chunk-z6654rf6.js} +21 -16
- package/dist/{chunk-e5pbggsh.js.map → chunk-z6654rf6.js.map} +2 -2
- package/dist/cron.js +12976 -0
- package/dist/cron.js.map +134 -0
- package/dist/hosted-agent.js +63 -0
- package/dist/hosted-agent.js.map +10 -0
- package/dist/main.js +12 -12
- package/dist/main.js.map +1 -1
- package/dist/mcp-servers.js +47 -0
- package/dist/mcp-servers.js.map +10 -0
- package/dist/prompts.js +451 -0
- package/dist/prompts.js.map +13 -0
- package/dist/registry.js +258 -0
- package/dist/registry.js.map +17 -0
- package/dist/rollout.js +43 -0
- package/dist/rollout.js.map +10 -0
- package/dist/runtime/cron/background-sessions.ts +91 -0
- package/dist/runtime/cron/context.ts +123 -0
- package/dist/runtime/cron/host.ts +67 -0
- package/dist/runtime/cron/index.ts +92 -0
- package/dist/runtime/cron/mcp-server.ts +517 -0
- package/dist/runtime/cron/module.ts +169 -0
- package/dist/runtime/cron/schedule.ts +211 -0
- package/dist/runtime/cron/scheduler.ts +444 -0
- package/dist/runtime/cron/scripts.ts +181 -0
- package/dist/runtime/cron/store.ts +1043 -0
- package/dist/runtime/cron/summarize.ts +115 -0
- package/dist/runtime/src/agents/execution-host.ts +20 -0
- package/dist/runtime/src/agents/hosted-agent.ts +23 -8
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +75 -1
- package/dist/runtime/src/agents/rollout/index.ts +20 -0
- package/dist/runtime/src/agents/rollout/shared.ts +36 -3
- package/dist/runtime/src/application/submit-user-message.ts +3 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +13 -0
- package/dist/runtime/src/prompts/builders.ts +21 -10
- package/dist/runtime/src/query/active-rooms.ts +33 -11
- package/dist/runtime/src/runtime/inbox.ts +146 -49
- package/dist/runtime/src/runtime/public-helpers.ts +11 -0
- package/dist/runtime/src/runtime/task-format.ts +18 -0
- package/dist/runtime/src/runtime/tasks.ts +2 -16
- package/dist/runtime/src/runtime/turn-runner.ts +78 -16
- package/dist/runtime/src/storage/runtime-turn-requests.ts +1 -0
- package/dist/runtime/src/storage/vault-crypto.ts +11 -5
- package/dist/runtime/src/storage/vault-public.ts +27 -0
- package/dist/runtime/src/storage/vault.ts +95 -42
- package/dist/runtime/src/types/api.ts +2 -0
- package/dist/runtime/src/types.ts +2 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +465 -0
- package/dist/runtime-helpers.js.map +18 -0
- package/dist/types/apps/negotium/src/mcp-servers.d.ts +7 -0
- package/dist/types/packages/core/src/agents/claude-provider.d.ts +23 -0
- package/dist/types/packages/core/src/agents/claude-registry.d.ts +2 -0
- package/dist/types/packages/core/src/agents/codex-native-multi-agent.d.ts +8 -0
- package/dist/types/packages/core/src/agents/codex-provider.d.ts +45 -0
- package/dist/types/packages/core/src/agents/codex-registry.d.ts +2 -0
- package/dist/types/packages/core/src/agents/codex-tree-kill.d.ts +78 -0
- package/dist/types/packages/core/src/agents/contracts.d.ts +63 -0
- package/dist/types/packages/core/src/agents/deep-map.d.ts +2 -0
- package/dist/types/packages/core/src/agents/execution-host.d.ts +31 -0
- package/dist/types/packages/core/src/agents/hosted-agent.d.ts +11 -0
- package/dist/types/packages/core/src/agents/maestro-provider.d.ts +67 -0
- package/dist/types/packages/core/src/agents/maestro-registry.d.ts +17 -0
- package/dist/types/packages/core/src/agents/registry.d.ts +4 -0
- package/dist/types/packages/core/src/agents/rollout/claude.d.ts +72 -0
- package/dist/types/packages/core/src/agents/rollout/codex.d.ts +70 -0
- package/dist/types/packages/core/src/agents/rollout/index.d.ts +6 -0
- package/dist/types/packages/core/src/agents/rollout/shared.d.ts +50 -0
- package/dist/types/packages/core/src/agents/vault-tool-policy.d.ts +4 -0
- package/dist/types/packages/core/src/mcp/canonical-bridge-config.d.ts +21 -0
- package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +36 -0
- package/dist/types/packages/core/src/mcp/session-comm/bridge-ipc-config.d.ts +6 -0
- package/dist/types/packages/core/src/media/file-events.d.ts +14 -0
- package/dist/types/packages/core/src/platform/config.d.ts +95 -0
- package/dist/types/packages/core/src/platform/error.d.ts +6 -0
- package/dist/types/packages/core/src/platform/file-utils.d.ts +6 -0
- package/dist/types/packages/core/src/platform/jsonl.d.ts +33 -0
- package/dist/types/packages/core/src/platform/logger.d.ts +14 -0
- package/dist/types/packages/core/src/platform/maestro-bootstrap-env.d.ts +0 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +227 -0
- package/dist/types/packages/core/src/prompts/builders.d.ts +32 -0
- package/dist/types/packages/core/src/runtime/attachments.d.ts +36 -0
- package/dist/types/packages/core/src/runtime/channel-context.d.ts +13 -0
- package/dist/types/packages/core/src/runtime/file-hooks.d.ts +38 -0
- package/dist/types/packages/core/src/runtime/public-helpers.d.ts +3 -0
- package/dist/types/packages/core/src/runtime/task-format.d.ts +4 -0
- package/dist/types/packages/core/src/runtime/visual-html.d.ts +13 -0
- package/dist/types/packages/core/src/security/sanitize.d.ts +16 -0
- package/dist/types/packages/core/src/security/sensitive-path.d.ts +1 -0
- package/dist/types/packages/core/src/storage/api-messages.d.ts +111 -0
- package/dist/types/packages/core/src/storage/conversations.d.ts +123 -0
- package/dist/types/packages/core/src/storage/forum-db.d.ts +1 -0
- package/dist/types/packages/core/src/storage/sqlite.d.ts +4 -0
- package/dist/types/packages/core/src/storage/vault-crypto.d.ts +11 -0
- package/dist/types/packages/core/src/storage/vault-public.d.ts +3 -0
- package/dist/types/packages/core/src/storage/vault.d.ts +39 -0
- package/dist/types/packages/core/src/types/api.d.ts +472 -0
- package/dist/types/packages/core/src/types.d.ts +249 -0
- package/dist/types/packages/core/src/version.d.ts +1 -0
- package/dist/types/packages/module-cron/src/background-sessions.d.ts +2 -0
- package/dist/types/packages/module-cron/src/context.d.ts +15 -0
- package/dist/types/packages/module-cron/src/host.d.ts +23 -0
- package/dist/types/packages/module-cron/src/index.d.ts +14 -0
- package/dist/types/packages/module-cron/src/module.d.ts +13 -0
- package/dist/types/packages/module-cron/src/schedule.d.ts +23 -0
- package/dist/types/packages/module-cron/src/scheduler.d.ts +46 -0
- package/dist/types/packages/module-cron/src/scripts.d.ts +23 -0
- package/dist/types/packages/module-cron/src/store.d.ts +127 -0
- package/dist/types/packages/module-cron/src/summarize.d.ts +4 -0
- package/dist/vault.js +533 -0
- package/dist/vault.js.map +15 -0
- package/package.json +52 -2
- package/dist/chunk-6cpn7ht2.js +0 -287
- package/dist/chunk-6cpn7ht2.js.map +0 -13
- package/dist/chunk-cds1qxsf.js.map +0 -17
- package/dist/chunk-fgrme9tr.js.map +0 -47
- package/dist/chunk-fka9eqe1.js.map +0 -11
- package/dist/chunk-rbrxjvbj.js.map +0 -23
- package/dist/chunk-t9zcg8f1.js.map +0 -13
- /package/dist/{chunk-n17yxpa8.js.map → chunk-1jdws5ns.js.map} +0 -0
- /package/dist/{chunk-j2behpy7.js.map → chunk-39czd64s.js.map} +0 -0
- /package/dist/{chunk-hkdqhm34.js.map → chunk-4yvkqxmm.js.map} +0 -0
- /package/dist/{chunk-g82jygez.js.map → chunk-fcskj4xy.js.map} +0 -0
- /package/dist/{chunk-a92a7nrk.js.map → chunk-mamgm6gb.js.map} +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import {
|
|
4
|
+
cancelDeferredInject,
|
|
5
|
+
logger,
|
|
6
|
+
type NegotiumNodeModule,
|
|
7
|
+
triggerTopicAiTurn,
|
|
8
|
+
} from "@negotium/core";
|
|
9
|
+
import { listCronBackgroundSessions } from "#background-sessions";
|
|
10
|
+
import { resetCronTopicContext } from "#context";
|
|
11
|
+
import { type CronHost, configureCronHost, cronHost } from "#host";
|
|
12
|
+
import { CronScheduler, type CronSchedulerOptions } from "#scheduler";
|
|
13
|
+
import { runCronPromptScript } from "#scripts";
|
|
14
|
+
import {
|
|
15
|
+
type CronDatabase,
|
|
16
|
+
configureCronDatabase,
|
|
17
|
+
ensureCronSchema,
|
|
18
|
+
listOrphanedCronTopicSessions,
|
|
19
|
+
} from "#store";
|
|
20
|
+
|
|
21
|
+
const SOURCE_MCP_SERVER_FILE = fileURLToPath(new URL("./mcp-server.ts", import.meta.url));
|
|
22
|
+
const PACKAGED_MCP_SERVER_FILE = fileURLToPath(
|
|
23
|
+
new URL("./runtime/cron/mcp-server.ts", import.meta.url),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export interface CronModuleOptions {
|
|
27
|
+
pollIntervalMs?: number;
|
|
28
|
+
runTimeoutMs?: number;
|
|
29
|
+
queueTimeoutMs?: number;
|
|
30
|
+
scriptTimeoutMs?: number;
|
|
31
|
+
database?: CronDatabase;
|
|
32
|
+
host?: Partial<CronHost>;
|
|
33
|
+
mcpServerFile?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function envMs(name: string, fallback: number): number {
|
|
37
|
+
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
38
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createCronModule(options: CronModuleOptions = {}): NegotiumNodeModule {
|
|
42
|
+
return {
|
|
43
|
+
name: "cron",
|
|
44
|
+
singleton: true,
|
|
45
|
+
capabilities: ["scheduler.cron.v1", "scheduler.cron.v2"],
|
|
46
|
+
start(context) {
|
|
47
|
+
const restoreDatabase = options.database ? configureCronDatabase(options.database) : () => {};
|
|
48
|
+
const restoreHost = configureCronHost(options.host ?? {});
|
|
49
|
+
const host = cronHost();
|
|
50
|
+
ensureCronSchema();
|
|
51
|
+
const cleanupTasks = new Set<Promise<unknown>>();
|
|
52
|
+
const trackCleanup = (promise: Promise<unknown>) => {
|
|
53
|
+
cleanupTasks.add(promise);
|
|
54
|
+
void promise.then(
|
|
55
|
+
() => cleanupTasks.delete(promise),
|
|
56
|
+
(error) => {
|
|
57
|
+
cleanupTasks.delete(promise);
|
|
58
|
+
logger.warn({ err: error }, "cron: topic context cleanup failed");
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
for (const topicId of new Set(
|
|
63
|
+
listOrphanedCronTopicSessions().map((entry) => entry.topicId),
|
|
64
|
+
)) {
|
|
65
|
+
trackCleanup(resetCronTopicContext(topicId));
|
|
66
|
+
}
|
|
67
|
+
const unsubscribeTopicCleanup = context.bus.subscribe((event) => {
|
|
68
|
+
if (event.type === "topic-deleted") trackCleanup(resetCronTopicContext(event.topicId));
|
|
69
|
+
});
|
|
70
|
+
const mcpServerFile =
|
|
71
|
+
options.mcpServerFile ??
|
|
72
|
+
(existsSync(SOURCE_MCP_SERVER_FILE) ? SOURCE_MCP_SERVER_FILE : PACKAGED_MCP_SERVER_FILE);
|
|
73
|
+
const unregisterMcp = host.registerRuntimeMcpServer("cron-manager", {
|
|
74
|
+
scopes: ["forum", "manager"],
|
|
75
|
+
forumRequired: true,
|
|
76
|
+
build({ userId, session, topicId, agent }) {
|
|
77
|
+
const args = [`--user-id=${userId}`, `--topic=${session}`];
|
|
78
|
+
if (topicId) args.push(`--topic-id=${topicId}`);
|
|
79
|
+
if (host.authorize?.(userId, "cron:admin", { type: "cron" })) {
|
|
80
|
+
args.push("--authorized=true");
|
|
81
|
+
}
|
|
82
|
+
return host.buildStdioMcpServer(agent, mcpServerFile, args);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const unregisterBackgroundSessions = host.registerBackgroundSessionProvider(
|
|
86
|
+
listCronBackgroundSessions,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const schedulerOptions: CronSchedulerOptions = {
|
|
90
|
+
bus: context.bus,
|
|
91
|
+
pollIntervalMs: options.pollIntervalMs ?? envMs("NEGOTIUM_CRON_POLL_INTERVAL_MS", 1_000),
|
|
92
|
+
runTimeoutMs: options.runTimeoutMs ?? envMs("NEGOTIUM_CRON_RUN_TIMEOUT_MS", 10 * 60_000),
|
|
93
|
+
queueTimeoutMs:
|
|
94
|
+
options.queueTimeoutMs ?? envMs("NEGOTIUM_CRON_QUEUE_TIMEOUT_MS", 5 * 60_000),
|
|
95
|
+
dispatch:
|
|
96
|
+
options.host?.dispatch ??
|
|
97
|
+
(async (job, run, hooks, execution) => {
|
|
98
|
+
const taskPrompt =
|
|
99
|
+
job.prompt ??
|
|
100
|
+
(await runCronPromptScript({
|
|
101
|
+
script: job.script!,
|
|
102
|
+
cwd: host.resolveTopicWorkspaceDir(job.topicId),
|
|
103
|
+
jobId: job.id,
|
|
104
|
+
topicId: job.topicId,
|
|
105
|
+
timeoutMs:
|
|
106
|
+
options.scriptTimeoutMs ?? envMs("NEGOTIUM_CRON_SCRIPT_TIMEOUT_MS", 10 * 60_000),
|
|
107
|
+
signal: execution.signal,
|
|
108
|
+
}));
|
|
109
|
+
if (!taskPrompt.trim()) {
|
|
110
|
+
return { status: "skipped", reason: "prompt script produced no output" } as const;
|
|
111
|
+
}
|
|
112
|
+
if (!host.getTopic(job.topicId)?.agent)
|
|
113
|
+
throw new Error("target topic was deleted during run");
|
|
114
|
+
const prompt = [
|
|
115
|
+
`<scheduled-task name="${job.name}" run-id="${run.id}">`,
|
|
116
|
+
"This is a background scheduled run. Do not ask interactive questions.",
|
|
117
|
+
"All scheduled tasks in this topic share one Cron conversation.",
|
|
118
|
+
"Use relevant conclusions and state from previous scheduled runs before acting.",
|
|
119
|
+
"Complete the task and report only meaningful results in the user's language.",
|
|
120
|
+
taskPrompt,
|
|
121
|
+
"</scheduled-task>",
|
|
122
|
+
].join("\n");
|
|
123
|
+
const requestId = `cron:${run.id}`;
|
|
124
|
+
const queryId = triggerTopicAiTurn(
|
|
125
|
+
job.topicId,
|
|
126
|
+
job.ownerUserId,
|
|
127
|
+
prompt,
|
|
128
|
+
execution.agent,
|
|
129
|
+
{
|
|
130
|
+
origin: `cron:${job.id}:${run.id}`,
|
|
131
|
+
requestId,
|
|
132
|
+
hideInjectMessage: true,
|
|
133
|
+
modelOverride: job.model,
|
|
134
|
+
effortOverride: job.effort,
|
|
135
|
+
sessionId: execution.sessionId ?? null,
|
|
136
|
+
sessionName: execution.sessionName,
|
|
137
|
+
sessionType: "cron",
|
|
138
|
+
onDispatched: hooks.onDispatched,
|
|
139
|
+
onSessionId: hooks.onSessionId,
|
|
140
|
+
onSessionReset: hooks.onSessionReset,
|
|
141
|
+
bridgeSessionFromHistory: true,
|
|
142
|
+
onSettled: hooks.onSettled,
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
return {
|
|
146
|
+
status: queryId ? ("dispatched" as const) : ("deferred" as const),
|
|
147
|
+
requestId,
|
|
148
|
+
...(queryId ? { queryId } : {}),
|
|
149
|
+
cancel: () => cancelDeferredInject(job.topicId, requestId),
|
|
150
|
+
};
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
const scheduler = new CronScheduler(schedulerOptions);
|
|
154
|
+
scheduler.start();
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
async stop() {
|
|
158
|
+
scheduler.stop();
|
|
159
|
+
unsubscribeTopicCleanup();
|
|
160
|
+
unregisterBackgroundSessions();
|
|
161
|
+
unregisterMcp();
|
|
162
|
+
await Promise.allSettled(cleanupTasks);
|
|
163
|
+
restoreHost();
|
|
164
|
+
restoreDatabase();
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const FIELD_RANGES = [
|
|
2
|
+
[0, 59],
|
|
3
|
+
[0, 23],
|
|
4
|
+
[1, 31],
|
|
5
|
+
[1, 12],
|
|
6
|
+
[0, 7],
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
interface ParsedField {
|
|
10
|
+
values: Set<number>;
|
|
11
|
+
wildcard: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ParsedCronExpression {
|
|
15
|
+
minute: ParsedField;
|
|
16
|
+
hour: ParsedField;
|
|
17
|
+
dayOfMonth: ParsedField;
|
|
18
|
+
month: ParsedField;
|
|
19
|
+
dayOfWeek: ParsedField;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseNumber(raw: string, min: number, max: number): number {
|
|
23
|
+
if (!/^\d+$/.test(raw)) throw new Error(`invalid cron value: ${raw}`);
|
|
24
|
+
const value = Number.parseInt(raw, 10);
|
|
25
|
+
if (value < min || value > max) {
|
|
26
|
+
throw new Error(`cron value ${value} is outside ${min}-${max}`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseField(raw: string, min: number, max: number, normalize?: (n: number) => number) {
|
|
32
|
+
const values = new Set<number>();
|
|
33
|
+
const wildcard = raw === "*";
|
|
34
|
+
for (const item of raw.split(",")) {
|
|
35
|
+
if (!item) throw new Error("empty cron list item");
|
|
36
|
+
const [base, stepRaw, extra] = item.split("/");
|
|
37
|
+
if (extra !== undefined) throw new Error(`invalid cron step: ${item}`);
|
|
38
|
+
const step = stepRaw === undefined ? 1 : parseNumber(stepRaw, 1, max - min + 1);
|
|
39
|
+
let start: number;
|
|
40
|
+
let end: number;
|
|
41
|
+
if (base === "*") {
|
|
42
|
+
start = min;
|
|
43
|
+
end = max;
|
|
44
|
+
} else if (base.includes("-")) {
|
|
45
|
+
const [startRaw, endRaw, rangeExtra] = base.split("-");
|
|
46
|
+
if (!startRaw || !endRaw || rangeExtra !== undefined) {
|
|
47
|
+
throw new Error(`invalid cron range: ${base}`);
|
|
48
|
+
}
|
|
49
|
+
start = parseNumber(startRaw, min, max);
|
|
50
|
+
end = parseNumber(endRaw, min, max);
|
|
51
|
+
if (end < start) throw new Error(`descending cron range: ${base}`);
|
|
52
|
+
} else {
|
|
53
|
+
start = parseNumber(base, min, max);
|
|
54
|
+
end = stepRaw === undefined ? start : max;
|
|
55
|
+
}
|
|
56
|
+
for (let value = start; value <= end; value += step) {
|
|
57
|
+
values.add(normalize ? normalize(value) : value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { values, wildcard } satisfies ParsedField;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseCronExpression(expression: string): ParsedCronExpression {
|
|
64
|
+
const fields = expression.trim().split(/\s+/);
|
|
65
|
+
if (fields.length !== 5) throw new Error("cron expression must have exactly 5 fields");
|
|
66
|
+
const parsed = fields.map((field, index) => {
|
|
67
|
+
const [min, max] = FIELD_RANGES[index]!;
|
|
68
|
+
return parseField(
|
|
69
|
+
field!,
|
|
70
|
+
min,
|
|
71
|
+
max,
|
|
72
|
+
index === 4 ? (value) => (value === 7 ? 0 : value) : undefined,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
minute: parsed[0]!,
|
|
77
|
+
hour: parsed[1]!,
|
|
78
|
+
dayOfMonth: parsed[2]!,
|
|
79
|
+
month: parsed[3]!,
|
|
80
|
+
dayOfWeek: parsed[4]!,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function validateCronExpression(
|
|
85
|
+
expression: string,
|
|
86
|
+
): { ok: true } | { ok: false; error: string } {
|
|
87
|
+
try {
|
|
88
|
+
parseCronExpression(expression);
|
|
89
|
+
return { ok: true };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const formatters = new Map<string, Intl.DateTimeFormat>();
|
|
96
|
+
|
|
97
|
+
export function normalizeCronTimezone(timezone?: string | null): string | undefined {
|
|
98
|
+
const value = timezone?.trim();
|
|
99
|
+
if (!value) return undefined;
|
|
100
|
+
try {
|
|
101
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
|
|
102
|
+
return value;
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function dateParts(date: Date, timezone?: string) {
|
|
109
|
+
if (!timezone) {
|
|
110
|
+
return {
|
|
111
|
+
minute: date.getMinutes(),
|
|
112
|
+
hour: date.getHours(),
|
|
113
|
+
dayOfMonth: date.getDate(),
|
|
114
|
+
month: date.getMonth() + 1,
|
|
115
|
+
dayOfWeek: date.getDay(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
let formatter = formatters.get(timezone);
|
|
119
|
+
if (!formatter) {
|
|
120
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
121
|
+
timeZone: timezone,
|
|
122
|
+
year: "numeric",
|
|
123
|
+
month: "2-digit",
|
|
124
|
+
day: "2-digit",
|
|
125
|
+
hour: "2-digit",
|
|
126
|
+
minute: "2-digit",
|
|
127
|
+
hourCycle: "h23",
|
|
128
|
+
});
|
|
129
|
+
formatters.set(timezone, formatter);
|
|
130
|
+
}
|
|
131
|
+
const values = Object.fromEntries(
|
|
132
|
+
formatter
|
|
133
|
+
.formatToParts(date)
|
|
134
|
+
.filter((part) => part.type !== "literal")
|
|
135
|
+
.map((part) => [part.type, Number.parseInt(part.value, 10)]),
|
|
136
|
+
) as Record<string, number>;
|
|
137
|
+
const year = values.year!;
|
|
138
|
+
const month = values.month!;
|
|
139
|
+
const dayOfMonth = values.day!;
|
|
140
|
+
return {
|
|
141
|
+
minute: values.minute!,
|
|
142
|
+
hour: values.hour!,
|
|
143
|
+
dayOfMonth,
|
|
144
|
+
month,
|
|
145
|
+
dayOfWeek: new Date(Date.UTC(year, month - 1, dayOfMonth)).getUTCDay(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function cronMatchesDate(
|
|
150
|
+
expression: string | ParsedCronExpression,
|
|
151
|
+
date: Date,
|
|
152
|
+
timezone?: string,
|
|
153
|
+
): boolean {
|
|
154
|
+
const cron = typeof expression === "string" ? parseCronExpression(expression) : expression;
|
|
155
|
+
const parts = dateParts(date, timezone);
|
|
156
|
+
return cronMatchesParts(cron, parts);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cronMatchesParts(
|
|
160
|
+
cron: ParsedCronExpression,
|
|
161
|
+
parts: ReturnType<typeof dateParts>,
|
|
162
|
+
): boolean {
|
|
163
|
+
if (!cron.minute.values.has(parts.minute)) return false;
|
|
164
|
+
if (!cron.hour.values.has(parts.hour)) return false;
|
|
165
|
+
if (!cron.month.values.has(parts.month)) return false;
|
|
166
|
+
const dom = cron.dayOfMonth.values.has(parts.dayOfMonth);
|
|
167
|
+
const dow = cron.dayOfWeek.values.has(parts.dayOfWeek);
|
|
168
|
+
const dayMatches = cron.dayOfMonth.wildcard ? dow : cron.dayOfWeek.wildcard ? dom : dom || dow;
|
|
169
|
+
return dayMatches;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Return the next matching real instant, strictly after `after`. */
|
|
173
|
+
export function computeNextCronRun(
|
|
174
|
+
expression: string,
|
|
175
|
+
after: Date = new Date(),
|
|
176
|
+
timezone?: string,
|
|
177
|
+
): Date {
|
|
178
|
+
const parsed = parseCronExpression(expression);
|
|
179
|
+
if (timezone && !normalizeCronTimezone(timezone))
|
|
180
|
+
throw new Error(`invalid timezone: ${timezone}`);
|
|
181
|
+
const candidate = new Date(after.getTime());
|
|
182
|
+
candidate.setSeconds(0, 0);
|
|
183
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
184
|
+
const deadline = candidate.getTime() + 366 * 24 * 60 * 60_000 * 5;
|
|
185
|
+
while (candidate.getTime() <= deadline) {
|
|
186
|
+
const parts = dateParts(candidate, timezone);
|
|
187
|
+
if (cronMatchesParts(parsed, parts)) return candidate;
|
|
188
|
+
|
|
189
|
+
const dom = parsed.dayOfMonth.values.has(parts.dayOfMonth);
|
|
190
|
+
const dow = parsed.dayOfWeek.values.has(parts.dayOfWeek);
|
|
191
|
+
const dayMatches = parsed.dayOfMonth.wildcard
|
|
192
|
+
? dow
|
|
193
|
+
: parsed.dayOfWeek.wildcard
|
|
194
|
+
? dom
|
|
195
|
+
: dom || dow;
|
|
196
|
+
if (
|
|
197
|
+
!parsed.month.values.has(parts.month) ||
|
|
198
|
+
!dayMatches ||
|
|
199
|
+
!parsed.hour.values.has(parts.hour)
|
|
200
|
+
) {
|
|
201
|
+
// Skip to the next local-hour boundary. This keeps DST and non-whole-hour
|
|
202
|
+
// timezone offsets correct while reducing sparse schedules from ~2.6m
|
|
203
|
+
// minute checks to at most a few thousand hourly checks.
|
|
204
|
+
const minutes = parts.minute === 0 ? 60 : 60 - parts.minute;
|
|
205
|
+
candidate.setTime(candidate.getTime() + minutes * 60_000);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
candidate.setTime(candidate.getTime() + 60_000);
|
|
209
|
+
}
|
|
210
|
+
throw new Error("cron expression has no match within 5 years");
|
|
211
|
+
}
|