negotium 0.1.12 → 0.1.13

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 (137) hide show
  1. package/dist/canonical-mcp-bridge.js +2 -3
  2. package/dist/canonical-mcp-bridge.js.map +1 -1
  3. package/dist/chunk-a9y2rwxf.js.map +1 -1
  4. package/dist/{chunk-j0kjscb7.js → chunk-s2gez3wg.js} +20 -2
  5. package/dist/{chunk-j0kjscb7.js.map → chunk-s2gez3wg.js.map} +2 -2
  6. package/dist/cron.js +49 -25
  7. package/dist/cron.js.map +15 -14
  8. package/dist/hosted-agent.js +3012 -17
  9. package/dist/hosted-agent.js.map +30 -3
  10. package/dist/main.js +31323 -36
  11. package/dist/main.js.map +198 -3
  12. package/dist/prompts.js +11 -8
  13. package/dist/prompts.js.map +4 -4
  14. package/dist/runtime/src/application/submit-user-message.ts +3 -0
  15. package/dist/runtime/src/index.ts +7 -0
  16. package/dist/runtime/src/mcp/runtime-spec.ts +3 -0
  17. package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
  18. package/dist/runtime/src/platform/mcp-config.ts +13 -0
  19. package/dist/runtime/src/prompts/builders.ts +20 -6
  20. package/dist/runtime/src/query/active-rooms.ts +2 -0
  21. package/dist/runtime/src/runtime/delivery-ack.ts +112 -0
  22. package/dist/runtime/src/runtime/errors.ts +12 -12
  23. package/dist/runtime/src/runtime/turn-runner.ts +12 -0
  24. package/dist/runtime/src/storage/runtime-turn-requests.ts +1 -0
  25. package/dist/runtime/src/types/api.ts +2 -0
  26. package/dist/runtime/src/types.ts +2 -0
  27. package/dist/runtime/src/version.ts +1 -1
  28. package/dist/runtime-helpers.js.map +1 -1
  29. package/dist/storage.js.map +1 -1
  30. package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +2 -0
  31. package/dist/types/packages/core/src/platform/mcp-config.d.ts +4 -0
  32. package/dist/types/packages/core/src/prompts/builders.d.ts +2 -0
  33. package/dist/types/packages/core/src/types/api.d.ts +2 -0
  34. package/dist/types/packages/core/src/types.d.ts +2 -0
  35. package/dist/types/packages/core/src/version.d.ts +1 -1
  36. package/dist/vault.js.map +1 -1
  37. package/package.json +1 -1
  38. package/dist/chunk-00q25k7p.js +0 -2387
  39. package/dist/chunk-00q25k7p.js.map +0 -26
  40. package/dist/chunk-03kv7mvj.js +0 -2152
  41. package/dist/chunk-03kv7mvj.js.map +0 -15
  42. package/dist/chunk-05pm51dh.js +0 -589
  43. package/dist/chunk-05pm51dh.js.map +0 -11
  44. package/dist/chunk-0eh16bk1.js +0 -2936
  45. package/dist/chunk-0eh16bk1.js.map +0 -20
  46. package/dist/chunk-0yfqyv88.js +0 -108
  47. package/dist/chunk-0yfqyv88.js.map +0 -10
  48. package/dist/chunk-1k7t7bnx.js +0 -97
  49. package/dist/chunk-1k7t7bnx.js.map +0 -10
  50. package/dist/chunk-3gkfr9vb.js +0 -431
  51. package/dist/chunk-3gkfr9vb.js.map +0 -11
  52. package/dist/chunk-5akd1gbj.js +0 -281
  53. package/dist/chunk-5akd1gbj.js.map +0 -10
  54. package/dist/chunk-7k19pcg8.js +0 -577
  55. package/dist/chunk-7k19pcg8.js.map +0 -14
  56. package/dist/chunk-81tbfbmt.js +0 -10
  57. package/dist/chunk-81tbfbmt.js.map +0 -10
  58. package/dist/chunk-82qhv1mq.js +0 -512
  59. package/dist/chunk-82qhv1mq.js.map +0 -13
  60. package/dist/chunk-8dy7e2a9.js +0 -103
  61. package/dist/chunk-8dy7e2a9.js.map +0 -10
  62. package/dist/chunk-8f2sqmrj.js +0 -1712
  63. package/dist/chunk-8f2sqmrj.js.map +0 -18
  64. package/dist/chunk-9hjeemfz.js +0 -372
  65. package/dist/chunk-9hjeemfz.js.map +0 -10
  66. package/dist/chunk-a4aa2ccv.js +0 -141
  67. package/dist/chunk-a4aa2ccv.js.map +0 -10
  68. package/dist/chunk-bsg215q3.js +0 -3536
  69. package/dist/chunk-bsg215q3.js.map +0 -20
  70. package/dist/chunk-bxnfsc7w.js +0 -58
  71. package/dist/chunk-bxnfsc7w.js.map +0 -10
  72. package/dist/chunk-c500xe9z.js +0 -33
  73. package/dist/chunk-c500xe9z.js.map +0 -10
  74. package/dist/chunk-cngrf7ab.js +0 -149
  75. package/dist/chunk-cngrf7ab.js.map +0 -10
  76. package/dist/chunk-d7tprf06.js +0 -53
  77. package/dist/chunk-d7tprf06.js.map +0 -10
  78. package/dist/chunk-ddnrmkrv.js +0 -61
  79. package/dist/chunk-ddnrmkrv.js.map +0 -10
  80. package/dist/chunk-edb07sjp.js +0 -567
  81. package/dist/chunk-edb07sjp.js.map +0 -14
  82. package/dist/chunk-ejcm6zx8.js +0 -591
  83. package/dist/chunk-ejcm6zx8.js.map +0 -11
  84. package/dist/chunk-ezs352qs.js +0 -237
  85. package/dist/chunk-ezs352qs.js.map +0 -11
  86. package/dist/chunk-f5xgprg1.js +0 -59
  87. package/dist/chunk-f5xgprg1.js.map +0 -10
  88. package/dist/chunk-fbtb3v0s.js +0 -1339
  89. package/dist/chunk-fbtb3v0s.js.map +0 -16
  90. package/dist/chunk-ffefbd3r.js +0 -17
  91. package/dist/chunk-ffefbd3r.js.map +0 -10
  92. package/dist/chunk-gaknyw04.js +0 -708
  93. package/dist/chunk-gaknyw04.js.map +0 -14
  94. package/dist/chunk-gxmny6tq.js +0 -1637
  95. package/dist/chunk-gxmny6tq.js.map +0 -15
  96. package/dist/chunk-h7bj5zwb.js +0 -3069
  97. package/dist/chunk-h7bj5zwb.js.map +0 -32
  98. package/dist/chunk-hp4g7wx7.js +0 -46
  99. package/dist/chunk-hp4g7wx7.js.map +0 -10
  100. package/dist/chunk-hwy3n31w.js +0 -21
  101. package/dist/chunk-hwy3n31w.js.map +0 -9
  102. package/dist/chunk-jzz91jwh.js +0 -70
  103. package/dist/chunk-jzz91jwh.js.map +0 -10
  104. package/dist/chunk-k778y16p.js +0 -153
  105. package/dist/chunk-k778y16p.js.map +0 -11
  106. package/dist/chunk-mh5mexsf.js +0 -147
  107. package/dist/chunk-mh5mexsf.js.map +0 -11
  108. package/dist/chunk-mnwaext5.js +0 -318
  109. package/dist/chunk-mnwaext5.js.map +0 -10
  110. package/dist/chunk-nkgs40ch.js +0 -23
  111. package/dist/chunk-nkgs40ch.js.map +0 -10
  112. package/dist/chunk-pz57vwsm.js +0 -250
  113. package/dist/chunk-pz57vwsm.js.map +0 -11
  114. package/dist/chunk-r6kdp7gn.js +0 -252
  115. package/dist/chunk-r6kdp7gn.js.map +0 -10
  116. package/dist/chunk-rkvrkcgx.js +0 -117
  117. package/dist/chunk-rkvrkcgx.js.map +0 -10
  118. package/dist/chunk-s74rdyqe.js +0 -117
  119. package/dist/chunk-s74rdyqe.js.map +0 -12
  120. package/dist/chunk-sa874j5c.js +0 -9
  121. package/dist/chunk-sa874j5c.js.map +0 -10
  122. package/dist/chunk-sffqdkvq.js +0 -122
  123. package/dist/chunk-sffqdkvq.js.map +0 -10
  124. package/dist/chunk-tcs9qjps.js +0 -64
  125. package/dist/chunk-tcs9qjps.js.map +0 -10
  126. package/dist/chunk-wx83ep8e.js +0 -131
  127. package/dist/chunk-wx83ep8e.js.map +0 -10
  128. package/dist/chunk-xag86ng3.js +0 -122
  129. package/dist/chunk-xag86ng3.js.map +0 -10
  130. package/dist/chunk-xfdkep71.js +0 -371
  131. package/dist/chunk-xfdkep71.js.map +0 -11
  132. package/dist/chunk-xhm1rfpt.js +0 -2907
  133. package/dist/chunk-xhm1rfpt.js.map +0 -24
  134. package/dist/chunk-xpcps28p.js +0 -2138
  135. package/dist/chunk-xpcps28p.js.map +0 -29
  136. package/dist/chunk-yb4rbev9.js +0 -214
  137. package/dist/chunk-yb4rbev9.js.map +0 -10
