@capybara-hq/kernel 0.1.0-rc.23 → 0.1.0-rc.25

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 (293) hide show
  1. package/bootstrap.json +25 -25
  2. package/dist/admin/admin.module.d.ts +2 -1
  3. package/dist/admin/admin.module.js +9 -3
  4. package/dist/admin/health.service.d.ts +2 -2
  5. package/dist/admin/health.service.js +6 -2
  6. package/dist/admin/outbox-dead.controller.d.ts +23 -0
  7. package/dist/admin/outbox-dead.controller.js +62 -0
  8. package/dist/admin/outbox-dead.schemas.d.ts +8 -0
  9. package/dist/admin/outbox-dead.schemas.js +18 -0
  10. package/dist/admin/outbox-dead.service.d.ts +66 -0
  11. package/dist/admin/outbox-dead.service.js +179 -0
  12. package/dist/admin/overview.service.d.ts +40 -5
  13. package/dist/admin/overview.service.js +41 -7
  14. package/dist/agents/agents.module.js +3 -0
  15. package/dist/agents/agents.service.d.ts +6 -1
  16. package/dist/agents/agents.service.js +11 -2
  17. package/dist/agents/from-template.js +1 -1
  18. package/dist/background/finish.d.ts +5 -2
  19. package/dist/background/finish.js +14 -3
  20. package/dist/background/launch.d.ts +2 -1
  21. package/dist/background/launch.js +5 -2
  22. package/dist/background/store.d.ts +8 -0
  23. package/dist/background/store.js +24 -0
  24. package/dist/catalog/admin-catalog.controller.d.ts +21 -2
  25. package/dist/catalog/admin-catalog.controller.js +45 -6
  26. package/dist/catalog/admin-catalog.schemas.d.ts +5 -0
  27. package/dist/catalog/admin-catalog.schemas.js +8 -0
  28. package/dist/catalog/catalog-import.module.js +0 -4
  29. package/dist/catalog/catalog-scope.service.d.ts +63 -26
  30. package/dist/catalog/catalog-scope.service.js +146 -71
  31. package/dist/catalog/catalog.module.d.ts +2 -1
  32. package/dist/catalog/catalog.module.js +9 -4
  33. package/dist/catalog/catalog.service.d.ts +11 -4
  34. package/dist/catalog/catalog.service.js +21 -5
  35. package/dist/catalog/cli.d.ts +4 -3
  36. package/dist/catalog/cli.js +8 -13
  37. package/dist/catalog/job-ids.d.ts +8 -20
  38. package/dist/catalog/job-ids.js +11 -16
  39. package/dist/catalog/marketplaces.service.d.ts +1 -1
  40. package/dist/catalog/org-plugin-uploads.d.ts +11 -4
  41. package/dist/catalog/org-plugin-uploads.js +21 -6
  42. package/dist/catalog/owned-items.d.ts +6 -13
  43. package/dist/catalog/owned-items.js +11 -29
  44. package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
  45. package/dist/catalog/owned-plugin-cleanup.js +12 -66
  46. package/dist/catalog/platform-components.d.ts +56 -34
  47. package/dist/catalog/platform-components.js +68 -78
  48. package/dist/catalog/platform-import-update.d.ts +50 -0
  49. package/dist/catalog/platform-import-update.js +116 -0
  50. package/dist/catalog/plugin-import.d.ts +64 -26
  51. package/dist/catalog/plugin-import.js +113 -77
  52. package/dist/catalog/plugin-upgrade.d.ts +22 -4
  53. package/dist/catalog/plugin-upgrade.js +26 -19
  54. package/dist/catalog/scope-jobs.d.ts +59 -0
  55. package/dist/catalog/scope-jobs.js +65 -0
  56. package/dist/catalog/scope-sync-workers.d.ts +22 -0
  57. package/dist/catalog/scope-sync-workers.js +58 -2
  58. package/dist/catalog/scope-sync.d.ts +40 -65
  59. package/dist/catalog/scope-sync.js +114 -122
  60. package/dist/channels/channels.module.d.ts +5 -2
  61. package/dist/channels/channels.module.js +9 -2
  62. package/dist/channels/deliver-job.d.ts +25 -0
  63. package/dist/channels/deliver-job.js +25 -0
  64. package/dist/channels/inbound.d.ts +1 -1
  65. package/dist/channels/inbound.js +4 -17
  66. package/dist/channels/jobs.d.ts +28 -18
  67. package/dist/channels/jobs.js +63 -26
  68. package/dist/channels/threads.d.ts +4 -3
  69. package/dist/channels/threads.js +4 -3
  70. package/dist/config/schema.d.ts +0 -3
  71. package/dist/config/schema.js +11 -20
  72. package/dist/conversations/agent-tree.d.ts +16 -0
  73. package/dist/conversations/agent-tree.js +36 -0
  74. package/dist/conversations/conversations.controller.js +4 -2
  75. package/dist/conversations/conversations.service.d.ts +61 -19
  76. package/dist/conversations/conversations.service.js +69 -43
  77. package/dist/conversations/event-log.d.ts +7 -10
  78. package/dist/conversations/event-log.js +24 -15
  79. package/dist/conversations/events.d.ts +21 -0
  80. package/dist/conversations/events.js +26 -2
  81. package/dist/conversations/queue.service.d.ts +8 -3
  82. package/dist/conversations/queue.service.js +37 -13
  83. package/dist/conversations/source.d.ts +10 -0
  84. package/dist/conversations/source.js +17 -0
  85. package/dist/conversations/titles.d.ts +21 -8
  86. package/dist/conversations/titles.js +42 -15
  87. package/dist/conversations/tree.d.ts +10 -0
  88. package/dist/conversations/tree.js +35 -0
  89. package/dist/db/kernel-schema.d.ts +35 -22
  90. package/dist/devices/devices.service.d.ts +23 -11
  91. package/dist/devices/devices.service.js +34 -26
  92. package/dist/errors.d.ts +11 -0
  93. package/dist/errors.js +11 -0
  94. package/dist/evals/run-replay.d.ts +13 -2
  95. package/dist/evals/run-replay.js +42 -9
  96. package/dist/evals/snapshot.d.ts +7 -1
  97. package/dist/evals/snapshot.js +2 -1
  98. package/dist/extensions/extensions.module.js +3 -0
  99. package/dist/extensions/hooks.d.ts +22 -3
  100. package/dist/extensions/hooks.js +15 -1
  101. package/dist/extensions/lifecycle-hooks.d.ts +99 -0
  102. package/dist/extensions/lifecycle-hooks.js +273 -0
  103. package/dist/extensions/lifecycle.d.ts +6 -1
  104. package/dist/extensions/lifecycle.js +9 -2
  105. package/dist/extensions/org-config.d.ts +13 -1
  106. package/dist/extensions/org-config.js +19 -2
  107. package/dist/extensions/permission-view.d.ts +9 -5
  108. package/dist/extensions/permission-view.js +9 -4
  109. package/dist/extensions/point-defaults.d.ts +28 -1
  110. package/dist/extensions/point-defaults.js +44 -13
  111. package/dist/extensions/usage-subscribers.d.ts +43 -0
  112. package/dist/extensions/usage-subscribers.js +108 -0
  113. package/dist/http/api.module.js +2 -0
  114. package/dist/inbox/inbox.service.d.ts +6 -20
  115. package/dist/inbox/inbox.service.js +0 -3
  116. package/dist/inbox/org-notices.d.ts +1 -1
  117. package/dist/inbox/platform-inbox.service.d.ts +3 -32
  118. package/dist/inbox/platform-inbox.service.js +5 -54
  119. package/dist/index.d.ts +1 -0
  120. package/dist/index.js +1 -0
  121. package/dist/infra/cli/kernel-cli.module.js +5 -0
  122. package/dist/infra/events/kernel-events.d.ts +9 -35
  123. package/dist/infra/outbox/outbox-relay.module.d.ts +6 -0
  124. package/dist/infra/outbox/outbox-relay.module.js +18 -0
  125. package/dist/infra/outbox/outbox.dispatcher.d.ts +103 -0
  126. package/dist/infra/outbox/outbox.dispatcher.js +310 -0
  127. package/dist/infra/outbox/outbox.module.d.ts +24 -0
  128. package/dist/infra/outbox/outbox.module.js +50 -0
  129. package/dist/infra/outbox/outbox.relay.d.ts +31 -0
  130. package/dist/infra/outbox/outbox.relay.js +76 -0
  131. package/dist/infra/outbox/outbox.service.d.ts +51 -0
  132. package/dist/infra/outbox/outbox.service.js +131 -0
  133. package/dist/infra/outbox/registry.d.ts +244 -0
  134. package/dist/infra/outbox/registry.js +234 -0
  135. package/dist/infra/queue/queue-names.d.ts +8 -0
  136. package/dist/infra/queue/queue-names.js +17 -0
  137. package/dist/kernel.module.js +3 -0
  138. package/dist/local-devices/device-policy.service.d.ts +4 -0
  139. package/dist/local-devices/device-policy.service.js +61 -3
  140. package/dist/local-devices/machines.d.ts +13 -5
  141. package/dist/local-devices/machines.js +45 -7
  142. package/dist/local-devices/policy-summary.d.ts +44 -1
  143. package/dist/local-devices/policy-summary.js +69 -0
  144. package/dist/models/embeddings.d.ts +9 -9
  145. package/dist/models/embeddings.js +11 -18
  146. package/dist/models/models.module.js +3 -0
  147. package/dist/orgs/directory-sync.js +1 -1
  148. package/dist/orgs/members.service.js +1 -1
  149. package/dist/orgs/offboarding.d.ts +50 -57
  150. package/dist/orgs/offboarding.js +117 -162
  151. package/dist/orgs/orgs.module.js +3 -0
  152. package/dist/orgs/orgs.service.d.ts +11 -5
  153. package/dist/orgs/orgs.service.js +22 -6
  154. package/dist/overrides.d.ts +23 -3
  155. package/dist/overrides.js +12 -2
  156. package/dist/platform/exclusions.js +5 -0
  157. package/dist/platform/ops/admin.ops.js +18 -2
  158. package/dist/platform/ops/catalog-admin.ops.d.ts +3 -1
  159. package/dist/platform/ops/catalog-admin.ops.js +32 -4
  160. package/dist/platform/ops/inbox.ops.js +4 -20
  161. package/dist/platform/ops/outbox.ops.d.ts +21 -0
  162. package/dist/platform/ops/outbox.ops.js +125 -0
  163. package/dist/platform/ops/platform.ops.d.ts +1 -0
  164. package/dist/platform/ops/platform.ops.js +6 -3
  165. package/dist/platform/ops/schedules.ops.js +40 -1
  166. package/dist/platform/platform-ops.module.d.ts +2 -1
  167. package/dist/platform/platform-ops.module.js +3 -0
  168. package/dist/platform/route-schemas.js +3 -0
  169. package/dist/roles/worker-role.d.ts +10 -5
  170. package/dist/roles/worker-role.js +19 -9
  171. package/dist/roles/worker.module.js +11 -8
  172. package/dist/runtime/context/build.d.ts +3 -0
  173. package/dist/runtime/context/build.js +15 -0
  174. package/dist/runtime/context/plugin-context.d.ts +20 -0
  175. package/dist/runtime/context/plugin-context.js +30 -0
  176. package/dist/runtime/langgraph-loop.js +17 -5
  177. package/dist/runtime/limits.d.ts +15 -0
  178. package/dist/runtime/limits.js +25 -0
  179. package/dist/runtime/loop.d.ts +16 -3
  180. package/dist/runtime/loop.js +2 -1
  181. package/dist/runtime/maintenance.d.ts +7 -3
  182. package/dist/runtime/maintenance.js +52 -12
  183. package/dist/runtime/model-call.d.ts +20 -17
  184. package/dist/runtime/model-call.js +15 -24
  185. package/dist/runtime/plugin-context.service.d.ts +69 -0
  186. package/dist/runtime/plugin-context.service.js +208 -0
  187. package/dist/runtime/queue-lost-jobs.d.ts +24 -0
  188. package/dist/runtime/queue-lost-jobs.js +51 -0
  189. package/dist/runtime/run-control.d.ts +47 -5
  190. package/dist/runtime/run-control.js +79 -12
  191. package/dist/runtime/run-finish.d.ts +39 -12
  192. package/dist/runtime/run-finish.js +97 -14
  193. package/dist/runtime/run-jobs.d.ts +95 -13
  194. package/dist/runtime/run-jobs.js +199 -28
  195. package/dist/runtime/run-orphan-check.job.d.ts +142 -0
  196. package/dist/runtime/run-orphan-check.job.js +339 -0
  197. package/dist/runtime/run-processors.d.ts +16 -9
  198. package/dist/runtime/run-processors.js +1 -1
  199. package/dist/runtime/run-queue.d.ts +75 -53
  200. package/dist/runtime/run-queue.js +189 -263
  201. package/dist/runtime/run-steps.d.ts +1 -2
  202. package/dist/runtime/run-steps.js +8 -11
  203. package/dist/runtime/run-worker.d.ts +26 -16
  204. package/dist/runtime/run-worker.js +132 -37
  205. package/dist/runtime/runtime.module.d.ts +2 -3
  206. package/dist/runtime/runtime.module.js +7 -6
  207. package/dist/runtime/step-log.d.ts +16 -4
  208. package/dist/runtime/step-log.js +26 -6
  209. package/dist/runtime/stream.controller.d.ts +2 -4
  210. package/dist/runtime/stream.controller.js +5 -11
  211. package/dist/runtime/subagents/child.js +1 -1
  212. package/dist/runtime/subagents/definition.d.ts +2 -1
  213. package/dist/runtime/subagents/definition.js +4 -2
  214. package/dist/runtime/subagents/deliveries.d.ts +6 -3
  215. package/dist/runtime/subagents/deliveries.js +16 -7
  216. package/dist/runtime/subagents/dispatch.d.ts +12 -12
  217. package/dist/runtime/subagents/dispatch.js +14 -38
  218. package/dist/runtime/subagents/messages.d.ts +5 -1
  219. package/dist/runtime/subagents/messages.js +11 -3
  220. package/dist/runtime/subagents/skill-fork.d.ts +3 -3
  221. package/dist/runtime/subagents/skill-fork.js +8 -5
  222. package/dist/runtime/subagents/subagents.service.js +1 -1
  223. package/dist/runtime/subagents/tree-jobs.d.ts +61 -0
  224. package/dist/runtime/subagents/tree-jobs.js +86 -0
  225. package/dist/runtime/usage-report-jobs.d.ts +23 -48
  226. package/dist/runtime/usage-report-jobs.js +67 -119
  227. package/dist/runtime/usage-report.d.ts +28 -77
  228. package/dist/runtime/usage-report.js +35 -213
  229. package/dist/runtime/waits.d.ts +48 -198
  230. package/dist/runtime/waits.js +103 -805
  231. package/dist/sandbox/jobs.d.ts +20 -10
  232. package/dist/sandbox/jobs.js +23 -17
  233. package/dist/sandbox/sandbox.service.d.ts +17 -4
  234. package/dist/sandbox/sandbox.service.js +55 -14
  235. package/dist/schedules/calendar.d.ts +134 -0
  236. package/dist/schedules/calendar.js +410 -0
  237. package/dist/schedules/fire.d.ts +26 -7
  238. package/dist/schedules/fire.js +91 -22
  239. package/dist/schedules/job-ids.d.ts +66 -17
  240. package/dist/schedules/job-ids.js +81 -13
  241. package/dist/schedules/run-tracker.d.ts +0 -5
  242. package/dist/schedules/run-tracker.js +2 -14
  243. package/dist/schedules/scan.job.d.ts +13 -32
  244. package/dist/schedules/scan.job.js +50 -164
  245. package/dist/schedules/schedule-math.d.ts +42 -0
  246. package/dist/schedules/schedule-math.js +52 -0
  247. package/dist/schedules/scheduled-tasks.service.d.ts +22 -6
  248. package/dist/schedules/scheduled-tasks.service.js +70 -14
  249. package/dist/schedules/schedules.controller.d.ts +5 -1
  250. package/dist/schedules/schedules.controller.js +15 -1
  251. package/dist/schedules/schedules.module.js +0 -4
  252. package/dist/schedules/schedules.schemas.d.ts +35 -0
  253. package/dist/schedules/schedules.schemas.js +27 -0
  254. package/dist/schedules/store.d.ts +5 -0
  255. package/dist/schedules/store.js +20 -0
  256. package/dist/skills/changes.controller.d.ts +1 -1
  257. package/dist/skills/changes.controller.js +1 -1
  258. package/dist/tools/approvals.service.d.ts +41 -11
  259. package/dist/tools/approvals.service.js +108 -14
  260. package/dist/tools/builtin-names.d.ts +5 -3
  261. package/dist/tools/builtin-names.js +12 -4
  262. package/dist/tools/builtins/builtin-tools.d.ts +1 -2
  263. package/dist/tools/builtins/builtin-tools.js +21 -17
  264. package/dist/tools/builtins/sandbox/bash.js +9 -3
  265. package/dist/tools/builtins/sandbox/common.d.ts +18 -1
  266. package/dist/tools/builtins/sandbox/common.js +14 -1
  267. package/dist/tools/builtins/sandbox/index.d.ts +2 -1
  268. package/dist/tools/builtins/sandbox/index.js +4 -1
  269. package/dist/tools/builtins/sandbox/task-list.d.ts +12 -0
  270. package/dist/tools/builtins/sandbox/task-list.js +130 -0
  271. package/dist/tools/builtins/sandbox/task-output.d.ts +8 -1
  272. package/dist/tools/builtins/sandbox/task-output.js +21 -3
  273. package/dist/tools/builtins/web/extract.d.ts +2 -4
  274. package/dist/tools/builtins/web/extract.js +2 -5
  275. package/dist/tools/pipeline.d.ts +12 -3
  276. package/dist/tools/pipeline.js +6 -1
  277. package/dist/tools/questions.service.d.ts +30 -10
  278. package/dist/tools/questions.service.js +41 -27
  279. package/dist/tools/registry.d.ts +12 -3
  280. package/dist/tools/registry.js +13 -3
  281. package/dist/tools/tools.module.js +4 -3
  282. package/package.json +3 -3
  283. package/src/migrations/0118_device_policy_ops.sql +6 -0
  284. package/src/migrations/0119_outbox.sql +22 -0
  285. package/src/migrations/0120_drop_usage_report_sweep.sql +7 -0
  286. package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
  287. package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
  288. package/src/migrations/0123_pgvector.sql +27 -0
  289. package/src/migrations/0124_conversation_source.sql +36 -0
  290. package/dist/catalog/platform-migration.d.ts +0 -115
  291. package/dist/catalog/platform-migration.js +0 -583
  292. package/dist/infra/queue/add-job-once.d.ts +0 -14
  293. package/dist/infra/queue/add-job-once.js +0 -16