@@ -1,1712 +0,0 @@
1
- // @bun
2
- import {
3
- backgroundSessionProgress,
4
- registerBackgroundSessionProvider
5
- } from "./chunk-h7bj5zwb.js";
6
- import"./chunk-tcs9qjps.js";
7
- import {
8
- purgeTopicLogs,
9
- rotateTopicLogs
10
- } from "./chunk-7k19pcg8.js";
11
- import"./chunk-8dy7e2a9.js";
12
- import"./chunk-9hjeemfz.js";
13
- import"./chunk-1k7t7bnx.js";
14
- import {
15
- triggerTopicAiTurn
16
- } from "./chunk-xhm1rfpt.js";
17
- import {
18
- isParticipant
19
- } from "./chunk-mnwaext5.js";
20
- import"./chunk-xpcps28p.js";
21
- import {
22
- runtimeBus
23
- } from "./chunk-fbtb3v0s.js";
24
- import {
25
- getTopic
26
- } from "./chunk-ejcm6zx8.js";
27
- import"./chunk-hp4g7wx7.js";
28
- import"./chunk-s74rdyqe.js";
29
- import"./chunk-k778y16p.js";
30
- import"./chunk-xag86ng3.js";
31
- import"./chunk-wx83ep8e.js";
32
- import {
33
- buildStdioMcpServer,
34
- registerRuntimeMcpServer
35
- } from "./chunk-00q25k7p.js";
36
- import"./chunk-gaknyw04.js";
37
- import"./chunk-mh5mexsf.js";
38
- import {
39
- WORKSPACE_DIR,
40
- resolveTopicWorkspaceDir
41
- } from "./chunk-ezs352qs.js";
42
- import"./chunk-j0kjscb7.js";
43
- import {
44
- abortRoom,
45
- cancelDeferredInject,
46
- getRoomQuery,
47
- listRuntimeTurnLeases
48
- } from "./chunk-82qhv1mq.js";
49
- import {
50
- logger
51
- } from "./chunk-ffefbd3r.js";
52
- import {
53
- db
54
- } from "./chunk-81tbfbmt.js";
55
- import"./chunk-sffqdkvq.js";
56
- import"./chunk-f5xgprg1.js";
57
- import"./chunk-hwy3n31w.js";
58
-
59
- // ../../packages/module-cron/src/store.ts
60
- import { randomUUID } from "crypto";
61
-
62
- // ../../packages/module-cron/src/schedule.ts
63
- var FIELD_RANGES = [
64
- [0, 59],
65
- [0, 23],
66
- [1, 31],
67
- [1, 12],
68
- [0, 7]
69
- ];
70
- function parseNumber(raw, min, max) {
71
- if (!/^\d+$/.test(raw))
72
- throw new Error(`invalid cron value: ${raw}`);
73
- const value = Number.parseInt(raw, 10);
74
- if (value < min || value > max) {
75
- throw new Error(`cron value ${value} is outside ${min}-${max}`);
76
- }
77
- return value;
78
- }
79
- function parseField(raw, min, max, normalize) {
80
- const values = new Set;
81
- const wildcard = raw === "*";
82
- for (const item of raw.split(",")) {
83
- if (!item)
84
- throw new Error("empty cron list item");
85
- const [base, stepRaw, extra] = item.split("/");
86
- if (extra !== undefined)
87
- throw new Error(`invalid cron step: ${item}`);
88
- const step = stepRaw === undefined ? 1 : parseNumber(stepRaw, 1, max - min + 1);
89
- let start;
90
- let end;
91
- if (base === "*") {
92
- start = min;
93
- end = max;
94
- } else if (base.includes("-")) {
95
- const [startRaw, endRaw, rangeExtra] = base.split("-");
96
- if (!startRaw || !endRaw || rangeExtra !== undefined) {
97
- throw new Error(`invalid cron range: ${base}`);
98
- }
99
- start = parseNumber(startRaw, min, max);
100
- end = parseNumber(endRaw, min, max);
101
- if (end < start)
102
- throw new Error(`descending cron range: ${base}`);
103
- } else {
104
- start = parseNumber(base, min, max);
105
- end = stepRaw === undefined ? start : max;
106
- }
107
- for (let value = start;value <= end; value += step) {
108
- values.add(normalize ? normalize(value) : value);
109
- }
110
- }
111
- return { values, wildcard };
112
- }
113
- function parseCronExpression(expression) {
114
- const fields = expression.trim().split(/\s+/);
115
- if (fields.length !== 5)
116
- throw new Error("cron expression must have exactly 5 fields");
117
- const parsed = fields.map((field, index) => {
118
- const [min, max] = FIELD_RANGES[index];
119
- return parseField(field, min, max, index === 4 ? (value) => value === 7 ? 0 : value : undefined);
120
- });
121
- return {
122
- minute: parsed[0],
123
- hour: parsed[1],
124
- dayOfMonth: parsed[2],
125
- month: parsed[3],
126
- dayOfWeek: parsed[4]
127
- };
128
- }
129
- function validateCronExpression(expression) {
130
- try {
131
- parseCronExpression(expression);
132
- return { ok: true };
133
- } catch (error) {
134
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
135
- }
136
- }
137
- var formatters = new Map;
138
- function normalizeCronTimezone(timezone) {
139
- const value = timezone?.trim();
140
- if (!value)
141
- return;
142
- try {
143
- new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date);
144
- return value;
145
- } catch {
146
- return;
147
- }
148
- }
149
- function dateParts(date, timezone) {
150
- if (!timezone) {
151
- return {
152
- minute: date.getMinutes(),
153
- hour: date.getHours(),
154
- dayOfMonth: date.getDate(),
155
- month: date.getMonth() + 1,
156
- dayOfWeek: date.getDay()
157
- };
158
- }
159
- let formatter = formatters.get(timezone);
160
- if (!formatter) {
161
- formatter = new Intl.DateTimeFormat("en-US", {
162
- timeZone: timezone,
163
- year: "numeric",
164
- month: "2-digit",
165
- day: "2-digit",
166
- hour: "2-digit",
167
- minute: "2-digit",
168
- hourCycle: "h23"
169
- });
170
- formatters.set(timezone, formatter);
171
- }
172
- const values = Object.fromEntries(formatter.formatToParts(date).filter((part) => part.type !== "literal").map((part) => [part.type, Number.parseInt(part.value, 10)]));
173
- const year = values.year;
174
- const month = values.month;
175
- const dayOfMonth = values.day;
176
- return {
177
- minute: values.minute,
178
- hour: values.hour,
179
- dayOfMonth,
180
- month,
181
- dayOfWeek: new Date(Date.UTC(year, month - 1, dayOfMonth)).getUTCDay()
182
- };
183
- }
184
- function cronMatchesDate(expression, date, timezone) {
185
- const cron = typeof expression === "string" ? parseCronExpression(expression) : expression;
186
- const parts = dateParts(date, timezone);
187
- return cronMatchesParts(cron, parts);
188
- }
189
- function cronMatchesParts(cron, parts) {
190
- if (!cron.minute.values.has(parts.minute))
191
- return false;
192
- if (!cron.hour.values.has(parts.hour))
193
- return false;
194
- if (!cron.month.values.has(parts.month))
195
- return false;
196
- const dom = cron.dayOfMonth.values.has(parts.dayOfMonth);
197
- const dow = cron.dayOfWeek.values.has(parts.dayOfWeek);
198
- const dayMatches = cron.dayOfMonth.wildcard ? dow : cron.dayOfWeek.wildcard ? dom : dom || dow;
199
- return dayMatches;
200
- }
201
- function computeNextCronRun(expression, after = new Date, timezone) {
202
- const parsed = parseCronExpression(expression);
203
- if (timezone && !normalizeCronTimezone(timezone))
204
- throw new Error(`invalid timezone: ${timezone}`);
205
- const candidate = new Date(after.getTime());
206
- candidate.setSeconds(0, 0);
207
- candidate.setMinutes(candidate.getMinutes() + 1);
208
- const deadline = candidate.getTime() + 366 * 24 * 60 * 60000 * 5;
209
- while (candidate.getTime() <= deadline) {
210
- const parts = dateParts(candidate, timezone);
211
- if (cronMatchesParts(parsed, parts))
212
- return candidate;
213
- const dom = parsed.dayOfMonth.values.has(parts.dayOfMonth);
214
- const dow = parsed.dayOfWeek.values.has(parts.dayOfWeek);
215
- const dayMatches = parsed.dayOfMonth.wildcard ? dow : parsed.dayOfWeek.wildcard ? dom : dom || dow;
216
- if (!parsed.month.values.has(parts.month) || !dayMatches || !parsed.hour.values.has(parts.hour)) {
217
- const minutes = parts.minute === 0 ? 60 : 60 - parts.minute;
218
- candidate.setTime(candidate.getTime() + minutes * 60000);
219
- continue;
220
- }
221
- candidate.setTime(candidate.getTime() + 60000);
222
- }
223
- throw new Error("cron expression has no match within 5 years");
224
- }
225
-
226
- // ../../packages/module-cron/src/scripts.ts
227
- import { spawn } from "child_process";
228
- import { existsSync, mkdirSync, readdirSync } from "fs";
229
- import { resolve, sep } from "path";
230
- var CRON_JOBS_DIR = resolve(process.env.NEGOTIUM_CRON_JOBS_DIR?.trim() || resolve(WORKSPACE_DIR, "cron", "jobs"));
231
- var DEFAULT_SCRIPT_TIMEOUT_MS = 10 * 60000;
232
- var DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
233
- var KILL_GRACE_MS = 2500;
234
-
235
- class CronScriptError extends Error {
236
- kind;
237
- constructor(message, kind = "error") {
238
- super(message);
239
- this.kind = kind;
240
- this.name = "CronScriptError";
241
- }
242
- }
243
- function validateCronScriptName(script) {
244
- if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*\.py$/.test(script) || script.includes("..") || script.includes("/") || script.includes("\\")) {
245
- return { ok: false, error: `script must be a plain .py filename under ${CRON_JOBS_DIR}` };
246
- }
247
- return { ok: true };
248
- }
249
- function resolveCronScriptPath(script) {
250
- const valid = validateCronScriptName(script);
251
- if (!valid.ok)
252
- throw new Error(valid.error);
253
- const path = resolve(CRON_JOBS_DIR, script);
254
- if (!path.startsWith(`${CRON_JOBS_DIR}${sep}`))
255
- throw new Error("script path escaped jobs directory");
256
- return path;
257
- }
258
- function listCronScripts() {
259
- mkdirSync(CRON_JOBS_DIR, { recursive: true });
260
- return readdirSync(CRON_JOBS_DIR, { withFileTypes: true }).filter((entry) => entry.isFile() && validateCronScriptName(entry.name).ok).map((entry) => entry.name).sort();
261
- }
262
- function cronScriptExists(script) {
263
- try {
264
- return existsSync(resolveCronScriptPath(script));
265
- } catch {
266
- return false;
267
- }
268
- }
269
- function commandForScript(scriptPath) {
270
- const configured = process.env.NEGOTIUM_CRON_PYTHON?.trim();
271
- if (configured)
272
- return { executable: configured, args: [scriptPath] };
273
- if (Bun.which("uv")) {
274
- return {
275
- executable: "uv",
276
- args: ["run", "--project", CRON_JOBS_DIR, "python", scriptPath]
277
- };
278
- }
279
- return { executable: "python3", args: [scriptPath] };
280
- }
281
- function signalTree(child, signal) {
282
- if (!child.pid)
283
- return;
284
- try {
285
- process.kill(-child.pid, signal);
286
- } catch {
287
- try {
288
- child.kill(signal);
289
- } catch {}
290
- }
291
- }
292
- async function runCronPromptScript(options) {
293
- if (options.signal?.aborted)
294
- throw new CronScriptError("script run aborted");
295
- const scriptPath = resolveCronScriptPath(options.script);
296
- if (!existsSync(scriptPath))
297
- throw new CronScriptError(`cron script not found: ${options.script}`);
298
- const timeoutMs = Math.max(5000, options.timeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS);
299
- const outputLimitBytes = Math.max(1024, options.outputLimitBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES);
300
- const command = commandForScript(scriptPath);
301
- let child;
302
- try {
303
- child = spawn(command.executable, command.args, {
304
- cwd: options.cwd,
305
- detached: true,
306
- env: {
307
- ...process.env,
308
- NEGOTIUM_CRON_JOB_ID: options.jobId,
309
- NEGOTIUM_CRON_TOPIC_ID: options.topicId
310
- },
311
- stdio: ["ignore", "pipe", "pipe"]
312
- });
313
- } catch (error) {
314
- throw new CronScriptError(`script spawn failed: ${error instanceof Error ? error.message : String(error)}`);
315
- }
316
- if (!child.stdout || !child.stderr) {
317
- signalTree(child, "SIGKILL");
318
- throw new CronScriptError("script spawn failed: stdout/stderr pipes unavailable");
319
- }
320
- const stdout = [];
321
- const stderr = [];
322
- let outputBytes = 0;
323
- let failure;
324
- let killTimer;
325
- const terminate = (error) => {
326
- if (failure)
327
- return;
328
- failure = error;
329
- signalTree(child, "SIGTERM");
330
- killTimer = setTimeout(() => signalTree(child, "SIGKILL"), KILL_GRACE_MS);
331
- killTimer.unref?.();
332
- };
333
- const collect = (target) => (chunk) => {
334
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
335
- outputBytes += buffer.length;
336
- if (outputBytes > outputLimitBytes) {
337
- terminate(new CronScriptError(`script output exceeded ${outputLimitBytes} bytes`, "output-limit"));
338
- return;
339
- }
340
- target.push(buffer);
341
- };
342
- child.stdout.on("data", collect(stdout));
343
- child.stderr.on("data", collect(stderr));
344
- const timeout = setTimeout(() => terminate(new CronScriptError(`script exceeded ${timeoutMs}ms`, "timeout")), timeoutMs);
345
- timeout.unref?.();
346
- const onAbort = () => terminate(new CronScriptError("script run aborted"));
347
- options.signal?.addEventListener("abort", onAbort, { once: true });
348
- const result = await new Promise((resolvePromise, reject) => {
349
- child.once("error", reject);
350
- child.once("close", (code, signal) => resolvePromise({ code, signal }));
351
- }).finally(() => {
352
- clearTimeout(timeout);
353
- if (killTimer)
354
- clearTimeout(killTimer);
355
- options.signal?.removeEventListener("abort", onAbort);
356
- });
357
- if (failure)
358
- throw failure;
359
- if (result.code !== 0) {
360
- const detail = result.signal ? `signal ${result.signal}` : `code ${result.code ?? "unknown"}`;
361
- const errorText = Buffer.concat(stderr).toString("utf8").trim();
362
- throw new CronScriptError(`script exited with ${detail}${errorText ? `: ${errorText.slice(0, 2000)}` : ""}`);
363
- }
364
- return Buffer.concat(stdout).toString("utf8").trim();
365
- }
366
-
367
- // ../../packages/module-cron/src/store.ts
368
- var schemaReady = false;
369
- var db2 = db;
370
- function configureCronDatabase(database) {
371
- const previous = db2;
372
- db2 = database;
373
- schemaReady = false;
374
- let disposed = false;
375
- return () => {
376
- if (disposed)
377
- return;
378
- disposed = true;
379
- db2 = previous;
380
- schemaReady = false;
381
- };
382
- }
383
- function ensureCronSchema() {
384
- if (schemaReady)
385
- return;
386
- db2.exec(`
387
- CREATE TABLE IF NOT EXISTS negotium_cron_jobs (
388
- id TEXT PRIMARY KEY,
389
- name TEXT NOT NULL,
390
- owner_user_id TEXT NOT NULL,
391
- topic_id TEXT NOT NULL REFERENCES api_topics(id) ON DELETE CASCADE,
392
- prompt TEXT NOT NULL,
393
- script TEXT,
394
- summary TEXT,
395
- schedule TEXT NOT NULL,
396
- timezone TEXT,
397
- enabled INTEGER NOT NULL DEFAULT 1,
398
- agent TEXT,
399
- model TEXT,
400
- effort TEXT,
401
- session_id TEXT,
402
- next_run_at TEXT NOT NULL,
403
- created_at TEXT NOT NULL,
404
- updated_at TEXT NOT NULL
405
- );
406
- CREATE UNIQUE INDEX IF NOT EXISTS idx_negotium_cron_owner_name
407
- ON negotium_cron_jobs(owner_user_id, name);
408
- CREATE INDEX IF NOT EXISTS idx_negotium_cron_due
409
- ON negotium_cron_jobs(enabled, next_run_at);
410
- CREATE TABLE IF NOT EXISTS negotium_cron_runs (
411
- id TEXT PRIMARY KEY,
412
- job_id TEXT NOT NULL REFERENCES negotium_cron_jobs(id) ON DELETE CASCADE,
413
- source TEXT NOT NULL,
414
- scheduled_at TEXT NOT NULL,
415
- started_at TEXT,
416
- finished_at TEXT,
417
- status TEXT NOT NULL,
418
- query_id TEXT,
419
- duration_ms INTEGER,
420
- output_preview TEXT,
421
- error TEXT,
422
- exit_code INTEGER,
423
- topic_id TEXT
424
- );
425
- CREATE INDEX IF NOT EXISTS idx_negotium_cron_runs_job
426
- ON negotium_cron_runs(job_id, scheduled_at DESC);
427
- CREATE TABLE IF NOT EXISTS negotium_cron_requests (
428
- id TEXT PRIMARY KEY,
429
- job_id TEXT NOT NULL REFERENCES negotium_cron_jobs(id) ON DELETE CASCADE,
430
- requested_at TEXT NOT NULL
431
- );
432
- CREATE TABLE IF NOT EXISTS negotium_cron_cancellations (
433
- id TEXT PRIMARY KEY,
434
- job_id TEXT NOT NULL REFERENCES negotium_cron_jobs(id) ON DELETE CASCADE,
435
- requested_at TEXT NOT NULL
436
- );
437
- CREATE TABLE IF NOT EXISTS negotium_cron_topic_sessions (
438
- topic_id TEXT NOT NULL,
439
- agent TEXT NOT NULL,
440
- owner_user_id TEXT NOT NULL,
441
- session_id TEXT NOT NULL,
442
- created_at TEXT NOT NULL,
443
- updated_at TEXT NOT NULL,
444
- PRIMARY KEY (topic_id, agent)
445
- );
446
- CREATE INDEX IF NOT EXISTS idx_negotium_cron_topic_sessions_owner
447
- ON negotium_cron_topic_sessions(owner_user_id, topic_id);
448
- CREATE TABLE IF NOT EXISTS negotium_cron_topic_context (
449
- topic_id TEXT PRIMARY KEY REFERENCES api_topics(id) ON DELETE CASCADE,
450
- successful_runs_since_rotation INTEGER NOT NULL DEFAULT 0,
451
- last_rotated_at TEXT,
452
- updated_at TEXT NOT NULL
453
- );
454
- `);
455
- const jobColumns = new Set(db2.query("PRAGMA table_info(negotium_cron_jobs)").all().map((column) => column.name));
456
- if (!jobColumns.has("script")) {
457
- db2.exec("ALTER TABLE negotium_cron_jobs ADD COLUMN script TEXT");
458
- }
459
- if (!jobColumns.has("summary")) {
460
- db2.exec("ALTER TABLE negotium_cron_jobs ADD COLUMN summary TEXT");
461
- }
462
- const runColumns = new Set(db2.query("PRAGMA table_info(negotium_cron_runs)").all().map((column) => column.name));
463
- if (!runColumns.has("exit_code")) {
464
- db2.exec("ALTER TABLE negotium_cron_runs ADD COLUMN exit_code INTEGER");
465
- }
466
- if (!runColumns.has("topic_id")) {
467
- db2.exec("ALTER TABLE negotium_cron_runs ADD COLUMN topic_id TEXT");
468
- }
469
- const legacySessions = db2.query(`SELECT j.topic_id, COALESCE(j.agent, t.agent) AS agent,
470
- j.owner_user_id, j.session_id, j.created_at, j.updated_at
471
- FROM negotium_cron_jobs j
472
- LEFT JOIN api_topics t ON t.id = j.topic_id
473
- WHERE j.session_id IS NOT NULL AND COALESCE(j.agent, t.agent) IS NOT NULL
474
- ORDER BY j.updated_at ASC`).all();
475
- for (const legacy of legacySessions) {
476
- db2.query(`INSERT INTO negotium_cron_topic_sessions
477
- (topic_id,agent,owner_user_id,session_id,created_at,updated_at)
478
- VALUES (?,?,?,?,?,?)
479
- ON CONFLICT(topic_id,agent) DO UPDATE SET
480
- owner_user_id = excluded.owner_user_id,
481
- session_id = excluded.session_id,
482
- updated_at = excluded.updated_at`).run(legacy.topic_id, legacy.agent, legacy.owner_user_id, legacy.session_id, legacy.created_at, legacy.updated_at);
483
- }
484
- if (legacySessions.length > 0) {
485
- db2.query("UPDATE negotium_cron_jobs SET session_id = NULL WHERE session_id IS NOT NULL").run();
486
- }
487
- schemaReady = true;
488
- }
489
- function toJob(row) {
490
- const topicAgent = row.agent ? row.agent : db2.query("SELECT agent FROM api_topics WHERE id = ?").get(row.topic_id)?.agent;
491
- const topicSession = topicAgent ? db2.query("SELECT session_id FROM negotium_cron_topic_sessions WHERE topic_id = ? AND agent = ?").get(row.topic_id, topicAgent) : undefined;
492
- return {
493
- id: row.id,
494
- name: row.name,
495
- ownerUserId: row.owner_user_id,
496
- topicId: row.topic_id,
497
- prompt: row.prompt.trim() || undefined,
498
- script: row.script?.trim() || undefined,
499
- summary: row.summary?.trim() || undefined,
500
- schedule: row.schedule,
501
- timezone: row.timezone ?? undefined,
502
- enabled: row.enabled !== 0,
503
- agent: row.agent ?? undefined,
504
- model: row.model ?? undefined,
505
- effort: row.effort ?? undefined,
506
- sessionId: topicSession?.session_id,
507
- nextRunAt: row.next_run_at,
508
- createdAt: row.created_at,
509
- updatedAt: row.updated_at
510
- };
511
- }
512
- function toTopicSession(row) {
513
- return {
514
- topicId: row.topic_id,
515
- agent: row.agent,
516
- ownerUserId: row.owner_user_id,
517
- sessionId: row.session_id,
518
- createdAt: row.created_at,
519
- updatedAt: row.updated_at
520
- };
521
- }
522
- function toTopicContext(row) {
523
- return {
524
- topicId: row.topic_id,
525
- successfulRunsSinceRotation: row.successful_runs_since_rotation,
526
- lastRotatedAt: row.last_rotated_at ?? undefined,
527
- updatedAt: row.updated_at
528
- };
529
- }
530
- function cronTopicSessionName(topicId) {
531
- return `cron-${topicId}`;
532
- }
533
- function toRun(row) {
534
- return {
535
- id: row.id,
536
- jobId: row.job_id,
537
- source: row.source,
538
- scheduledAt: row.scheduled_at,
539
- startedAt: row.started_at ?? undefined,
540
- finishedAt: row.finished_at ?? undefined,
541
- status: row.status,
542
- queryId: row.query_id ?? undefined,
543
- durationMs: row.duration_ms ?? undefined,
544
- outputPreview: row.output_preview ?? undefined,
545
- error: row.error ?? undefined,
546
- exitCode: row.exit_code ?? undefined
547
- };
548
- }
549
- function createCronJob(input) {
550
- ensureCronSchema();
551
- const prompt = input.prompt?.trim() || undefined;
552
- const script = input.script?.trim() || undefined;
553
- if (Boolean(prompt) === Boolean(script)) {
554
- throw new Error("cron job requires exactly one of prompt or script");
555
- }
556
- if (script) {
557
- const valid = validateCronScriptName(script);
558
- if (!valid.ok)
559
- throw new Error(valid.error);
560
- }
561
- const otherOwner = db2.query(`SELECT owner_user_id
562
- FROM negotium_cron_jobs
563
- WHERE topic_id = ? AND owner_user_id <> ?
564
- LIMIT 1`).get(input.topicId, input.ownerUserId);
565
- if (otherOwner) {
566
- throw new Error(`topic cron context is already owned by ${otherOwner.owner_user_id}; ` + "all cron jobs in one topic must share one owner");
567
- }
568
- parseCronExpression(input.schedule);
569
- const timezone = input.timezone ? normalizeCronTimezone(input.timezone) : undefined;
570
- if (input.timezone && !timezone)
571
- throw new Error(`invalid timezone: ${input.timezone}`);
572
- const now = input.now ?? new Date;
573
- const nowIso = now.toISOString();
574
- const job = {
575
- id: randomUUID(),
576
- name: input.name,
577
- ownerUserId: input.ownerUserId,
578
- topicId: input.topicId,
579
- prompt,
580
- script,
581
- summary: input.summary?.trim() || undefined,
582
- schedule: input.schedule.trim(),
583
- timezone,
584
- enabled: true,
585
- agent: input.agent,
586
- model: input.model,
587
- effort: input.effort,
588
- nextRunAt: computeNextCronRun(input.schedule, now, timezone).toISOString(),
589
- createdAt: nowIso,
590
- updatedAt: nowIso
591
- };
592
- db2.query(`INSERT INTO negotium_cron_jobs
593
- (id,name,owner_user_id,topic_id,prompt,script,summary,schedule,timezone,enabled,agent,model,effort,session_id,next_run_at,created_at,updated_at)
594
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(job.id, job.name, job.ownerUserId, job.topicId, job.prompt ?? "", job.script ?? null, job.summary ?? null, job.schedule, job.timezone ?? null, 1, job.agent ?? null, job.model ?? null, job.effort ?? null, null, job.nextRunAt, job.createdAt, job.updatedAt);
595
- return job;
596
- }
597
- function listCronJobs(ownerUserId) {
598
- ensureCronSchema();
599
- const rows = ownerUserId ? db2.query("SELECT * FROM negotium_cron_jobs WHERE owner_user_id = ? ORDER BY created_at DESC").all(ownerUserId) : db2.query("SELECT * FROM negotium_cron_jobs ORDER BY created_at DESC").all();
600
- return rows.map(toJob);
601
- }
602
- function listEnabledCronJobs() {
603
- ensureCronSchema();
604
- return db2.query("SELECT * FROM negotium_cron_jobs WHERE enabled = 1 ORDER BY created_at DESC").all().map(toJob);
605
- }
606
- function listCronJobsForTopic(topicId) {
607
- ensureCronSchema();
608
- return db2.query("SELECT * FROM negotium_cron_jobs WHERE topic_id = ? ORDER BY created_at DESC").all(topicId).map(toJob);
609
- }
610
- function listCronJobsForTopicOwner(topicId, ownerUserId) {
611
- ensureCronSchema();
612
- return db2.query("SELECT * FROM negotium_cron_jobs WHERE topic_id = ? AND owner_user_id = ? ORDER BY created_at DESC").all(topicId, ownerUserId).map(toJob);
613
- }
614
- function getCronJob(id) {
615
- ensureCronSchema();
616
- const row = db2.query("SELECT * FROM negotium_cron_jobs WHERE id = ?").get(id);
617
- return row ? toJob(row) : null;
618
- }
619
- function getCronJobByOwnerAndName(ownerUserId, name) {
620
- ensureCronSchema();
621
- const row = db2.query("SELECT * FROM negotium_cron_jobs WHERE owner_user_id = ? AND name = ?").get(ownerUserId, name);
622
- return row ? toJob(row) : null;
623
- }
624
- function cronJobPatchChangesContext(job, patch) {
625
- return patch.topicId !== undefined && patch.topicId !== job.topicId || patch.prompt !== undefined && (patch.prompt?.trim() || undefined) !== job.prompt || patch.script !== undefined && (patch.script?.trim() || undefined) !== job.script || patch.agent !== undefined && (patch.agent ?? undefined) !== job.agent || patch.model !== undefined && (patch.model?.trim() || undefined) !== job.model || patch.effort !== undefined && (patch.effort ?? undefined) !== job.effort;
626
- }
627
- function updateCronJob(id, patch, now = new Date) {
628
- ensureCronSchema();
629
- const job = getCronJob(id);
630
- if (!job)
631
- return null;
632
- if (Object.values(patch).every((value) => value === undefined))
633
- return job;
634
- const nextName = patch.name !== undefined ? patch.name.trim() : job.name;
635
- if (!/^[A-Za-z0-9_-]+$/.test(nextName)) {
636
- throw new Error("name must use only letters, numbers, dashes, and underscores");
637
- }
638
- const nextPrompt = patch.prompt !== undefined ? patch.prompt?.trim() || undefined : job.prompt;
639
- const nextScript = patch.script !== undefined ? patch.script?.trim() || undefined : job.script;
640
- if (Boolean(nextPrompt) === Boolean(nextScript)) {
641
- throw new Error("cron job requires exactly one of prompt or script");
642
- }
643
- if (nextScript) {
644
- const valid = validateCronScriptName(nextScript);
645
- if (!valid.ok)
646
- throw new Error(valid.error);
647
- }
648
- const nextSchedule = patch.schedule?.trim() ?? job.schedule;
649
- parseCronExpression(nextSchedule);
650
- const rawTimezone = patch.timezone !== undefined ? patch.timezone?.trim() || undefined : job.timezone;
651
- const nextTimezone = rawTimezone ? normalizeCronTimezone(rawTimezone) : undefined;
652
- if (rawTimezone && !nextTimezone)
653
- throw new Error(`invalid timezone: ${rawTimezone}`);
654
- const nextTopicId = patch.topicId?.trim() || job.topicId;
655
- const otherOwner = db2.query(`SELECT owner_user_id FROM negotium_cron_jobs
656
- WHERE topic_id = ? AND owner_user_id <> ? AND id <> ? LIMIT 1`).get(nextTopicId, job.ownerUserId, id);
657
- if (otherOwner) {
658
- throw new Error(`topic cron context is already owned by ${otherOwner.owner_user_id}; ` + "all cron jobs in one topic must share one owner");
659
- }
660
- const enabled = patch.enabled ?? job.enabled;
661
- const scheduleChanged = nextSchedule !== job.schedule || nextTimezone !== job.timezone;
662
- const nextRunAt = enabled && (scheduleChanged || !job.enabled && enabled) ? computeNextCronRun(nextSchedule, now, nextTimezone).toISOString() : job.nextRunAt;
663
- const timestamp = now.toISOString();
664
- const sessionSensitiveChanged = cronJobPatchChangesContext(job, patch);
665
- if (sessionSensitiveChanged) {
666
- const active = db2.query("SELECT 1 FROM negotium_cron_runs WHERE job_id = ? AND status IN ('pending','running') LIMIT 1").get(id);
667
- if (active) {
668
- throw new Error("cannot change Cron context while a run is active; cancel it first");
669
- }
670
- }
671
- db2.transaction(() => {
672
- db2.query(`UPDATE negotium_cron_jobs SET
673
- name = ?, topic_id = ?, prompt = ?, script = ?, summary = ?, schedule = ?, timezone = ?,
674
- enabled = ?, agent = ?, model = ?, effort = ?, next_run_at = ?, updated_at = ?
675
- WHERE id = ?`).run(nextName, nextTopicId, nextPrompt ?? "", nextScript ?? null, patch.summary !== undefined ? patch.summary?.trim() || null : job.summary ?? null, nextSchedule, nextTimezone ?? null, enabled ? 1 : 0, patch.agent !== undefined ? patch.agent : job.agent ?? null, patch.model !== undefined ? patch.model?.trim() || null : job.model ?? null, patch.effort !== undefined ? patch.effort : job.effort ?? null, nextRunAt, timestamp, id);
676
- })();
677
- return getCronJob(id);
678
- }
679
- function updateCronJobSummaryIfPromptMatches(id, expectedPrompt, summary) {
680
- ensureCronSchema();
681
- const result = db2.query("UPDATE negotium_cron_jobs SET summary = ? WHERE id = ? AND prompt = ?").run(summary.trim(), id, expectedPrompt.trim());
682
- return Number(result.changes) > 0;
683
- }
684
- function setCronJobEnabled(id, enabled, now = new Date) {
685
- ensureCronSchema();
686
- const job = getCronJob(id);
687
- if (!job)
688
- return null;
689
- const nextRunAt = enabled ? computeNextCronRun(job.schedule, now, job.timezone).toISOString() : job.nextRunAt;
690
- db2.query("UPDATE negotium_cron_jobs SET enabled = ?, next_run_at = ?, updated_at = ? WHERE id = ?").run(enabled ? 1 : 0, nextRunAt, now.toISOString(), id);
691
- return getCronJob(id);
692
- }
693
- function setCronJobSessionId(id, sessionId) {
694
- const job = getCronJob(id);
695
- if (!job)
696
- return;
697
- const topic = db2.query("SELECT agent FROM api_topics WHERE id = ?").get(job.topicId);
698
- const agent = job.agent ?? topic?.agent;
699
- if (!agent)
700
- return;
701
- if (sessionId)
702
- setCronTopicSession(job.topicId, agent, job.ownerUserId, sessionId);
703
- else
704
- resetCronTopicSessions(job.topicId);
705
- }
706
- function getCronTopicSession(topicId, agent) {
707
- ensureCronSchema();
708
- const row = db2.query("SELECT * FROM negotium_cron_topic_sessions WHERE topic_id = ? AND agent = ?").get(topicId, agent);
709
- return row ? toTopicSession(row) : null;
710
- }
711
- function listCronTopicSessions(topicId) {
712
- ensureCronSchema();
713
- return db2.query("SELECT * FROM negotium_cron_topic_sessions WHERE topic_id = ? ORDER BY agent").all(topicId).map(toTopicSession);
714
- }
715
- function setCronTopicSession(topicId, agent, ownerUserId, sessionId, now = new Date) {
716
- ensureCronSchema();
717
- const timestamp = now.toISOString();
718
- db2.query(`INSERT INTO negotium_cron_topic_sessions
719
- (topic_id,agent,owner_user_id,session_id,created_at,updated_at)
720
- VALUES (?,?,?,?,?,?)
721
- ON CONFLICT(topic_id,agent) DO UPDATE SET
722
- owner_user_id = excluded.owner_user_id,
723
- session_id = excluded.session_id,
724
- updated_at = excluded.updated_at`).run(topicId, agent, ownerUserId, sessionId, timestamp, timestamp);
725
- return getCronTopicSession(topicId, agent);
726
- }
727
- function setCronTopicSessionIfJobUpdatedAt(jobId, expectedJobUpdatedAt, topicId, agent, ownerUserId, sessionId, now = new Date) {
728
- ensureCronSchema();
729
- let updated = false;
730
- db2.transaction(() => {
731
- const current = db2.query("SELECT topic_id, updated_at FROM negotium_cron_jobs WHERE id = ?").get(jobId);
732
- if (!current || current.topic_id !== topicId || current.updated_at !== expectedJobUpdatedAt)
733
- return;
734
- setCronTopicSession(topicId, agent, ownerUserId, sessionId, now);
735
- updated = true;
736
- })();
737
- return updated ? getCronTopicSession(topicId, agent) : null;
738
- }
739
- function clearCronTopicSession(topicId, agent) {
740
- ensureCronSchema();
741
- const result = db2.query("DELETE FROM negotium_cron_topic_sessions WHERE topic_id = ? AND agent = ?").run(topicId, agent);
742
- return Number(result.changes) > 0;
743
- }
744
- function resetCronTopicSessions(topicId) {
745
- ensureCronSchema();
746
- const sessions = listCronTopicSessions(topicId);
747
- db2.query("DELETE FROM negotium_cron_topic_sessions WHERE topic_id = ?").run(topicId);
748
- return sessions;
749
- }
750
- function getCronTopicContext(topicId) {
751
- ensureCronSchema();
752
- const row = db2.query("SELECT * FROM negotium_cron_topic_context WHERE topic_id = ?").get(topicId);
753
- return row ? toTopicContext(row) : null;
754
- }
755
- function incrementCronTopicSuccess(topicId, now) {
756
- const timestamp = now.toISOString();
757
- db2.query(`INSERT INTO negotium_cron_topic_context
758
- (topic_id,successful_runs_since_rotation,last_rotated_at,updated_at)
759
- VALUES (?,1,NULL,?)
760
- ON CONFLICT(topic_id) DO UPDATE SET
761
- successful_runs_since_rotation = successful_runs_since_rotation + 1,
762
- updated_at = excluded.updated_at`).run(topicId, timestamp);
763
- return getCronTopicContext(topicId).successfulRunsSinceRotation;
764
- }
765
- function markCronTopicContextRotated(topicId, now = new Date) {
766
- ensureCronSchema();
767
- const timestamp = now.toISOString();
768
- db2.query(`INSERT INTO negotium_cron_topic_context
769
- (topic_id,successful_runs_since_rotation,last_rotated_at,updated_at)
770
- VALUES (?,0,?,?)
771
- ON CONFLICT(topic_id) DO UPDATE SET
772
- successful_runs_since_rotation = 0,
773
- last_rotated_at = excluded.last_rotated_at,
774
- updated_at = excluded.updated_at`).run(topicId, timestamp, timestamp);
775
- }
776
- function resetCronTopicContextState(topicId) {
777
- ensureCronSchema();
778
- const result = db2.query("DELETE FROM negotium_cron_topic_context WHERE topic_id = ?").run(topicId);
779
- return Number(result.changes) > 0;
780
- }
781
- function listOrphanedCronTopicSessions() {
782
- ensureCronSchema();
783
- return db2.query(`SELECT s.* FROM negotium_cron_topic_sessions s
784
- LEFT JOIN api_topics t ON t.id = s.topic_id
785
- WHERE t.id IS NULL
786
- ORDER BY s.topic_id, s.agent`).all().map(toTopicSession);
787
- }
788
- function deleteCronJob(id) {
789
- ensureCronSchema();
790
- const result = db2.query("DELETE FROM negotium_cron_jobs WHERE id = ?").run(id);
791
- return Number(result.changes) > 0;
792
- }
793
- function requestCronRun(jobId, now = new Date) {
794
- ensureCronSchema();
795
- const id = randomUUID();
796
- db2.query("INSERT INTO negotium_cron_requests (id,job_id,requested_at) VALUES (?,?,?)").run(id, jobId, now.toISOString());
797
- return id;
798
- }
799
- function requestCronCancel(jobId, now = new Date) {
800
- ensureCronSchema();
801
- const id = randomUUID();
802
- db2.query("INSERT INTO negotium_cron_cancellations (id,job_id,requested_at) VALUES (?,?,?)").run(id, jobId, now.toISOString());
803
- return id;
804
- }
805
- function claimCronCancellations(limit = 100) {
806
- ensureCronSchema();
807
- const jobIds = [];
808
- db2.transaction(() => {
809
- const rows = db2.query(`SELECT id, job_id FROM negotium_cron_cancellations
810
- ORDER BY requested_at LIMIT ?`).all(limit);
811
- for (const row of rows) {
812
- db2.query("DELETE FROM negotium_cron_cancellations WHERE id = ?").run(row.id);
813
- if (!jobIds.includes(row.job_id))
814
- jobIds.push(row.job_id);
815
- }
816
- })();
817
- return jobIds;
818
- }
819
- function insertRun(jobId, topicId, source, scheduledAt) {
820
- const run = {
821
- id: randomUUID(),
822
- jobId,
823
- source,
824
- scheduledAt,
825
- status: "pending"
826
- };
827
- db2.query("INSERT INTO negotium_cron_runs (id,job_id,topic_id,source,scheduled_at,status) VALUES (?,?,?,?,?,?)").run(run.id, run.jobId, topicId, run.source, run.scheduledAt, run.status);
828
- return run;
829
- }
830
- function claimCronRuns(now = new Date, limit = 20) {
831
- ensureCronSchema();
832
- const claimed = [];
833
- db2.transaction(() => {
834
- const requests = db2.query(`SELECT r.id AS request_id, r.requested_at, j.*
835
- FROM negotium_cron_requests r JOIN negotium_cron_jobs j ON j.id = r.job_id
836
- ORDER BY r.requested_at LIMIT ?`).all(limit);
837
- for (const row of requests) {
838
- db2.query("DELETE FROM negotium_cron_requests WHERE id = ?").run(row.request_id);
839
- claimed.push({
840
- job: toJob(row),
841
- run: insertRun(row.id, row.topic_id, "manual", row.requested_at)
842
- });
843
- }
844
- const remaining = Math.max(0, limit - claimed.length);
845
- if (remaining === 0)
846
- return;
847
- const due = db2.query(`SELECT * FROM negotium_cron_jobs
848
- WHERE enabled = 1 AND next_run_at <= ? ORDER BY next_run_at LIMIT ?`).all(now.toISOString(), remaining);
849
- for (const row of due) {
850
- const job = toJob(row);
851
- const run = insertRun(job.id, job.topicId, "schedule", job.nextRunAt);
852
- const nextRunAt = computeNextCronRun(job.schedule, now, job.timezone).toISOString();
853
- db2.query("UPDATE negotium_cron_jobs SET next_run_at = ? WHERE id = ?").run(nextRunAt, job.id);
854
- claimed.push({ job: { ...job, nextRunAt }, run });
855
- }
856
- })();
857
- return claimed;
858
- }
859
- function markCronRunStarted(runId, queryId, now = new Date) {
860
- ensureCronSchema();
861
- db2.query(`UPDATE negotium_cron_runs
862
- SET status = 'running', query_id = ?, started_at = COALESCE(started_at, ?)
863
- WHERE id = ?`).run(queryId, now.toISOString(), runId);
864
- }
865
- function finishCronRun(runId, result, now = new Date) {
866
- ensureCronSchema();
867
- const row = db2.query(`SELECT r.started_at, r.status, COALESCE(r.topic_id, j.topic_id) AS topic_id
868
- FROM negotium_cron_runs r
869
- JOIN negotium_cron_jobs j ON j.id = r.job_id
870
- WHERE r.id = ?`).get(runId);
871
- if (!row || row.status !== "pending" && row.status !== "running")
872
- return null;
873
- const durationMs = row?.started_at ? Math.max(0, now.getTime() - Date.parse(row.started_at)) : null;
874
- let successfulRunsSinceRotation = null;
875
- db2.transaction(() => {
876
- const exitCode = result.exitCode ?? { succeeded: 0, skipped: 0, failed: 1, aborted: 130, interrupted: 137 }[result.status];
877
- const updated = db2.query(`UPDATE negotium_cron_runs
878
- SET status = ?, finished_at = ?, duration_ms = ?, output_preview = ?, error = ?, exit_code = ?
879
- WHERE id = ? AND status IN ('pending','running')`).run(result.status, now.toISOString(), durationMs, result.outputPreview?.slice(0, 500) ?? null, result.error ?? null, exitCode, runId);
880
- if (Number(updated.changes) > 0 && result.status === "succeeded") {
881
- successfulRunsSinceRotation = incrementCronTopicSuccess(row.topic_id, now);
882
- }
883
- })();
884
- return successfulRunsSinceRotation;
885
- }
886
- function listCronRuns(jobId, limit = 20) {
887
- ensureCronSchema();
888
- return db2.query("SELECT * FROM negotium_cron_runs WHERE job_id = ? ORDER BY scheduled_at DESC LIMIT ?").all(jobId, limit).map(toRun);
889
- }
890
- function getLastCronRun(jobId) {
891
- return listCronRuns(jobId, 1)[0] ?? null;
892
- }
893
- function countCronRuns(jobId) {
894
- ensureCronSchema();
895
- const row = db2.query("SELECT COUNT(*) AS count FROM negotium_cron_runs WHERE job_id = ?").get(jobId);
896
- return row?.count ?? 0;
897
- }
898
- function recoverPendingCronRuns(limit = 100) {
899
- ensureCronSchema();
900
- const rows = db2.query(`SELECT r.id AS run_id, r.job_id AS run_job_id, r.source AS run_source,
901
- r.scheduled_at AS run_scheduled_at, r.started_at AS run_started_at,
902
- r.finished_at AS run_finished_at, r.status AS run_status,
903
- r.query_id AS run_query_id, r.duration_ms AS run_duration_ms,
904
- r.output_preview AS run_output_preview, r.error AS run_error,
905
- r.exit_code AS run_exit_code,
906
- r.topic_id AS run_topic_id,
907
- j.*
908
- FROM negotium_cron_runs r
909
- JOIN negotium_cron_jobs j ON j.id = r.job_id
910
- WHERE r.status = 'pending'
911
- ORDER BY r.scheduled_at
912
- LIMIT ?`).all(limit);
913
- return rows.map((row) => ({
914
- job: toJob(row),
915
- run: toRun({
916
- id: row.run_id,
917
- job_id: row.run_job_id,
918
- source: row.run_source,
919
- scheduled_at: row.run_scheduled_at,
920
- started_at: row.run_started_at,
921
- finished_at: row.run_finished_at,
922
- status: row.run_status,
923
- query_id: row.run_query_id,
924
- duration_ms: row.run_duration_ms,
925
- output_preview: row.run_output_preview,
926
- error: row.run_error,
927
- exit_code: row.run_exit_code,
928
- topic_id: row.run_topic_id
929
- })
930
- }));
931
- }
932
- function finalizeOrphanedCronRuns(now = new Date) {
933
- ensureCronSchema();
934
- const result = db2.query(`UPDATE negotium_cron_runs
935
- SET status = 'interrupted', finished_at = ?, exit_code = 137,
936
- error = 'node restarted after dispatch; final outcome is unknown'
937
- WHERE status = 'running'`).run(now.toISOString());
938
- return Number(result.changes);
939
- }
940
-
941
- // ../../packages/module-cron/src/background-sessions.ts
942
- function promptForJob(job) {
943
- return job.prompt ?? `Script: ${job.script ?? "unknown"}`;
944
- }
945
- function latestRun(jobs) {
946
- return jobs.flatMap((job) => {
947
- const run = getLastCronRun(job.id);
948
- return run ? [{ job, run }] : [];
949
- }).sort((left, right) => right.run.scheduledAt.localeCompare(left.run.scheduledAt))[0];
950
- }
951
- function listCronBackgroundSessions(userId) {
952
- const jobsByTopic = new Map;
953
- for (const job of listCronJobs()) {
954
- const topic = getTopic(job.topicId);
955
- if (!topic || !isParticipant(topic, userId))
956
- continue;
957
- const jobs = jobsByTopic.get(job.topicId) ?? [];
958
- jobs.push(job);
959
- jobsByTopic.set(job.topicId, jobs);
960
- }
961
- const cronLeases = listRuntimeTurnLeases().filter((lease) => lease.origin.startsWith("cron:"));
962
- return [...jobsByTopic].flatMap(([topicId, jobs]) => {
963
- const topic = getTopic(topicId);
964
- if (!topic)
965
- return [];
966
- const lease = cronLeases.find((candidate) => candidate.topicId === topicId);
967
- const activeJobId = lease?.origin.split(":")[1];
968
- const recent = latestRun(jobs);
969
- const displayJob = jobs.find((job) => job.id === activeJobId) ?? (recent?.job.id ? jobs.find((job) => job.id === recent.job.id) : undefined) ?? jobs[0];
970
- if (!displayJob)
971
- return [];
972
- const queryId = lease?.queryId ?? recent?.run.queryId;
973
- const progress = queryId ? backgroundSessionProgress(topicId, queryId) : undefined;
974
- const enabledJobs = jobs.filter((job) => job.enabled);
975
- const nextJob = [...enabledJobs].sort((left, right) => left.nextRunAt.localeCompare(right.nextRunAt))[0];
976
- const steps = progress?.steps.length ? progress.steps : [
977
- ...recent ? [`Last run: ${recent.job.name} \xB7 ${recent.run.status}`] : ["No runs yet"],
978
- ...nextJob ? [`Next: ${nextJob.name} \xB7 ${nextJob.nextRunAt}`] : []
979
- ];
980
- return [
981
- {
982
- id: `cron:${topicId}`,
983
- kind: "cron",
984
- title: topic.title,
985
- topicId,
986
- startedAt: (lease ? new Date(lease.startedAt).toISOString() : recent?.run.startedAt) ?? displayJob.createdAt,
987
- status: lease ? lease.abortRequested ? "Stopping" : progress?.status ?? "Running" : enabledJobs.length > 0 ? "Scheduled" : "Paused",
988
- active: Boolean(lease),
989
- agent: displayJob.agent ?? topic.agent,
990
- model: displayJob.model ?? topic.effectiveModel ?? topic.defaultModel,
991
- effort: displayJob.effort ?? topic.effectiveEffort ?? topic.defaultEffort,
992
- prompt: promptForJob(displayJob),
993
- promptTitle: `Prompt \xB7 ${displayJob.name}`,
994
- steps
995
- }
996
- ];
997
- });
998
- }
999
- // ../../packages/module-cron/src/host.ts
1000
- var defaultHost = {
1001
- getTopic,
1002
- getRoomQuery,
1003
- abortRoom,
1004
- resolveTopicWorkspaceDir,
1005
- purgeTopicLogs,
1006
- rotateTopicLogs,
1007
- registerRuntimeMcpServer,
1008
- registerBackgroundSessionProvider,
1009
- buildStdioMcpServer
1010
- };
1011
- var activeHost = defaultHost;
1012
- function cronHost() {
1013
- return activeHost;
1014
- }
1015
- function configureCronHost(overrides) {
1016
- const previous = activeHost;
1017
- activeHost = { ...previous, ...overrides };
1018
- let disposed = false;
1019
- return () => {
1020
- if (disposed)
1021
- return;
1022
- disposed = true;
1023
- activeHost = previous;
1024
- };
1025
- }
1026
-
1027
- // ../../packages/module-cron/src/context.ts
1028
- var CRON_CONTEXT_ROTATE_EVERY = 5;
1029
- var CRON_CONTEXT_RETAIN_TURNS = 5;
1030
- async function updateCronJobWithContextReset(id, patch, now = new Date) {
1031
- const previous = getCronJob(id);
1032
- if (!previous)
1033
- return null;
1034
- const resetRequired = cronJobPatchChangesContext(previous, patch);
1035
- const updated = updateCronJob(id, patch, now);
1036
- if (!updated || !resetRequired)
1037
- return updated;
1038
- await resetCronTopicContext(previous.topicId, [previous.ownerUserId]);
1039
- if (updated.topicId !== previous.topicId) {
1040
- await resetCronTopicContext(updated.topicId, [updated.ownerUserId]);
1041
- }
1042
- return getCronJob(id);
1043
- }
1044
- async function resetCronTopicContext(topicId, extraOwnerUserIds = []) {
1045
- const host = cronHost();
1046
- const sessions = listCronTopicSessions(topicId);
1047
- const owners = new Set([
1048
- ...sessions.map((session) => session.ownerUserId),
1049
- ...listCronJobsForTopic(topicId).map((job) => job.ownerUserId),
1050
- ...extraOwnerUserIds
1051
- ]);
1052
- for (const ownerUserId of owners) {
1053
- await host.purgeTopicLogs({
1054
- userId: ownerUserId,
1055
- topicName: cronTopicSessionName(topicId),
1056
- cwd: host.resolveTopicWorkspaceDir(topicId),
1057
- extraSessions: sessions.filter((session) => session.ownerUserId === ownerUserId).map((session) => ({ agent: session.agent, sessionId: session.sessionId }))
1058
- });
1059
- }
1060
- const removed = resetCronTopicSessions(topicId).length;
1061
- resetCronTopicContextState(topicId);
1062
- logger.info({ topicId, sessions: removed, owners: owners.size }, "cron: reset topic context");
1063
- return removed;
1064
- }
1065
- async function rotateCronTopicContext(topicId, retainTurns = CRON_CONTEXT_RETAIN_TURNS) {
1066
- const host = cronHost();
1067
- const sessions = listCronTopicSessions(topicId);
1068
- const jobs = listCronJobsForTopic(topicId);
1069
- const owners = new Set([
1070
- ...sessions.map((session) => session.ownerUserId),
1071
- ...jobs.map((job) => job.ownerUserId)
1072
- ]);
1073
- let totalTurns = 0;
1074
- let retained = 0;
1075
- for (const ownerUserId of owners) {
1076
- const result = await host.rotateTopicLogs({
1077
- userId: ownerUserId,
1078
- topicName: cronTopicSessionName(topicId),
1079
- cwd: host.resolveTopicWorkspaceDir(topicId),
1080
- retainTurns,
1081
- extraSessions: sessions.filter((session) => session.ownerUserId === ownerUserId).map((session) => ({ agent: session.agent, sessionId: session.sessionId }))
1082
- });
1083
- if (!result.rotated) {
1084
- return {
1085
- rotated: false,
1086
- clearedProviderSessions: 0,
1087
- totalTurns: result.totalTurns,
1088
- retainedTurns: result.retainedTurns
1089
- };
1090
- }
1091
- totalTurns = Math.max(totalTurns, result.totalTurns);
1092
- retained = Math.max(retained, result.retainedTurns);
1093
- }
1094
- const clearedProviderSessions = resetCronTopicSessions(topicId).length;
1095
- markCronTopicContextRotated(topicId);
1096
- logger.info({ topicId, clearedProviderSessions, totalTurns, retainedTurns: retained }, "cron: rotated bounded topic context");
1097
- return {
1098
- rotated: true,
1099
- clearedProviderSessions,
1100
- totalTurns,
1101
- retainedTurns: retained
1102
- };
1103
- }
1104
- // ../../packages/module-cron/src/module.ts
1105
- import { existsSync as existsSync2 } from "fs";
1106
- import { fileURLToPath } from "url";
1107
-
1108
- // ../../packages/module-cron/src/scheduler.ts
1109
- var DEFAULT_POLL_INTERVAL_MS = 1000;
1110
- var DEFAULT_RUN_TIMEOUT_MS = 10 * 60000;
1111
- var DEFAULT_QUEUE_TIMEOUT_MS = 5 * 60000;
1112
-
1113
- class CronScheduler {
1114
- #dispatch;
1115
- #bus;
1116
- #pollIntervalMs;
1117
- #runTimeoutMs;
1118
- #queueTimeoutMs;
1119
- #now;
1120
- #activeByTopic = new Map;
1121
- #activeByQuery = new Map;
1122
- #queuedByTopic = new Map;
1123
- #maintenanceByTopic = new Set;
1124
- #rotationBypassOnce = new Set;
1125
- #timer;
1126
- #unsubscribe;
1127
- #ticking = false;
1128
- #stopped = false;
1129
- constructor(options) {
1130
- this.#dispatch = options.dispatch;
1131
- this.#bus = options.bus ?? runtimeBus();
1132
- this.#pollIntervalMs = Math.max(250, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
1133
- this.#runTimeoutMs = Math.max(1000, options.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS);
1134
- this.#queueTimeoutMs = Math.max(1000, options.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS);
1135
- this.#now = options.now ?? (() => new Date);
1136
- }
1137
- start() {
1138
- if (this.#timer)
1139
- return () => this.stop();
1140
- this.#stopped = false;
1141
- const orphaned = finalizeOrphanedCronRuns(this.#now());
1142
- if (orphaned > 0)
1143
- logger.warn({ orphaned }, "cron: finalized interrupted runs on startup");
1144
- this.#unsubscribe = this.#bus.subscribe((event) => {
1145
- if (event.type !== "message")
1146
- return;
1147
- const message = event.payload;
1148
- if (!message?.queryId || message.authorId !== "ai")
1149
- return;
1150
- const active = this.#activeByQuery.get(message.queryId);
1151
- if (active)
1152
- active.outputPreview = message.text.slice(0, 500);
1153
- });
1154
- this.#timer = setInterval(() => void this.tick(), this.#pollIntervalMs);
1155
- this.#timer.unref?.();
1156
- const recovered = recoverPendingCronRuns();
1157
- for (const pending of recovered)
1158
- this.#enqueueOrStart(pending.job, pending.run);
1159
- if (recovered.length > 0) {
1160
- logger.info({ recovered: recovered.length }, "cron: recovered pending pre-dispatch runs");
1161
- }
1162
- this.tick();
1163
- return () => this.stop();
1164
- }
1165
- stop() {
1166
- const host = cronHost();
1167
- if (this.#timer)
1168
- clearInterval(this.#timer);
1169
- this.#timer = undefined;
1170
- this.#stopped = true;
1171
- this.#unsubscribe?.();
1172
- this.#unsubscribe = undefined;
1173
- for (const active of [...this.#activeByTopic.values()]) {
1174
- active.abortController.abort();
1175
- active.cancelDeferred?.();
1176
- if (active.queryId && host.getRoomQuery(active.job.topicId)?.queryId === active.queryId) {
1177
- host.abortRoom(active.job.topicId);
1178
- }
1179
- this.#finish(active, "aborted", "scheduler stopped before run completed");
1180
- }
1181
- for (const queued of this.#queuedByTopic.values()) {
1182
- for (const entry of queued) {
1183
- finishCronRun(entry.run.id, {
1184
- status: "aborted",
1185
- error: "scheduler stopped before queued run dispatched"
1186
- });
1187
- }
1188
- }
1189
- this.#queuedByTopic.clear();
1190
- this.#activeByTopic.clear();
1191
- this.#activeByQuery.clear();
1192
- this.#rotationBypassOnce.clear();
1193
- }
1194
- async tick() {
1195
- if (this.#ticking || this.#stopped)
1196
- return;
1197
- this.#ticking = true;
1198
- try {
1199
- for (const jobId of claimCronCancellations())
1200
- this.#cancelJob(jobId);
1201
- for (const claimed of claimCronRuns(this.#now())) {
1202
- this.#enqueueOrStart(claimed.job, claimed.run);
1203
- }
1204
- } catch (error) {
1205
- logger.warn({ err: error }, "cron: scheduler tick failed");
1206
- } finally {
1207
- this.#ticking = false;
1208
- }
1209
- }
1210
- #enqueueOrStart(job, run) {
1211
- const topic = cronHost().getTopic(job.topicId);
1212
- if (!topic?.agent) {
1213
- setCronJobEnabled(job.id, false, this.#now());
1214
- finishCronRun(run.id, {
1215
- status: "failed",
1216
- error: "target topic is missing or has no agent; job disabled"
1217
- });
1218
- return;
1219
- }
1220
- if (!topic.participants.some((participant) => participant.userId === job.ownerUserId)) {
1221
- setCronJobEnabled(job.id, false, this.#now());
1222
- finishCronRun(run.id, {
1223
- status: "failed",
1224
- error: "job owner is no longer a topic participant; job disabled"
1225
- });
1226
- return;
1227
- }
1228
- if (this.#activeByTopic.has(job.topicId) || this.#maintenanceByTopic.has(job.topicId)) {
1229
- const queue = this.#queuedByTopic.get(job.topicId) ?? [];
1230
- queue.push({ job, run, queuedAt: this.#now().getTime() });
1231
- this.#queuedByTopic.set(job.topicId, queue);
1232
- return;
1233
- }
1234
- const rotationDue = (getCronTopicContext(job.topicId)?.successfulRunsSinceRotation ?? 0) >= CRON_CONTEXT_ROTATE_EVERY;
1235
- if (rotationDue && !this.#rotationBypassOnce.delete(job.topicId)) {
1236
- const queue = this.#queuedByTopic.get(job.topicId) ?? [];
1237
- queue.push({ job, run, queuedAt: this.#now().getTime() });
1238
- this.#queuedByTopic.set(job.topicId, queue);
1239
- this.#beginContextRotation(job.topicId);
1240
- return;
1241
- }
1242
- this.#start(job, run, job.agent ?? topic.agent);
1243
- }
1244
- #start(job, run, agent) {
1245
- const storedSession = getCronTopicSession(job.topicId, agent);
1246
- if (storedSession && storedSession.ownerUserId !== job.ownerUserId) {
1247
- setCronJobEnabled(job.id, false, this.#now());
1248
- finishCronRun(run.id, {
1249
- status: "failed",
1250
- error: "topic Cron context belongs to another owner; job disabled"
1251
- });
1252
- this.#drainTopic(job.topicId);
1253
- return;
1254
- }
1255
- const active = { job, run, agent, abortController: new AbortController };
1256
- this.#activeByTopic.set(job.topicId, active);
1257
- const hooks = {
1258
- onDispatched: (queryId) => this.#markDispatched(active, queryId),
1259
- onSessionId: (sessionId) => {
1260
- if (this.#isActive(active)) {
1261
- setCronTopicSessionIfJobUpdatedAt(job.id, job.updatedAt, job.topicId, agent, job.ownerUserId, sessionId, this.#now());
1262
- }
1263
- },
1264
- onSessionReset: () => {
1265
- if (this.#isActive(active) && getCronJob(job.id)?.updatedAt === job.updatedAt) {
1266
- clearCronTopicSession(job.topicId, agent);
1267
- }
1268
- },
1269
- onSettled: (result) => this.#settle(active, result)
1270
- };
1271
- const context = {
1272
- agent,
1273
- sessionId: storedSession?.sessionId,
1274
- sessionName: cronTopicSessionName(job.topicId),
1275
- signal: active.abortController.signal
1276
- };
1277
- Promise.resolve().then(() => this.#dispatch(job, run, hooks, context)).then((result) => this.#handleDispatchResult(active, result)).catch((error) => {
1278
- this.#finish(active, "failed", error instanceof Error ? error.message : String(error));
1279
- });
1280
- }
1281
- #handleDispatchResult(active, result) {
1282
- if (!this.#isActive(active))
1283
- return;
1284
- if (typeof result === "string") {
1285
- if (active.queryId !== result)
1286
- this.#markDispatched(active, result);
1287
- return;
1288
- }
1289
- if (result === null) {
1290
- this.#finish(active, "failed", "dispatch returned no query and no deferred handle");
1291
- return;
1292
- }
1293
- if (result.status === "skipped") {
1294
- this.#finish(active, "skipped", result.reason);
1295
- return;
1296
- }
1297
- active.cancelDeferred = result.cancel;
1298
- if (result.status === "dispatched") {
1299
- if (result.queryId && active.queryId !== result.queryId) {
1300
- this.#markDispatched(active, result.queryId);
1301
- }
1302
- return;
1303
- }
1304
- active.deferredTimeout = setTimeout(() => {
1305
- if (!this.#isActive(active) || active.queryId)
1306
- return;
1307
- const cancelled = active.cancelDeferred?.() ?? false;
1308
- this.#finish(active, cancelled ? "skipped" : "failed", cancelled ? `topic remained busy for more than ${this.#queueTimeoutMs}ms` : "deferred run disappeared before dispatch");
1309
- }, this.#queueTimeoutMs);
1310
- active.deferredTimeout.unref?.();
1311
- }
1312
- #markDispatched(active, queryId) {
1313
- if (!this.#isActive(active))
1314
- return;
1315
- if (active.queryId)
1316
- this.#activeByQuery.delete(active.queryId);
1317
- if (active.timeout)
1318
- clearTimeout(active.timeout);
1319
- if (active.deferredTimeout)
1320
- clearTimeout(active.deferredTimeout);
1321
- active.deferredTimeout = undefined;
1322
- active.queryId = queryId;
1323
- this.#activeByQuery.set(queryId, active);
1324
- markCronRunStarted(active.run.id, queryId, this.#now());
1325
- active.timeout = setTimeout(() => {
1326
- const host = cronHost();
1327
- const room = host.getRoomQuery(active.job.topicId);
1328
- if (room?.queryId === active.queryId)
1329
- host.abortRoom(active.job.topicId);
1330
- else
1331
- active.cancelDeferred?.();
1332
- this.#finish(active, "failed", `run exceeded ${this.#runTimeoutMs}ms`);
1333
- }, this.#runTimeoutMs);
1334
- active.timeout.unref?.();
1335
- }
1336
- #settle(active, result) {
1337
- if (!this.#isActive(active))
1338
- return;
1339
- if (result.queryId && active.queryId && result.queryId !== active.queryId)
1340
- return;
1341
- if (result.kind === "completed")
1342
- this.#finish(active, "succeeded");
1343
- else if (result.kind === "aborted")
1344
- this.#finish(active, "aborted", result.error);
1345
- else
1346
- this.#finish(active, "failed", result.error ?? "agent turn failed");
1347
- }
1348
- #finish(active, status, error) {
1349
- if (!this.#isActive(active))
1350
- return;
1351
- active.abortController.abort();
1352
- if (active.timeout)
1353
- clearTimeout(active.timeout);
1354
- if (active.deferredTimeout)
1355
- clearTimeout(active.deferredTimeout);
1356
- if (active.queryId)
1357
- this.#activeByQuery.delete(active.queryId);
1358
- this.#activeByTopic.delete(active.job.topicId);
1359
- const successfulRunsSinceRotation = finishCronRun(active.run.id, { status, outputPreview: active.outputPreview, error }, this.#now());
1360
- if (status === "succeeded" && successfulRunsSinceRotation !== null && successfulRunsSinceRotation >= CRON_CONTEXT_ROTATE_EVERY) {
1361
- this.#beginContextRotation(active.job.topicId);
1362
- return;
1363
- }
1364
- if (!this.#stopped)
1365
- queueMicrotask(() => this.#drainTopic(active.job.topicId));
1366
- }
1367
- #beginContextRotation(topicId) {
1368
- if (this.#stopped || this.#maintenanceByTopic.has(topicId))
1369
- return;
1370
- this.#maintenanceByTopic.add(topicId);
1371
- rotateCronTopicContext(topicId).then((result) => {
1372
- if (!result.rotated) {
1373
- this.#rotationBypassOnce.add(topicId);
1374
- logger.warn({ topicId }, "cron: context rotation deferred after cleanup failure");
1375
- }
1376
- }).catch((error) => {
1377
- this.#rotationBypassOnce.add(topicId);
1378
- logger.warn({ err: error, topicId }, "cron: context rotation failed");
1379
- }).finally(() => {
1380
- this.#maintenanceByTopic.delete(topicId);
1381
- if (!this.#stopped)
1382
- this.#drainTopic(topicId);
1383
- });
1384
- }
1385
- #drainTopic(topicId) {
1386
- if (this.#stopped || this.#activeByTopic.has(topicId) || this.#maintenanceByTopic.has(topicId))
1387
- return;
1388
- const queue = this.#queuedByTopic.get(topicId);
1389
- while (queue?.length) {
1390
- const next = queue.shift();
1391
- if (this.#now().getTime() - next.queuedAt > this.#queueTimeoutMs) {
1392
- finishCronRun(next.run.id, {
1393
- status: "skipped",
1394
- error: `topic Cron queue wait exceeded ${this.#queueTimeoutMs}ms`
1395
- });
1396
- continue;
1397
- }
1398
- const currentJob = getCronJob(next.job.id);
1399
- if (!currentJob)
1400
- continue;
1401
- if (queue.length === 0)
1402
- this.#queuedByTopic.delete(topicId);
1403
- this.#enqueueOrStart(currentJob, next.run);
1404
- return;
1405
- }
1406
- this.#queuedByTopic.delete(topicId);
1407
- }
1408
- #cancelJob(jobId) {
1409
- for (const active of [...this.#activeByTopic.values()]) {
1410
- if (active.job.id !== jobId)
1411
- continue;
1412
- active.abortController.abort();
1413
- active.cancelDeferred?.();
1414
- const host = cronHost();
1415
- if (active.queryId && host.getRoomQuery(active.job.topicId)?.queryId === active.queryId) {
1416
- host.abortRoom(active.job.topicId);
1417
- }
1418
- this.#finish(active, "aborted", "run cancelled by cron_kill");
1419
- }
1420
- for (const [topicId, queue] of this.#queuedByTopic) {
1421
- const kept = [];
1422
- for (const queued of queue) {
1423
- if (queued.job.id === jobId) {
1424
- finishCronRun(queued.run.id, {
1425
- status: "aborted",
1426
- error: "queued run cancelled by cron_kill"
1427
- });
1428
- } else {
1429
- kept.push(queued);
1430
- }
1431
- }
1432
- if (kept.length > 0)
1433
- this.#queuedByTopic.set(topicId, kept);
1434
- else
1435
- this.#queuedByTopic.delete(topicId);
1436
- }
1437
- }
1438
- #isActive(active) {
1439
- return this.#activeByTopic.get(active.job.topicId) === active;
1440
- }
1441
- }
1442
-
1443
- // ../../packages/module-cron/src/module.ts
1444
- var SOURCE_MCP_SERVER_FILE = fileURLToPath(new URL("./mcp-server.ts", import.meta.url));
1445
- var PACKAGED_MCP_SERVER_FILE = fileURLToPath(new URL("./runtime/cron/mcp-server.ts", import.meta.url));
1446
- function envMs(name, fallback) {
1447
- const parsed = Number.parseInt(process.env[name] ?? "", 10);
1448
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
1449
- }
1450
- function createCronModule(options = {}) {
1451
- return {
1452
- name: "cron",
1453
- singleton: true,
1454
- capabilities: ["scheduler.cron.v1", "scheduler.cron.v2"],
1455
- start(context) {
1456
- const restoreDatabase = options.database ? configureCronDatabase(options.database) : () => {};
1457
- const restoreHost = configureCronHost(options.host ?? {});
1458
- const host = cronHost();
1459
- ensureCronSchema();
1460
- const cleanupTasks = new Set;
1461
- const trackCleanup = (promise) => {
1462
- cleanupTasks.add(promise);
1463
- promise.then(() => cleanupTasks.delete(promise), (error) => {
1464
- cleanupTasks.delete(promise);
1465
- logger.warn({ err: error }, "cron: topic context cleanup failed");
1466
- });
1467
- };
1468
- for (const topicId of new Set(listOrphanedCronTopicSessions().map((entry) => entry.topicId))) {
1469
- trackCleanup(resetCronTopicContext(topicId));
1470
- }
1471
- const unsubscribeTopicCleanup = context.bus.subscribe((event) => {
1472
- if (event.type === "topic-deleted")
1473
- trackCleanup(resetCronTopicContext(event.topicId));
1474
- });
1475
- const mcpServerFile = options.mcpServerFile ?? (existsSync2(SOURCE_MCP_SERVER_FILE) ? SOURCE_MCP_SERVER_FILE : PACKAGED_MCP_SERVER_FILE);
1476
- const unregisterMcp = host.registerRuntimeMcpServer("cron-manager", {
1477
- scopes: ["forum", "manager"],
1478
- forumRequired: true,
1479
- build({ userId, session, topicId, agent }) {
1480
- const args = [`--user-id=${userId}`, `--topic=${session}`];
1481
- if (topicId)
1482
- args.push(`--topic-id=${topicId}`);
1483
- if (host.authorize?.(userId, "cron:admin", { type: "cron" })) {
1484
- args.push("--authorized=true");
1485
- }
1486
- return host.buildStdioMcpServer(agent, mcpServerFile, args);
1487
- }
1488
- });
1489
- const unregisterBackgroundSessions = host.registerBackgroundSessionProvider(listCronBackgroundSessions);
1490
- const schedulerOptions = {
1491
- bus: context.bus,
1492
- pollIntervalMs: options.pollIntervalMs ?? envMs("NEGOTIUM_CRON_POLL_INTERVAL_MS", 1000),
1493
- runTimeoutMs: options.runTimeoutMs ?? envMs("NEGOTIUM_CRON_RUN_TIMEOUT_MS", 10 * 60000),
1494
- queueTimeoutMs: options.queueTimeoutMs ?? envMs("NEGOTIUM_CRON_QUEUE_TIMEOUT_MS", 5 * 60000),
1495
- dispatch: options.host?.dispatch ?? (async (job, run, hooks, execution) => {
1496
- const taskPrompt = job.prompt ?? await runCronPromptScript({
1497
- script: job.script,
1498
- cwd: host.resolveTopicWorkspaceDir(job.topicId),
1499
- jobId: job.id,
1500
- topicId: job.topicId,
1501
- timeoutMs: options.scriptTimeoutMs ?? envMs("NEGOTIUM_CRON_SCRIPT_TIMEOUT_MS", 10 * 60000),
1502
- signal: execution.signal
1503
- });
1504
- if (!taskPrompt.trim()) {
1505
- return { status: "skipped", reason: "prompt script produced no output" };
1506
- }
1507
- if (!host.getTopic(job.topicId)?.agent)
1508
- throw new Error("target topic was deleted during run");
1509
- const prompt = [
1510
- `<scheduled-task name="${job.name}" run-id="${run.id}">`,
1511
- "This is a background scheduled run. Do not ask interactive questions.",
1512
- "All scheduled tasks in this topic share one Cron conversation.",
1513
- "Use relevant conclusions and state from previous scheduled runs before acting.",
1514
- "Complete the task and report only meaningful results in the user's language.",
1515
- taskPrompt,
1516
- "</scheduled-task>"
1517
- ].join(`
1518
- `);
1519
- const requestId = `cron:${run.id}`;
1520
- const queryId = triggerTopicAiTurn(job.topicId, job.ownerUserId, prompt, execution.agent, {
1521
- origin: `cron:${job.id}:${run.id}`,
1522
- requestId,
1523
- hideInjectMessage: true,
1524
- modelOverride: job.model,
1525
- effortOverride: job.effort,
1526
- sessionId: execution.sessionId ?? null,
1527
- sessionName: execution.sessionName,
1528
- sessionType: "cron",
1529
- onDispatched: hooks.onDispatched,
1530
- onSessionId: hooks.onSessionId,
1531
- onSessionReset: hooks.onSessionReset,
1532
- bridgeSessionFromHistory: true,
1533
- onSettled: hooks.onSettled
1534
- });
1535
- return {
1536
- status: queryId ? "dispatched" : "deferred",
1537
- requestId,
1538
- ...queryId ? { queryId } : {},
1539
- cancel: () => cancelDeferredInject(job.topicId, requestId)
1540
- };
1541
- })
1542
- };
1543
- const scheduler = new CronScheduler(schedulerOptions);
1544
- scheduler.start();
1545
- return {
1546
- async stop() {
1547
- scheduler.stop();
1548
- unsubscribeTopicCleanup();
1549
- unregisterBackgroundSessions();
1550
- unregisterMcp();
1551
- await Promise.allSettled(cleanupTasks);
1552
- restoreHost();
1553
- restoreDatabase();
1554
- }
1555
- };
1556
- }
1557
- };
1558
- }
1559
- // ../../packages/module-cron/src/summarize.ts
1560
- var DEFAULT_URL = "https://api.deepseek.com/v1/chat/completions";
1561
- var DEFAULT_MODEL = "deepseek-chat";
1562
- var MAX_SUMMARY_LENGTH = 60;
1563
- var MAX_CONCURRENT_SUMMARIES = 3;
1564
- function cleanCronPromptSummary(raw) {
1565
- let text = raw.replace(/\s+/g, " ").trim();
1566
- text = text.replace(/^["'\u201C\u201D]+|["'\u201C\u201D]+$/g, "").trim();
1567
- text = text.replace(/^(task|summary|label|\uC694\uC57D|\uC791\uC5C5)\s*[:\uFF1A]\s*/i, "").trim();
1568
- text = text.replace(/^["'\u201C\u201D]+|["'\u201C\u201D]+$/g, "").trim();
1569
- text = text.replace(/[.\u3002]+$/, "").trim();
1570
- if (!text)
1571
- return null;
1572
- return text.length > MAX_SUMMARY_LENGTH ? `${text.slice(0, MAX_SUMMARY_LENGTH - 3)}...` : text;
1573
- }
1574
- async function summarizeCronPrompt(prompt) {
1575
- const clean = prompt.trim();
1576
- const apiKey = process.env.NEGOTIUM_CRON_SUMMARY_API_KEY ?? process.env.DEEPSEEK_API_KEY;
1577
- if (!clean || !apiKey)
1578
- return null;
1579
- const controller = new AbortController;
1580
- const timeoutMs = Number.parseInt(process.env.NEGOTIUM_CRON_SUMMARY_TIMEOUT_MS ?? "8000", 10);
1581
- const timer = setTimeout(() => controller.abort(), Number.isFinite(timeoutMs) ? timeoutMs : 8000);
1582
- try {
1583
- const response = await fetch(process.env.NEGOTIUM_CRON_SUMMARY_URL ?? DEFAULT_URL, {
1584
- method: "POST",
1585
- signal: controller.signal,
1586
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1587
- body: JSON.stringify({
1588
- model: process.env.NEGOTIUM_CRON_SUMMARY_MODEL ?? DEFAULT_MODEL,
1589
- messages: [
1590
- {
1591
- role: "system",
1592
- content: "Label the scheduled task in the instruction's language using at most 8 words. " + "Describe the outcome only. Return no quotes, prefix, or trailing period."
1593
- },
1594
- { role: "user", content: clean.slice(0, 4000) }
1595
- ],
1596
- max_tokens: 512,
1597
- temperature: 0.2,
1598
- stream: false
1599
- })
1600
- });
1601
- if (!response.ok) {
1602
- logger.warn({ status: response.status }, "cron: prompt summary request failed");
1603
- return null;
1604
- }
1605
- const body = await response.json();
1606
- return cleanCronPromptSummary(body.choices?.[0]?.message?.content ?? "");
1607
- } catch (error) {
1608
- logger.warn({ err: error }, "cron: prompt summary request errored");
1609
- return null;
1610
- } finally {
1611
- clearTimeout(timer);
1612
- }
1613
- }
1614
- var generations = new Map;
1615
- var pending = [];
1616
- var active = 0;
1617
- var nextGeneration = 0;
1618
- function drain() {
1619
- while (active < MAX_CONCURRENT_SUMMARIES) {
1620
- const next = pending.shift();
1621
- if (!next)
1622
- return;
1623
- if (generations.get(next.jobId) !== next.generation)
1624
- continue;
1625
- active += 1;
1626
- next.summarize(next.prompt).then((summary) => {
1627
- if (summary && generations.get(next.jobId) === next.generation) {
1628
- updateCronJobSummaryIfPromptMatches(next.jobId, next.prompt, summary);
1629
- }
1630
- }).catch((error) => logger.warn({ err: error, jobId: next.jobId }, "cron: summary persist failed")).finally(() => {
1631
- active -= 1;
1632
- if (generations.get(next.jobId) === next.generation)
1633
- generations.delete(next.jobId);
1634
- drain();
1635
- });
1636
- }
1637
- }
1638
- function queueCronPromptSummary(jobId, prompt, summarize = summarizeCronPrompt) {
1639
- const generation = ++nextGeneration;
1640
- generations.set(jobId, generation);
1641
- for (let index = pending.length - 1;index >= 0; index -= 1) {
1642
- if (pending[index]?.jobId === jobId)
1643
- pending.splice(index, 1);
1644
- }
1645
- pending.push({ jobId, prompt, generation, summarize });
1646
- drain();
1647
- }
1648
- export {
1649
- validateCronScriptName,
1650
- validateCronExpression,
1651
- updateCronJobWithContextReset,
1652
- updateCronJobSummaryIfPromptMatches,
1653
- updateCronJob,
1654
- summarizeCronPrompt,
1655
- setCronTopicSessionIfJobUpdatedAt,
1656
- setCronTopicSession,
1657
- setCronJobSessionId,
1658
- setCronJobEnabled,
1659
- runCronPromptScript,
1660
- rotateCronTopicContext,
1661
- resolveCronScriptPath,
1662
- resetCronTopicSessions,
1663
- resetCronTopicContextState,
1664
- resetCronTopicContext,
1665
- requestCronRun,
1666
- requestCronCancel,
1667
- recoverPendingCronRuns,
1668
- queueCronPromptSummary,
1669
- parseCronExpression,
1670
- normalizeCronTimezone,
1671
- markCronTopicContextRotated,
1672
- markCronRunStarted,
1673
- listOrphanedCronTopicSessions,
1674
- listEnabledCronJobs,
1675
- listCronTopicSessions,
1676
- listCronScripts,
1677
- listCronRuns,
1678
- listCronJobsForTopicOwner,
1679
- listCronJobsForTopic,
1680
- listCronJobs,
1681
- listCronBackgroundSessions,
1682
- getLastCronRun,
1683
- getCronTopicSession,
1684
- getCronTopicContext,
1685
- getCronJobByOwnerAndName,
1686
- getCronJob,
1687
- finishCronRun,
1688
- finalizeOrphanedCronRuns,
1689
- ensureCronSchema,
1690
- deleteCronJob,
1691
- cronTopicSessionName,
1692
- cronScriptExists,
1693
- cronMatchesDate,
1694
- cronJobPatchChangesContext,
1695
- createCronModule,
1696
- createCronJob,
1697
- countCronRuns,
1698
- configureCronHost,
1699
- configureCronDatabase,
1700
- computeNextCronRun,
1701
- clearCronTopicSession,
1702
- cleanCronPromptSummary,
1703
- claimCronRuns,
1704
- claimCronCancellations,
1705
- CronScheduler,
1706
- CRON_JOBS_DIR,
1707
- CRON_CONTEXT_ROTATE_EVERY,
1708
- CRON_CONTEXT_RETAIN_TURNS
1709
- };
1710
- export { CRON_JOBS_DIR, cronScriptExists, createCronJob, listCronJobs, getCronJob, getCronJobByOwnerAndName, setCronJobEnabled, deleteCronJob, requestCronRun, requestCronCancel, listCronRuns, updateCronJobWithContextReset, resetCronTopicContext, queueCronPromptSummary };
1711
-
1712
- //# debugId=38E78E450D25029964756E2164756E21