@@ -0,0 +1,410 @@
1
+ import { scanStep, tickerOf, ticksIn } from "./schedule-math.js";
2
+ /**
3
+ * The scheduled task calendar (spec 2026-11-12-scheduled-tasks-calendar-design.md §5): recorded
4
+ * runs and planned occurrences between two instants, dense task-days collapsed, cut at a whole
5
+ * day when a limit is reached. The assembly here is pure; `ScheduledTasksService.calendar` reads
6
+ * the rows and decides who sees what.
7
+ */
8
+ const DAY_MS = 86_400_000;
9
+ /** Planned occurrences reach this far ahead of now (spec §5.2, "about six weeks"). */
10
+ export const CALENDAR_HORIZON_DAYS = 42;
11
+ /** The longest range one request may ask for: a 6-week month grid plus a DST hour (§5.1). */
12
+ export const CALENDAR_RANGE_DAYS = 43;
13
+ /** The longest range with `taskId` (the panel's one-day list, §5.4). */
14
+ export const CALENDAR_TASK_RANGE_DAYS = 2;
15
+ /** A task-day with more items than this is collapsed into one summary (§5.4). */
16
+ export const CALENDAR_COLLAPSE_OVER = 6;
17
+ /**
18
+ * Ticks taken per request after the first local day, across all tasks (§5.5). One tick costs
19
+ * about 150-340 µs (cron's `getNextDateFrom`, measured 2026-11-12 with a ticker built once per
20
+ * task), so 3,000 keeps a worst-case request near one second.
21
+ */
22
+ export const CALENDAR_TICK_BUDGET = 3_000;
23
+ /** Entries (`items` + `collapsed`) per request after the first local day (§5.5). */
24
+ export const CALENDAR_ITEM_CAP = 3_000;
25
+ /** Run rows read per request after the first local day (§5.5). */
26
+ export const CALENDAR_RUN_ROW_CAP = 20_000;
27
+ /**
28
+ * The first requested local day is always returned whole, outside the limits above; only these
29
+ * safety caps stop it (then nothing is returned, `truncated`, `horizon` = `from`).
30
+ */
31
+ export const CALENDAR_FIRST_DAY_TICK_CAP = 10_000;
32
+ export const CALENDAR_FIRST_DAY_RUN_CAP = 20_000;
33
+ export const CALENDAR_LIMITS = {
34
+ horizonDays: CALENDAR_HORIZON_DAYS,
35
+ collapseOver: CALENDAR_COLLAPSE_OVER,
36
+ tickBudget: CALENDAR_TICK_BUDGET,
37
+ itemCap: CALENDAR_ITEM_CAP,
38
+ runRowCap: CALENDAR_RUN_ROW_CAP,
39
+ firstDayTickCap: CALENDAR_FIRST_DAY_TICK_CAP,
40
+ firstDayRunCap: CALENDAR_FIRST_DAY_RUN_CAP,
41
+ };
42
+ /** How many run rows the service reads: enough to tell where either run cap cuts. */
43
+ export const calendarRunReadLimit = (limits) => limits.firstDayRunCap + limits.runRowCap + 1;
44
+ /** The calendar answer from the rows (spec §5.2–§5.5). */
45
+ export function assembleCalendar(input, limits = CALENDAR_LIMITS) {
46
+ const { timeZone, now } = input;
47
+ const horizonAt = now.getTime() + limits.horizonDays * DAY_MS;
48
+ const fromMs = input.from.getTime();
49
+ // `cut`: the exclusive end of what is returned; a limit moves it back to a local day start, and
50
+ // only a first-day safety cap moves it to `from` (nothing returned)
51
+ let cut = input.to.getTime();
52
+ let truncated = false;
53
+ // the first requested local day is returned whole, outside the budget and the caps
54
+ const firstDay = dayKey(fromMs, timeZone);
55
+ const firstDayEnd = Math.min(cut, startOfLocalDay(startOfLocalDay(fromMs, timeZone) + 26 * 3_600_000, timeZone));
56
+ // the run read is bounded too: past a cap, the range ends at the start of the local day of the
57
+ // first row over it
58
+ const firstDayRuns = input.runs.filter((r) => r.scheduledFor.getTime() < firstDayEnd).length;
59
+ const overRun = input.runs[firstDayRuns + limits.runRowCap];
60
+ if (firstDayRuns > limits.firstDayRunCap) {
61
+ cut = fromMs;
62
+ truncated = true;
63
+ }
64
+ else if (overRun !== undefined) {
65
+ cut = startOfLocalDay(overRun.scheduledFor.getTime(), timeZone);
66
+ truncated = true;
67
+ }
68
+ const planned = input.plan && cut > fromMs
69
+ ? planOccurrences(input, horizonAt, limits, firstDayEnd, cut)
70
+ : { ticks: new Map(), cut };
71
+ if (planned.cut < cut) {
72
+ cut = planned.cut;
73
+ truncated = true;
74
+ }
75
+ // a tick the scan recorded is shown as its run, never twice
76
+ const recorded = new Set(input.runs.filter((r) => !r.trial).map((r) => `${r.taskId}|${r.scheduledFor.getTime()}`));
77
+ const byId = new Map(input.tasks.map((t) => [t.id, t]));
78
+ const raw = [];
79
+ for (const r of input.runs) {
80
+ const own = byId.get(r.taskId)?.accountId === input.viewerAccountId;
81
+ raw.push({ at: r.scheduledFor.getTime(), taskId: r.taskId, item: runItem(r, own) });
82
+ }
83
+ for (const [taskId, ticks] of planned.ticks) {
84
+ const jitterMs = (byId.get(taskId)?.jitterSeconds ?? 0) * 1000;
85
+ for (const tick of ticks) {
86
+ if (recorded.has(`${taskId}|${tick}`))
87
+ continue;
88
+ raw.push({
89
+ at: tick,
90
+ taskId,
91
+ item: {
92
+ kind: "occurrence",
93
+ taskId,
94
+ scheduledFor: new Date(tick).toISOString(),
95
+ at: new Date(tick + jitterMs).toISOString(),
96
+ },
97
+ });
98
+ }
99
+ }
100
+ // group by local day, then by task: a task-day over the threshold becomes one summary
101
+ const days = new Map();
102
+ for (const e of raw) {
103
+ if (e.at >= cut)
104
+ continue;
105
+ const day = dayKey(e.at, timeZone);
106
+ let tasks = days.get(day);
107
+ if (!tasks) {
108
+ tasks = new Map();
109
+ days.set(day, tasks);
110
+ }
111
+ const list = tasks.get(e.taskId);
112
+ if (list)
113
+ list.push(e);
114
+ else
115
+ tasks.set(e.taskId, [e]);
116
+ }
117
+ // walk the days in order; a day is added whole or not at all
118
+ const entries = [];
119
+ // the first day's entries do not count against the cap (the day is always returned whole)
120
+ let counted = 0;
121
+ for (const day of [...days.keys()].sort()) {
122
+ const dayEntries = [];
123
+ for (const [taskId, list] of days.get(day) ?? []) {
124
+ if (input.collapse && list.length > limits.collapseOver) {
125
+ dayEntries.push({
126
+ kind: "collapsed",
127
+ at: firstAt(list),
128
+ taskId,
129
+ collapsed: summarize(taskId, day, list),
130
+ });
131
+ }
132
+ else {
133
+ for (const e of list)
134
+ dayEntries.push({ kind: "item", ...e });
135
+ }
136
+ }
137
+ if (day !== firstDay && counted + dayEntries.length > limits.itemCap) {
138
+ const start = startOfLocalDay(dayEntries[0]?.at ?? cut, timeZone);
139
+ if (start < cut) {
140
+ cut = start;
141
+ truncated = true;
142
+ }
143
+ break;
144
+ }
145
+ entries.push(...dayEntries);
146
+ if (day !== firstDay)
147
+ counted += dayEntries.length;
148
+ }
149
+ const kept = entries.filter((e) => e.at < cut);
150
+ kept.sort((a, b) => a.at - b.at || cmp(a.taskId, b.taskId) || cmp(entryKey(a), entryKey(b)));
151
+ const items = [];
152
+ const collapsed = [];
153
+ const named = new Set();
154
+ for (const e of kept) {
155
+ named.add(e.taskId);
156
+ if (e.kind === "item")
157
+ items.push(e.item);
158
+ else
159
+ collapsed.push(e.collapsed);
160
+ }
161
+ return {
162
+ tasks: input.tasks.filter((t) => named.has(t.id)).map(calendarTask),
163
+ items,
164
+ collapsed,
165
+ horizon: new Date(Math.max(fromMs, truncated ? Math.min(cut, horizonAt) : horizonAt)).toISOString(),
166
+ truncated,
167
+ };
168
+ }
169
+ /** The stream's next tick, or null when it has no more (or its trigger cannot be evaluated). */
170
+ function pull(s) {
171
+ if (s.after >= s.until)
172
+ return null;
173
+ try {
174
+ const tick = ticksIn(s.ticker, new Date(s.after), new Date(s.until), 1).ticks[0];
175
+ s.after = tick === undefined ? Number.POSITIVE_INFINITY : tick.getTime();
176
+ return tick === undefined ? null : s.after;
177
+ }
178
+ catch {
179
+ // a trigger the math cannot evaluate: the scanner pauses the task; nothing to plan
180
+ s.after = Number.POSITIVE_INFINITY;
181
+ return null;
182
+ }
183
+ }
184
+ /**
185
+ * Each active task's planned ticks in `[from, min(to, horizon, expires_at)]` (spec §5.2), from the
186
+ * scanner's own math: a task already due (`next_run_at <= now`, not scanned yet) keeps only the
187
+ * tick the scan will fire (the latest due one inside the catch-up window; the older ones become
188
+ * one `missed` run), then the ticks after it as the scan continues; a task not due starts at its
189
+ * due tick, `next_run_at - jitter`.
190
+ *
191
+ * The tasks' ticks are taken in time order across all tasks (a merge), each one tick evaluation
192
+ * on the task's ticker (built once). Ticks of the first local day count against the first day's
193
+ * safety cap only (past it, nothing is returned: the cut moves to `from`); the first later tick
194
+ * past the budget cuts the range at the start of its local day, and the ticks already taken on
195
+ * that day are dropped, so the days before the cut are whole (spec §5.5). The counts are ticks
196
+ * taken: each stream's lookahead past its `until` and `scanStep`'s doubling search for a task
197
+ * already due are not counted (at most about 2 × tasks evaluations more).
198
+ */
199
+ function planOccurrences(input, horizonAt, limits, firstDayEnd, end) {
200
+ const fromMs = input.from.getTime();
201
+ const ticks = new Map();
202
+ const heap = new TickHeap();
203
+ for (const task of input.tasks) {
204
+ if (task.status !== "active" || task.nextRunAt === null)
205
+ continue;
206
+ const jitterMs = task.jitterSeconds * 1000;
207
+ const until = Math.min(end - 1, horizonAt, task.expiresAt?.getTime() ?? Number.POSITIVE_INFINITY);
208
+ if (until < fromMs)
209
+ continue;
210
+ let after = task.nextRunAt.getTime() - jitterMs - 1;
211
+ let first = null;
212
+ if (task.nextRunAt.getTime() <= input.now.getTime()) {
213
+ let step;
214
+ try {
215
+ step = scanStep({
216
+ trigger: task.trigger,
217
+ nextRunAt: task.nextRunAt,
218
+ jitterSeconds: task.jitterSeconds,
219
+ expiresAt: task.expiresAt,
220
+ }, input.now, input.catchUpWindowMinutes);
221
+ }
222
+ catch {
223
+ continue;
224
+ }
225
+ const fire = step.fire?.getTime();
226
+ if (fire !== undefined && fire >= fromMs && fire <= until)
227
+ first = fire;
228
+ after =
229
+ step.nextRunAt === null
230
+ ? Number.POSITIVE_INFINITY
231
+ : step.nextRunAt.getTime() - jitterMs - 1;
232
+ }
233
+ let ticker;
234
+ try {
235
+ ticker = tickerOf(task.trigger);
236
+ }
237
+ catch {
238
+ continue;
239
+ }
240
+ const stream = { task, ticker, until, after: Math.max(after, fromMs - 1) };
241
+ const head = first ?? pull(stream);
242
+ if (head === null)
243
+ continue;
244
+ ticks.set(task.id, []);
245
+ heap.push({ at: head, stream });
246
+ }
247
+ let cut = end;
248
+ // the first local day has its own safety cap; the budget counts the days after it
249
+ let used = 0;
250
+ let firstDayUsed = 0;
251
+ for (let top = heap.pop(); top !== undefined; top = heap.pop()) {
252
+ if (top.at < firstDayEnd) {
253
+ if (firstDayUsed >= limits.firstDayTickCap) {
254
+ cut = fromMs;
255
+ break;
256
+ }
257
+ firstDayUsed++;
258
+ }
259
+ else {
260
+ if (used >= limits.tickBudget) {
261
+ cut = startOfLocalDay(top.at, input.timeZone);
262
+ break;
263
+ }
264
+ used++;
265
+ }
266
+ ticks.get(top.stream.task.id)?.push(top.at);
267
+ const next = pull(top.stream);
268
+ if (next !== null)
269
+ heap.push({ at: next, stream: top.stream });
270
+ }
271
+ if (cut < end) {
272
+ for (const [id, list] of ticks)
273
+ ticks.set(id, list.filter((t) => t < cut));
274
+ }
275
+ return { ticks, cut };
276
+ }
277
+ /** A binary min-heap of stream heads by tick, then task id (a stable order). */
278
+ class TickHeap {
279
+ a = [];
280
+ less(i, j) {
281
+ const x = this.a[i];
282
+ const y = this.a[j];
283
+ return x.at < y.at || (x.at === y.at && x.stream.task.id < y.stream.task.id);
284
+ }
285
+ swap(i, j) {
286
+ const t = this.a[i];
287
+ this.a[i] = this.a[j];
288
+ this.a[j] = t;
289
+ }
290
+ push(e) {
291
+ this.a.push(e);
292
+ for (let i = this.a.length - 1; i > 0;) {
293
+ const p = (i - 1) >> 1;
294
+ if (!this.less(i, p))
295
+ break;
296
+ this.swap(i, p);
297
+ i = p;
298
+ }
299
+ }
300
+ pop() {
301
+ const top = this.a[0];
302
+ const last = this.a.pop();
303
+ if (top === undefined || last === undefined || this.a.length === 0)
304
+ return top;
305
+ this.a[0] = last;
306
+ for (let i = 0;;) {
307
+ const l = 2 * i + 1;
308
+ const r = l + 1;
309
+ let m = i;
310
+ if (l < this.a.length && this.less(l, m))
311
+ m = l;
312
+ if (r < this.a.length && this.less(r, m))
313
+ m = r;
314
+ if (m === i)
315
+ break;
316
+ this.swap(i, m);
317
+ i = m;
318
+ }
319
+ return top;
320
+ }
321
+ }
322
+ function runItem(r, own) {
323
+ return {
324
+ kind: "run",
325
+ taskId: r.taskId,
326
+ id: r.id,
327
+ scheduledFor: r.scheduledFor.toISOString(),
328
+ firedAt: r.firedAt?.toISOString() ?? null,
329
+ status: r.status,
330
+ skipReason: r.skipReason,
331
+ trial: r.trial,
332
+ manual: r.manual,
333
+ conversationId: own ? r.conversationId : null,
334
+ error: own ? r.error : null,
335
+ missingGrants: own ? r.missingGrants.length : null,
336
+ };
337
+ }
338
+ function summarize(taskId, day, list) {
339
+ const c = {
340
+ taskId,
341
+ day,
342
+ count: list.length,
343
+ upcoming: 0,
344
+ running: 0,
345
+ completed: 0,
346
+ failed: 0,
347
+ skipped: 0,
348
+ first: new Date(firstAt(list)).toISOString(),
349
+ last: new Date(Math.max(...list.map((e) => e.at))).toISOString(),
350
+ };
351
+ for (const { item } of list) {
352
+ if (item.kind === "occurrence")
353
+ c.upcoming++;
354
+ else if (item.status === "pending" || item.status === "running")
355
+ c.running++;
356
+ else
357
+ c[item.status]++;
358
+ }
359
+ return c;
360
+ }
361
+ function calendarTask(t) {
362
+ return {
363
+ id: t.id,
364
+ name: t.name,
365
+ agentId: t.agentId,
366
+ accountId: t.accountId,
367
+ status: t.status,
368
+ pauseReason: t.pauseReason,
369
+ trigger: t.trigger,
370
+ mode: t.mode,
371
+ jitterSeconds: t.jitterSeconds,
372
+ nextRunAt: t.nextRunAt?.toISOString() ?? null,
373
+ };
374
+ }
375
+ const firstAt = (list) => Math.min(...list.map((e) => e.at));
376
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
377
+ const entryKey = (e) => e.kind === "collapsed" ? "~" : e.item.kind === "run" ? `r${e.item.id}` : "o";
378
+ const dayFormats = new Map();
379
+ /** YYYY-MM-DD of the instant (epoch ms) in `timeZone`. */
380
+ export function dayKey(ms, timeZone) {
381
+ let f = dayFormats.get(timeZone);
382
+ if (!f) {
383
+ f = new Intl.DateTimeFormat("en-CA", {
384
+ timeZone,
385
+ year: "numeric",
386
+ month: "2-digit",
387
+ day: "2-digit",
388
+ });
389
+ dayFormats.set(timeZone, f);
390
+ }
391
+ const p = Object.fromEntries(f.formatToParts(new Date(ms)).map((x) => [x.type, x.value]));
392
+ return `${p.year}-${p.month}-${p.day}`;
393
+ }
394
+ /**
395
+ * The first instant (epoch ms) of the local day of `ms` in `timeZone`: midnight, or the end of a
396
+ * DST gap that swallows it. A day is at most 25 hours long, so 27 hours back is an earlier day.
397
+ */
398
+ export function startOfLocalDay(ms, timeZone) {
399
+ const day = dayKey(ms, timeZone);
400
+ let lo = ms - 27 * 3_600_000;
401
+ let hi = ms;
402
+ while (lo < hi) {
403
+ const mid = Math.floor((lo + hi) / 2);
404
+ if (dayKey(mid, timeZone) < day)
405
+ lo = mid + 1;
406
+ else
407
+ hi = mid;
408
+ }
409
+ return lo;
410
+ }
@@ -7,9 +7,10 @@ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
7
7
  import { KernelDb } from "../infra/database/kernel-db.js";
8
8
  import { TransactionHost } from "../infra/database/transaction-host.js";
9
9
  import { LockService } from "../infra/lock/lock.service.js";
10
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
10
11
  import type { KernelLogger } from "../log.js";
11
12
  import { OrgLock } from "../orgs/org-lock.js";
12
- import { RunControl } from "../runtime/run-control.js";
13
+ import { type SchedSettleJobData } from "./job-ids.js";
13
14
  import { ScheduledTaskRunTracker } from "./run-tracker.js";
14
15
  import { ScheduledTaskStore } from "./store.js";
15
16
  /** A task run's message client id in a conversation's queue (never read back as "the task's"). */
@@ -22,10 +23,12 @@ export declare function localTimeOf(at: Date, timeZone: string): string;
22
23
  *
23
24
  * 1. The claim, one transaction under the org row lock (which serializes the org's rate limit
24
25
  * count), then the task run and the task rows: the checks in order (task paused since the
25
- * scan, unless a person's "run now"; org disabled, creator
26
- * inactive or not a member, agent gone or not visible, conversation gone, connection inactive,
26
+ * scan, unless a person's "run now"; a tick fired more than 10 minutes past the org's catch-up
27
+ * window, `missed`, unless a "run now"; org disabled, creator inactive or not a member, agent
28
+ * gone or not visible, conversation gone, connection inactive,
27
29
  * overlap, rate limit, device offline, queue paused), each a `skipped` run with its reason, the
28
- * lasting ones also pausing the task; then `fired_at` is written, after the rate limit count.
30
+ * lasting ones also pausing the task; then `fired_at` is written, after the rate limit count,
31
+ * with the run's delayed settle check owed (`sched-settle`, {@link settle}).
29
32
  * A run whose claim committed before (a retried job) goes straight to the start.
30
33
  * 2. The start, idempotent: `new` creates the conversation with its first message keyed by the
31
34
  * task run id (`create_request_id`); `conversation` queues the prompt in the target
@@ -49,16 +52,32 @@ export declare class ScheduledTaskFirer {
49
52
  private readonly conversations;
50
53
  private readonly queue;
51
54
  private readonly connections;
52
- private readonly runs;
53
55
  /** Reads a local device's connection lease (online or not). */
54
56
  private readonly locks;
55
57
  private readonly log;
56
- constructor(kdb: KernelDb, txHost: TransactionHost, config: Pick<KernelConfigService, "get">, store: ScheduledTaskStore, tracker: Pick<ScheduledTaskRunTracker, "inFlightIn" | "failStartIn">, orgLock: Pick<OrgLock, "tryLock">, agents: Pick<AgentLookup, "get">, access: Pick<ConversationAccess, "isAccessible">, conversations: Pick<ConversationsService, "createWithMessage" | "create">, queue: Pick<ConversationQueue, "add">, connections: Pick<ChannelConnections, "row" | "channel">, runs: Pick<RunControl, "ensureRun">,
58
+ /** The claim owes the run's settle check (ruling R26). */
59
+ private readonly outbox;
60
+ constructor(kdb: KernelDb, txHost: TransactionHost, config: Pick<KernelConfigService, "get">, store: ScheduledTaskStore, tracker: Pick<ScheduledTaskRunTracker, "inFlightIn" | "failStartIn">, orgLock: Pick<OrgLock, "tryLock">, agents: Pick<AgentLookup, "get">, access: Pick<ConversationAccess, "isAccessible">, conversations: Pick<ConversationsService, "createWithMessage" | "create">, queue: Pick<ConversationQueue, "add">, connections: Pick<ChannelConnections, "row" | "channel">,
57
61
  /** Reads a local device's connection lease (online or not). */
58
- locks: Pick<LockService, "holderOf">, log: Pick<KernelLogger, "warn" | "info">);
62
+ locks: Pick<LockService, "holderOf">, log: Pick<KernelLogger, "warn" | "info">,
63
+ /** The claim owes the run's settle check (ruling R26). */
64
+ outbox: Pick<OutboxService, "add">);
59
65
  private get db();
60
66
  /** Fires task run `taskRunId` (see the class); a run not `pending` is left as it is. */
61
67
  fire(taskRunId: string): Promise<void>;
68
+ /**
69
+ * The `sched-settle` job (ruling R26; `job-ids.ts`). The claimed check: a task run claimed
70
+ * (`fired_at`) and still `pending` goes through the run tracker's check, which links it to its
71
+ * run, settles it, or releases a run that left no trace (`skipped`, `removed`), so a `once`
72
+ * task's last run never stays pending; one still in flight (its item queued, its message posted)
73
+ * owes the next check an hour on. The unfired check: a run still `pending` that no claim took
74
+ * (its fire job failed for good before the claim) is skipped, `missed` for a tick, `removed` for
75
+ * a "run now", so the overlap check never waits on it. A run claimed, started, ended or skipped
76
+ * since is left as it is (the claimed check follows a claimed run).
77
+ */
78
+ settle(data: SchedSettleJobData): Promise<void>;
79
+ /** The unfired check (see {@link settle}), task row first (the one lock order). */
80
+ private settleUnfired;
62
81
  /** Step 1 (see the class): a skip, the plan, or null when there is nothing to fire. */
63
82
  private claim;
64
83
  /** Step 2 (see the class). */