botinabox 2.5.1 → 2.5.2

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 (277) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +190 -190
  3. package/bin/botinabox.mjs +1 -1
  4. package/dist/channels/discord/adapter.d.ts +32 -0
  5. package/dist/channels/discord/adapter.js +70 -0
  6. package/dist/channels/discord/inbound.d.ts +25 -0
  7. package/dist/channels/discord/inbound.js +24 -0
  8. package/dist/channels/discord/models.d.ts +8 -0
  9. package/dist/channels/discord/models.js +5 -0
  10. package/dist/channels/discord/outbound.d.ts +14 -0
  11. package/dist/channels/discord/outbound.js +38 -0
  12. package/dist/channels/slack/adapter.d.ts +33 -0
  13. package/dist/channels/slack/adapter.js +74 -0
  14. package/dist/channels/slack/inbound.d.ts +59 -0
  15. package/dist/channels/slack/inbound.js +96 -0
  16. package/dist/channels/slack/index.d.ts +1 -1
  17. package/dist/channels/slack/models.d.ts +9 -0
  18. package/dist/channels/slack/models.js +5 -0
  19. package/dist/channels/slack/outbound.d.ts +12 -0
  20. package/dist/channels/slack/outbound.js +18 -0
  21. package/dist/channels/slack/transcribe.d.ts +41 -0
  22. package/dist/channels/slack/transcribe.js +106 -0
  23. package/dist/channels/webhook/adapter.d.ts +23 -0
  24. package/dist/channels/webhook/adapter.js +86 -0
  25. package/dist/channels/webhook/hmac.d.ts +13 -0
  26. package/dist/channels/webhook/hmac.js +26 -0
  27. package/dist/channels/webhook/models.d.ts +9 -0
  28. package/dist/channels/webhook/models.js +5 -0
  29. package/dist/channels/webhook/server.d.ts +20 -0
  30. package/dist/channels/webhook/server.js +91 -0
  31. package/dist/chat-pipeline-C-XlLGNl.d.ts +648 -0
  32. package/dist/chat-pipeline-CR1KF6eX.d.ts +652 -0
  33. package/dist/chat-pipeline-DisuC8SB.d.ts +643 -0
  34. package/dist/chat-pipeline-DuNX5WoL.d.ts +655 -0
  35. package/dist/chunk-2LGXQPEA.js +41 -0
  36. package/dist/chunk-3X3YKI4T.js +357 -0
  37. package/dist/chunk-D47AIFOD.js +351 -0
  38. package/dist/chunk-DSNJKNEW.js +328 -0
  39. package/dist/chunk-GS2JFL6I.js +144 -0
  40. package/dist/chunk-J6S6QMUY.js +144 -0
  41. package/dist/chunk-QLA6YOFN.js +22 -0
  42. package/dist/chunk-UACT2WXX.js +381 -0
  43. package/dist/cli/templates/config.yml.d.ts +7 -0
  44. package/dist/cli/templates/config.yml.js +61 -0
  45. package/dist/cli/templates/env.d.ts +1 -0
  46. package/dist/cli/templates/env.js +30 -0
  47. package/dist/cli/templates/index.ts.d.ts +2 -0
  48. package/dist/cli/templates/index.ts.js +30 -0
  49. package/dist/cli/templates/package.json.d.ts +5 -0
  50. package/dist/cli/templates/package.json.js +28 -0
  51. package/dist/cli.js +0 -0
  52. package/dist/connector-DDahQw-2.d.ts +63 -0
  53. package/dist/connectors/google/calendar-connector.d.ts +40 -0
  54. package/dist/connectors/google/calendar-connector.js +243 -0
  55. package/dist/connectors/google/gmail-connector.d.ts +42 -0
  56. package/dist/connectors/google/gmail-connector.js +345 -0
  57. package/dist/connectors/google/oauth.d.ts +48 -0
  58. package/dist/connectors/google/oauth.js +112 -0
  59. package/dist/connectors/google/types.d.ts +78 -0
  60. package/dist/connectors/google/types.js +2 -0
  61. package/dist/core/chat/auto-discovery.d.ts +16 -0
  62. package/dist/core/chat/auto-discovery.js +54 -0
  63. package/dist/core/chat/channel-registry.d.ts +45 -0
  64. package/dist/core/chat/channel-registry.js +96 -0
  65. package/dist/core/chat/chat-pipeline.d.ts +113 -0
  66. package/dist/core/chat/chat-pipeline.js +395 -0
  67. package/dist/core/chat/chat-responder.d.ts +90 -0
  68. package/dist/core/chat/chat-responder.js +185 -0
  69. package/dist/core/chat/formatter.d.ts +11 -0
  70. package/dist/core/chat/formatter.js +60 -0
  71. package/dist/core/chat/index.d.ts +24 -0
  72. package/dist/core/chat/index.js +18 -0
  73. package/dist/core/chat/message-interpreter.d.ts +91 -0
  74. package/dist/core/chat/message-interpreter.js +166 -0
  75. package/dist/core/chat/message-store.d.ts +66 -0
  76. package/dist/core/chat/message-store.js +131 -0
  77. package/dist/core/chat/notification-queue.d.ts +34 -0
  78. package/dist/core/chat/notification-queue.js +111 -0
  79. package/dist/core/chat/pipeline.d.ts +38 -0
  80. package/dist/core/chat/pipeline.js +89 -0
  81. package/dist/core/chat/policies.d.ts +16 -0
  82. package/dist/core/chat/policies.js +25 -0
  83. package/dist/core/chat/routing.d.ts +17 -0
  84. package/dist/core/chat/routing.js +36 -0
  85. package/dist/core/chat/session-key.d.ts +30 -0
  86. package/dist/core/chat/session-key.js +65 -0
  87. package/dist/core/chat/session-manager.d.ts +17 -0
  88. package/dist/core/chat/session-manager.js +23 -0
  89. package/dist/core/chat/text-chunker.d.ts +9 -0
  90. package/dist/core/chat/text-chunker.js +48 -0
  91. package/dist/core/chat/triage-router.d.ts +75 -0
  92. package/dist/core/chat/triage-router.js +142 -0
  93. package/dist/core/chat/types.d.ts +5 -0
  94. package/dist/core/chat/types.js +5 -0
  95. package/dist/core/config/defaults.d.ts +2 -0
  96. package/dist/core/config/defaults.js +38 -0
  97. package/dist/core/config/index.d.ts +6 -0
  98. package/dist/core/config/index.js +4 -0
  99. package/dist/core/config/interpolate.d.ts +5 -0
  100. package/dist/core/config/interpolate.js +27 -0
  101. package/dist/core/config/loader.d.ts +24 -0
  102. package/dist/core/config/loader.js +59 -0
  103. package/dist/core/config/schema.d.ts +5 -0
  104. package/dist/core/config/schema.js +119 -0
  105. package/dist/core/data/core-entity-contexts.d.ts +14 -0
  106. package/dist/core/data/core-entity-contexts.js +197 -0
  107. package/dist/core/data/core-migrations.d.ts +5 -0
  108. package/dist/core/data/core-migrations.js +45 -0
  109. package/dist/core/data/core-schema.d.ts +6 -0
  110. package/dist/core/data/core-schema.js +454 -0
  111. package/dist/core/data/data-store.d.ts +67 -0
  112. package/dist/core/data/data-store.js +218 -0
  113. package/dist/core/data/domain-entity-contexts.d.ts +29 -0
  114. package/dist/core/data/domain-entity-contexts.js +321 -0
  115. package/dist/core/data/domain-schema.d.ts +36 -0
  116. package/dist/core/data/domain-schema.js +323 -0
  117. package/dist/core/data/index.d.ts +7 -0
  118. package/dist/core/data/index.js +7 -0
  119. package/dist/core/data/types.d.ts +111 -0
  120. package/dist/core/data/types.js +1 -0
  121. package/dist/core/hooks/hook-bus.d.ts +18 -0
  122. package/dist/core/hooks/hook-bus.js +120 -0
  123. package/dist/core/hooks/index.d.ts +2 -0
  124. package/dist/core/hooks/index.js +1 -0
  125. package/dist/core/hooks/types.d.ts +19 -0
  126. package/dist/core/hooks/types.js +1 -0
  127. package/dist/core/index.d.ts +4 -0
  128. package/dist/core/index.js +4 -0
  129. package/dist/core/llm/auto-discovery.d.ts +11 -0
  130. package/dist/core/llm/auto-discovery.js +49 -0
  131. package/dist/core/llm/cost-tracker.d.ts +6 -0
  132. package/dist/core/llm/cost-tracker.js +38 -0
  133. package/dist/core/llm/index.d.ts +4 -0
  134. package/dist/core/llm/index.js +3 -0
  135. package/dist/core/llm/model-router.d.ts +25 -0
  136. package/dist/core/llm/model-router.js +49 -0
  137. package/dist/core/llm/provider-registry.d.ts +9 -0
  138. package/dist/core/llm/provider-registry.js +25 -0
  139. package/dist/core/llm/types.d.ts +2 -0
  140. package/dist/core/llm/types.js +2 -0
  141. package/dist/core/orchestrator/adapters/api-adapter.d.ts +34 -0
  142. package/dist/core/orchestrator/adapters/api-adapter.js +88 -0
  143. package/dist/core/orchestrator/adapters/cli-adapter.d.ts +22 -0
  144. package/dist/core/orchestrator/adapters/cli-adapter.js +69 -0
  145. package/dist/core/orchestrator/adapters/deterministic-adapter.d.ts +35 -0
  146. package/dist/core/orchestrator/adapters/deterministic-adapter.js +75 -0
  147. package/dist/core/orchestrator/adapters/env-whitelist.d.ts +4 -0
  148. package/dist/core/orchestrator/adapters/env-whitelist.js +27 -0
  149. package/dist/core/orchestrator/adapters/output-extractor.d.ts +11 -0
  150. package/dist/core/orchestrator/adapters/output-extractor.js +59 -0
  151. package/dist/core/orchestrator/adapters/process-manager.d.ts +15 -0
  152. package/dist/core/orchestrator/adapters/process-manager.js +26 -0
  153. package/dist/core/orchestrator/adapters/tool-loop.d.ts +22 -0
  154. package/dist/core/orchestrator/adapters/tool-loop.js +66 -0
  155. package/dist/core/orchestrator/agent-registry.d.ts +31 -0
  156. package/dist/core/orchestrator/agent-registry.js +135 -0
  157. package/dist/core/orchestrator/budget-controller.d.ts +19 -0
  158. package/dist/core/orchestrator/budget-controller.js +73 -0
  159. package/dist/core/orchestrator/chain-guard.d.ts +14 -0
  160. package/dist/core/orchestrator/chain-guard.js +23 -0
  161. package/dist/core/orchestrator/circuit-breaker.d.ts +65 -0
  162. package/dist/core/orchestrator/circuit-breaker.js +159 -0
  163. package/dist/core/orchestrator/claude-stream-parser.d.ts +31 -0
  164. package/dist/core/orchestrator/claude-stream-parser.js +99 -0
  165. package/dist/core/orchestrator/config-revisions.d.ts +6 -0
  166. package/dist/core/orchestrator/config-revisions.js +17 -0
  167. package/dist/core/orchestrator/dependency-resolver.d.ts +20 -0
  168. package/dist/core/orchestrator/dependency-resolver.js +78 -0
  169. package/dist/core/orchestrator/governance-gate.d.ts +110 -0
  170. package/dist/core/orchestrator/governance-gate.js +170 -0
  171. package/dist/core/orchestrator/learning-pipeline.d.ts +109 -0
  172. package/dist/core/orchestrator/learning-pipeline.js +249 -0
  173. package/dist/core/orchestrator/loop-detector.d.ts +51 -0
  174. package/dist/core/orchestrator/loop-detector.js +133 -0
  175. package/dist/core/orchestrator/ndjson-logger.d.ts +6 -0
  176. package/dist/core/orchestrator/ndjson-logger.js +18 -0
  177. package/dist/core/orchestrator/permission-relay.d.ts +72 -0
  178. package/dist/core/orchestrator/permission-relay.js +164 -0
  179. package/dist/core/orchestrator/run-manager.d.ts +31 -0
  180. package/dist/core/orchestrator/run-manager.js +178 -0
  181. package/dist/core/orchestrator/scheduler.d.ts +70 -0
  182. package/dist/core/orchestrator/scheduler.js +198 -0
  183. package/dist/core/orchestrator/secret-store.d.ts +57 -0
  184. package/dist/core/orchestrator/secret-store.js +171 -0
  185. package/dist/core/orchestrator/session-manager.d.ts +13 -0
  186. package/dist/core/orchestrator/session-manager.js +66 -0
  187. package/dist/core/orchestrator/task-queue.d.ts +34 -0
  188. package/dist/core/orchestrator/task-queue.js +83 -0
  189. package/dist/core/orchestrator/template-interpolate.d.ts +5 -0
  190. package/dist/core/orchestrator/template-interpolate.js +18 -0
  191. package/dist/core/orchestrator/user-registry.d.ts +47 -0
  192. package/dist/core/orchestrator/user-registry.js +76 -0
  193. package/dist/core/orchestrator/wakeup-queue.d.ts +9 -0
  194. package/dist/core/orchestrator/wakeup-queue.js +45 -0
  195. package/dist/core/orchestrator/workflow-engine.d.ts +47 -0
  196. package/dist/core/orchestrator/workflow-engine.js +204 -0
  197. package/dist/core/security/audit.d.ts +20 -0
  198. package/dist/core/security/audit.js +33 -0
  199. package/dist/core/security/column-validator.d.ts +20 -0
  200. package/dist/core/security/column-validator.js +37 -0
  201. package/dist/core/security/index.d.ts +5 -0
  202. package/dist/core/security/index.js +5 -0
  203. package/dist/core/security/process-env.d.ts +13 -0
  204. package/dist/core/security/process-env.js +49 -0
  205. package/dist/core/security/sanitizer.d.ts +11 -0
  206. package/dist/core/security/sanitizer.js +39 -0
  207. package/dist/core/security/types.d.ts +11 -0
  208. package/dist/core/security/types.js +1 -0
  209. package/dist/core/update/auto-update.d.ts +21 -0
  210. package/dist/core/update/auto-update.js +102 -0
  211. package/dist/core/update/backup-manager.d.ts +7 -0
  212. package/dist/core/update/backup-manager.js +24 -0
  213. package/dist/core/update/index.d.ts +8 -0
  214. package/dist/core/update/index.js +6 -0
  215. package/dist/core/update/migration-hooks.d.ts +11 -0
  216. package/dist/core/update/migration-hooks.js +10 -0
  217. package/dist/core/update/types.d.ts +11 -0
  218. package/dist/core/update/types.js +1 -0
  219. package/dist/core/update/update-checker.d.ts +11 -0
  220. package/dist/core/update/update-checker.js +63 -0
  221. package/dist/core/update/update-manager.d.ts +25 -0
  222. package/dist/core/update/update-manager.js +101 -0
  223. package/dist/core/update/version-utils.d.ts +6 -0
  224. package/dist/core/update/version-utils.js +34 -0
  225. package/dist/gmail-connector-2FVYTQJH.js +6 -0
  226. package/dist/gmail-connector-MNUBRNFM.js +6 -0
  227. package/dist/gmail-connector-PS2VLGNE.js +6 -0
  228. package/dist/gmail-connector-ULSMN6X2.js +6 -0
  229. package/dist/gmail-connector-URRFX6A3.js +6 -0
  230. package/dist/inbound-AFBUPSPG.js +10 -0
  231. package/dist/inbound-AFOHYNUY.js +6 -0
  232. package/dist/inbound-CGIXRXGC.js +8 -0
  233. package/dist/inbound-MCOLRH6U.js +10 -0
  234. package/dist/inbound-SNEMBLGA.js +6 -0
  235. package/dist/inbound-ZJHAYVMF.js +10 -0
  236. package/dist/index.d.ts +16 -3
  237. package/dist/index.js +67 -28
  238. package/dist/provider-qqJYv9nv.d.ts +75 -0
  239. package/dist/providers/anthropic/models.d.ts +2 -0
  240. package/dist/providers/anthropic/models.js +29 -0
  241. package/dist/providers/anthropic/provider.d.ts +13 -0
  242. package/dist/providers/anthropic/provider.js +119 -0
  243. package/dist/providers/anthropic/tool-converter.d.ts +10 -0
  244. package/dist/providers/anthropic/tool-converter.js +7 -0
  245. package/dist/providers/ollama/provider.d.ts +17 -0
  246. package/dist/providers/ollama/provider.js +185 -0
  247. package/dist/providers/openai/models.d.ts +2 -0
  248. package/dist/providers/openai/models.js +29 -0
  249. package/dist/providers/openai/provider.d.ts +13 -0
  250. package/dist/providers/openai/provider.js +163 -0
  251. package/dist/providers/openai/tool-converter.d.ts +10 -0
  252. package/dist/providers/openai/tool-converter.js +10 -0
  253. package/dist/shared/constants.d.ts +50 -0
  254. package/dist/shared/constants.js +64 -0
  255. package/dist/shared/index.d.ts +14 -0
  256. package/dist/shared/index.js +14 -0
  257. package/dist/shared/types/agent.d.ts +36 -0
  258. package/dist/shared/types/agent.js +2 -0
  259. package/dist/shared/types/channel.d.ts +70 -0
  260. package/dist/shared/types/channel.js +2 -0
  261. package/dist/shared/types/config.d.ts +111 -0
  262. package/dist/shared/types/config.js +2 -0
  263. package/dist/shared/types/connector.d.ts +77 -0
  264. package/dist/shared/types/connector.js +2 -0
  265. package/dist/shared/types/execution.d.ts +29 -0
  266. package/dist/shared/types/execution.js +2 -0
  267. package/dist/shared/types/provider.d.ts +73 -0
  268. package/dist/shared/types/provider.js +2 -0
  269. package/dist/shared/types/task.d.ts +47 -0
  270. package/dist/shared/types/task.js +2 -0
  271. package/dist/shared/types/workflow.d.ts +39 -0
  272. package/dist/shared/types/workflow.js +2 -0
  273. package/dist/shared/utils.d.ts +6 -0
  274. package/dist/shared/utils.js +13 -0
  275. package/dist/update-check.d.ts +5 -0
  276. package/dist/update-check.js +56 -0
  277. package/package.json +100 -100
@@ -0,0 +1,31 @@
1
+ import type { DataStore } from '../data/data-store.js';
2
+ import type { HookBus } from '../hooks/hook-bus.js';
3
+ import type { CircuitBreaker } from './circuit-breaker.js';
4
+ export declare class RunManager {
5
+ private db;
6
+ private hooks;
7
+ private config?;
8
+ private locks;
9
+ private orphanTimer;
10
+ private readonly staleThresholdMs;
11
+ private circuitBreaker?;
12
+ constructor(db: DataStore, hooks: HookBus, config?: {
13
+ staleThresholdMs?: number;
14
+ maxBackoffMs?: number;
15
+ } | undefined);
16
+ /**
17
+ * Attach a CircuitBreaker to prevent retries on broken agents.
18
+ */
19
+ setCircuitBreaker(cb: CircuitBreaker): void;
20
+ isLocked(agentId: string): boolean;
21
+ startRun(agentId: string, taskId: string, adapter?: string): Promise<string>;
22
+ finishRun(runId: string, result: {
23
+ exitCode: number;
24
+ output?: string;
25
+ costCents?: number;
26
+ usage?: unknown;
27
+ }): Promise<void>;
28
+ reapOrphans(): Promise<void>;
29
+ startOrphanReaper(intervalMs?: number): void;
30
+ stopOrphanReaper(): void;
31
+ }
@@ -0,0 +1,178 @@
1
+ import { checkChainDepth, MAX_CHAIN_DEPTH } from './chain-guard.js';
2
+ import { interpolate } from './template-interpolate.js';
3
+ const DEFAULT_STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
4
+ const BASE_BACKOFF_MS = 5_000;
5
+ const DEFAULT_MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
6
+ export class RunManager {
7
+ db;
8
+ hooks;
9
+ config;
10
+ locks = new Map(); // agentId → runId
11
+ orphanTimer = null;
12
+ staleThresholdMs;
13
+ circuitBreaker;
14
+ constructor(db, hooks, config) {
15
+ this.db = db;
16
+ this.hooks = hooks;
17
+ this.config = config;
18
+ this.staleThresholdMs = config?.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
19
+ }
20
+ /**
21
+ * Attach a CircuitBreaker to prevent retries on broken agents.
22
+ */
23
+ setCircuitBreaker(cb) {
24
+ this.circuitBreaker = cb;
25
+ }
26
+ isLocked(agentId) {
27
+ return this.locks.has(agentId);
28
+ }
29
+ async startRun(agentId, taskId, adapter) {
30
+ if (this.locks.has(agentId)) {
31
+ throw new Error(`Agent already has an active run`);
32
+ }
33
+ const row = await this.db.insert('runs', {
34
+ agent_id: agentId,
35
+ task_id: taskId,
36
+ adapter: adapter,
37
+ status: 'running',
38
+ started_at: new Date().toISOString(),
39
+ });
40
+ const runId = row['id'];
41
+ this.locks.set(agentId, runId);
42
+ return runId;
43
+ }
44
+ async finishRun(runId, result) {
45
+ const run = await this.db.get('runs', { id: runId });
46
+ if (!run)
47
+ throw new Error(`Run not found: ${runId}`);
48
+ const succeeded = result.exitCode === 0;
49
+ const status = succeeded ? 'succeeded' : 'failed';
50
+ const usage = result.usage;
51
+ await this.db.update('runs', { id: runId }, {
52
+ status,
53
+ completed_at: new Date().toISOString(),
54
+ exit_code: result.exitCode,
55
+ cost_cents: result.costCents ?? 0,
56
+ input_tokens: usage?.['inputTokens'] ?? 0,
57
+ output_tokens: usage?.['outputTokens'] ?? 0,
58
+ error_message: result.exitCode !== 0 ? result.output : undefined,
59
+ });
60
+ // Release lock
61
+ const agentId = run['agent_id'];
62
+ this.locks.delete(agentId);
63
+ const taskId = run['task_id'];
64
+ if (!succeeded) {
65
+ // Record failure in circuit breaker (if attached)
66
+ if (this.circuitBreaker) {
67
+ await this.circuitBreaker.recordFailure(agentId, result.output);
68
+ }
69
+ // Retry policy — skip retry if circuit breaker is open
70
+ const task = await this.db.get('tasks', { id: taskId });
71
+ if (task) {
72
+ const retryCount = task['retry_count'] ?? 0;
73
+ const maxRetries = task['max_retries'] ?? 0;
74
+ const circuitOpen = this.circuitBreaker
75
+ ? !this.circuitBreaker.canExecute(agentId)
76
+ : false;
77
+ if (retryCount < maxRetries && !circuitOpen) {
78
+ // Exponential backoff: BASE_BACKOFF_MS * 2^retryCount
79
+ const maxBackoff = this.config?.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
80
+ const backoffMs = Math.min(BASE_BACKOFF_MS * Math.pow(2, retryCount), maxBackoff);
81
+ const nextRetryAt = new Date(Date.now() + backoffMs).toISOString();
82
+ await this.db.update('tasks', { id: taskId }, {
83
+ retry_count: retryCount + 1,
84
+ next_retry_at: nextRetryAt,
85
+ status: 'todo',
86
+ execution_run_id: null,
87
+ updated_at: new Date().toISOString(),
88
+ });
89
+ }
90
+ else {
91
+ // Retries exhausted or circuit open — mark task as failed
92
+ await this.db.update('tasks', { id: taskId }, {
93
+ status: 'failed',
94
+ updated_at: new Date().toISOString(),
95
+ });
96
+ }
97
+ }
98
+ }
99
+ else {
100
+ // Record success in circuit breaker (if attached)
101
+ if (this.circuitBreaker) {
102
+ await this.circuitBreaker.recordSuccess(agentId);
103
+ }
104
+ // Mark task done with result before emitting run.completed,
105
+ // so hook handlers can read task.result immediately.
106
+ await this.db.update('tasks', { id: taskId }, {
107
+ status: 'done',
108
+ result: result.output,
109
+ updated_at: new Date().toISOString(),
110
+ });
111
+ // Followup chain
112
+ const task = await this.db.get('tasks', { id: taskId });
113
+ if (task && task['followup_agent_id']) {
114
+ const chainDepth = (task['chain_depth'] ?? 0) + 1;
115
+ checkChainDepth(chainDepth, MAX_CHAIN_DEPTH);
116
+ const followupAgentId = task['followup_agent_id'];
117
+ const followupTemplate = task['followup_template'] ?? 'Followup: {{output}}';
118
+ const context = { output: result.output ?? '' };
119
+ const title = interpolate(followupTemplate, context);
120
+ const chainOriginId = task['chain_origin_id'] ?? taskId;
121
+ await this.db.insert('tasks', {
122
+ title,
123
+ description: title,
124
+ assignee_id: followupAgentId,
125
+ status: 'todo',
126
+ priority: task['priority'] ?? 5,
127
+ chain_depth: chainDepth,
128
+ chain_origin_id: chainOriginId,
129
+ });
130
+ await this.hooks.emit('task.followup.created', {
131
+ originTaskId: taskId,
132
+ followupAgentId,
133
+ chainDepth,
134
+ });
135
+ }
136
+ }
137
+ await this.hooks.emit('run.completed', {
138
+ runId,
139
+ agentId,
140
+ taskId,
141
+ status,
142
+ exitCode: result.exitCode,
143
+ });
144
+ }
145
+ async reapOrphans() {
146
+ const cutoff = new Date(Date.now() - this.staleThresholdMs).toISOString();
147
+ const staleRuns = (await this.db.query('runs', { where: { status: 'running' } }))
148
+ .filter((r) => {
149
+ const startedAt = r['started_at'];
150
+ return startedAt != null && startedAt < cutoff;
151
+ });
152
+ for (const run of staleRuns) {
153
+ await this.db.update('runs', { id: run['id'] }, {
154
+ status: 'failed',
155
+ completed_at: new Date().toISOString(),
156
+ error_message: 'Orphaned run reaped by RunManager',
157
+ });
158
+ // Release in-memory lock if we hold it
159
+ const agentId = run['agent_id'];
160
+ if (this.locks.get(agentId) === run['id']) {
161
+ this.locks.delete(agentId);
162
+ }
163
+ }
164
+ }
165
+ startOrphanReaper(intervalMs = 60_000) {
166
+ if (this.orphanTimer)
167
+ return;
168
+ this.orphanTimer = setInterval(() => {
169
+ void this.reapOrphans();
170
+ }, intervalMs);
171
+ }
172
+ stopOrphanReaper() {
173
+ if (this.orphanTimer) {
174
+ clearInterval(this.orphanTimer);
175
+ this.orphanTimer = null;
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Scheduler — database-backed job scheduling with cron expressions.
3
+ *
4
+ * Supports one-time and recurring schedules. When a schedule fires,
5
+ * it emits the schedule's `action` as a hook event with the
6
+ * `action_config` payload. Consumers subscribe to handle the action.
7
+ *
8
+ * Supports one-time and recurring use cases.
9
+ */
10
+ import type { DataStore } from "../data/data-store.js";
11
+ import type { HookBus } from "../hooks/hook-bus.js";
12
+ export interface ScheduleDef {
13
+ name: string;
14
+ description?: string;
15
+ /** Cron expression for recurring schedules */
16
+ cron?: string;
17
+ /** ISO 8601 datetime for one-time schedules */
18
+ runAt?: string;
19
+ /** Hook event name to emit when fired */
20
+ action: string;
21
+ /** JSON-serializable payload passed to the hook */
22
+ actionConfig?: Record<string, unknown>;
23
+ timezone?: string;
24
+ }
25
+ export interface Schedule {
26
+ id: string;
27
+ name: string;
28
+ description: string | null;
29
+ type: "one_time" | "recurring";
30
+ cron: string | null;
31
+ run_at: string | null;
32
+ timezone: string;
33
+ enabled: number;
34
+ action: string;
35
+ action_config: string;
36
+ last_fired_at: string | null;
37
+ next_fire_at: string | null;
38
+ created_at: string;
39
+ updated_at: string;
40
+ deleted_at: string | null;
41
+ }
42
+ export declare class Scheduler {
43
+ private db;
44
+ private hooks;
45
+ private timer;
46
+ constructor(db: DataStore, hooks: HookBus);
47
+ /**
48
+ * Start the scheduler. Computes initial next_fire_at for schedules
49
+ * that don't have one, then polls for due schedules.
50
+ */
51
+ start(pollIntervalMs?: number): Promise<void>;
52
+ stop(): void;
53
+ /** Check for and fire due schedules. */
54
+ tick(): Promise<void>;
55
+ /** Register a new schedule. */
56
+ register(def: ScheduleDef): Promise<string>;
57
+ /** Update an existing schedule. */
58
+ update(id: string, changes: Partial<Pick<ScheduleDef, "name" | "cron" | "runAt" | "action" | "actionConfig" | "timezone" | "description">> & {
59
+ enabled?: boolean;
60
+ }): Promise<void>;
61
+ /** Soft-delete a schedule. */
62
+ unregister(id: string): Promise<void>;
63
+ /** List schedules, optionally filtered. */
64
+ list(filter?: {
65
+ enabled?: boolean;
66
+ action?: string;
67
+ }): Promise<Schedule[]>;
68
+ /** Compute next_fire_at for any enabled schedule missing it. */
69
+ private initializeNextFireTimes;
70
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Scheduler — database-backed job scheduling with cron expressions.
3
+ *
4
+ * Supports one-time and recurring schedules. When a schedule fires,
5
+ * it emits the schedule's `action` as a hook event with the
6
+ * `action_config` payload. Consumers subscribe to handle the action.
7
+ *
8
+ * Supports one-time and recurring use cases.
9
+ */
10
+ import cronParser from "cron-parser";
11
+ import { v4 as uuid } from "uuid";
12
+ function computeNextFire(cron, timezone, after) {
13
+ const interval = cronParser.parseExpression(cron, {
14
+ currentDate: after ?? new Date(),
15
+ tz: timezone,
16
+ });
17
+ return interval.next().toISOString();
18
+ }
19
+ export class Scheduler {
20
+ db;
21
+ hooks;
22
+ timer = null;
23
+ constructor(db, hooks) {
24
+ this.db = db;
25
+ this.hooks = hooks;
26
+ }
27
+ /**
28
+ * Start the scheduler. Computes initial next_fire_at for schedules
29
+ * that don't have one, then polls for due schedules.
30
+ */
31
+ async start(pollIntervalMs = 30_000) {
32
+ await this.initializeNextFireTimes();
33
+ this.timer = setInterval(() => {
34
+ void this.tick();
35
+ }, pollIntervalMs);
36
+ // Fire immediately on start
37
+ void this.tick();
38
+ }
39
+ stop() {
40
+ if (this.timer) {
41
+ clearInterval(this.timer);
42
+ this.timer = null;
43
+ }
44
+ }
45
+ /** Check for and fire due schedules. */
46
+ async tick() {
47
+ const now = new Date().toISOString();
48
+ const schedules = (await this.db.query("schedules", {
49
+ where: { enabled: 1 },
50
+ })).filter((s) => s["deleted_at"] == null &&
51
+ s["next_fire_at"] != null &&
52
+ s["next_fire_at"] <= now);
53
+ for (const schedule of schedules) {
54
+ try {
55
+ const config = JSON.parse(schedule.action_config || "{}");
56
+ // Filter prototype pollution vectors
57
+ const safeConfig = {};
58
+ for (const [k, v] of Object.entries(config)) {
59
+ if (!k.startsWith('__'))
60
+ safeConfig[k] = v;
61
+ }
62
+ // Emit the action hook (e.g. 'connector.sync', 'channel.send')
63
+ await this.hooks.emit(schedule.action, {
64
+ schedule_id: schedule.id,
65
+ schedule_name: schedule.name,
66
+ ...safeConfig,
67
+ });
68
+ // Emit observability hook
69
+ await this.hooks.emit("schedule.fired", {
70
+ schedule_id: schedule.id,
71
+ schedule_name: schedule.name,
72
+ action: schedule.action,
73
+ fired_at: now,
74
+ });
75
+ // Update last_fired_at and compute next
76
+ if (schedule.type === "recurring" && schedule.cron) {
77
+ const nextFire = computeNextFire(schedule.cron, schedule.timezone, new Date());
78
+ await this.db.update("schedules", { id: schedule.id }, {
79
+ last_fired_at: now,
80
+ next_fire_at: nextFire,
81
+ updated_at: now,
82
+ });
83
+ }
84
+ else {
85
+ // One-time: disable after firing
86
+ await this.db.update("schedules", { id: schedule.id }, {
87
+ last_fired_at: now,
88
+ next_fire_at: null,
89
+ enabled: 0,
90
+ updated_at: now,
91
+ });
92
+ }
93
+ }
94
+ catch (err) {
95
+ console.error(`[Scheduler] Error firing schedule "${schedule.name}":`, err);
96
+ await this.hooks.emit("schedule.error", {
97
+ schedule_id: schedule.id,
98
+ schedule_name: schedule.name,
99
+ error: String(err),
100
+ });
101
+ }
102
+ }
103
+ }
104
+ /** Register a new schedule. */
105
+ async register(def) {
106
+ const id = uuid();
107
+ const type = def.cron ? "recurring" : "one_time";
108
+ const timezone = def.timezone ?? "UTC";
109
+ let nextFire = null;
110
+ if (def.cron) {
111
+ nextFire = computeNextFire(def.cron, timezone);
112
+ }
113
+ else if (def.runAt) {
114
+ nextFire = new Date(def.runAt).toISOString();
115
+ }
116
+ await this.db.insert("schedules", {
117
+ id,
118
+ name: def.name,
119
+ description: def.description ?? null,
120
+ type,
121
+ cron: def.cron ?? null,
122
+ run_at: def.runAt ?? null,
123
+ timezone,
124
+ enabled: 1,
125
+ action: def.action,
126
+ action_config: JSON.stringify(def.actionConfig ?? {}),
127
+ next_fire_at: nextFire,
128
+ });
129
+ return id;
130
+ }
131
+ /** Update an existing schedule. */
132
+ async update(id, changes) {
133
+ const row = {
134
+ updated_at: new Date().toISOString(),
135
+ };
136
+ if (changes.name !== undefined)
137
+ row["name"] = changes.name;
138
+ if (changes.description !== undefined)
139
+ row["description"] = changes.description;
140
+ if (changes.action !== undefined)
141
+ row["action"] = changes.action;
142
+ if (changes.actionConfig !== undefined)
143
+ row["action_config"] = JSON.stringify(changes.actionConfig);
144
+ if (changes.enabled !== undefined)
145
+ row["enabled"] = changes.enabled ? 1 : 0;
146
+ if (changes.cron !== undefined) {
147
+ row["cron"] = changes.cron;
148
+ row["type"] = "recurring";
149
+ row["run_at"] = null;
150
+ row["next_fire_at"] = computeNextFire(changes.cron, changes.timezone ?? "UTC");
151
+ }
152
+ else if (changes.runAt !== undefined) {
153
+ row["run_at"] = changes.runAt;
154
+ row["type"] = "one_time";
155
+ row["cron"] = null;
156
+ row["next_fire_at"] = new Date(changes.runAt).toISOString();
157
+ }
158
+ if (changes.timezone !== undefined)
159
+ row["timezone"] = changes.timezone;
160
+ await this.db.update("schedules", { id }, row);
161
+ }
162
+ /** Soft-delete a schedule. */
163
+ async unregister(id) {
164
+ await this.db.update("schedules", { id }, {
165
+ enabled: 0,
166
+ deleted_at: new Date().toISOString(),
167
+ updated_at: new Date().toISOString(),
168
+ });
169
+ }
170
+ /** List schedules, optionally filtered. */
171
+ async list(filter) {
172
+ const where = {};
173
+ if (filter?.enabled !== undefined)
174
+ where["enabled"] = filter.enabled ? 1 : 0;
175
+ if (filter?.action !== undefined)
176
+ where["action"] = filter.action;
177
+ return (await this.db.query("schedules", { where })).filter((s) => s["deleted_at"] == null);
178
+ }
179
+ /** Compute next_fire_at for any enabled schedule missing it. */
180
+ async initializeNextFireTimes() {
181
+ const schedules = (await this.db.query("schedules", { where: { enabled: 1 } })).filter((s) => s["deleted_at"] == null && s["next_fire_at"] == null);
182
+ for (const s of schedules) {
183
+ let nextFire = null;
184
+ if (s.type === "recurring" && s.cron) {
185
+ nextFire = computeNextFire(s.cron, s.timezone);
186
+ }
187
+ else if (s.type === "one_time" && s.run_at) {
188
+ nextFire = new Date(s.run_at).toISOString();
189
+ }
190
+ if (nextFire) {
191
+ await this.db.update("schedules", { id: s.id }, {
192
+ next_fire_at: nextFire,
193
+ updated_at: new Date().toISOString(),
194
+ });
195
+ }
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,57 @@
1
+ import type { DataStore } from "../data/data-store.js";
2
+ import type { HookBus } from "../hooks/hook-bus.js";
3
+ export interface SecretInput {
4
+ name: string;
5
+ type?: string;
6
+ environment?: string;
7
+ value?: string;
8
+ location?: string;
9
+ description?: string;
10
+ rotation_schedule?: string;
11
+ expires_at?: string;
12
+ notes?: string;
13
+ org_id?: string;
14
+ }
15
+ export interface SecretMeta {
16
+ id: string;
17
+ org_id: string | null;
18
+ name: string;
19
+ type: string;
20
+ environment: string;
21
+ location: string | null;
22
+ description: string | null;
23
+ rotation_schedule: string | null;
24
+ expires_at: string | null;
25
+ notes: string | null;
26
+ created_at: string;
27
+ updated_at: string;
28
+ }
29
+ export declare class SecretStore {
30
+ private readonly db;
31
+ private readonly hooks;
32
+ private readonly encKey;
33
+ /**
34
+ * @param db - DataStore instance
35
+ * @param hooks - HookBus instance
36
+ * @param encryptionKey - Optional master key for encrypting secrets at rest.
37
+ * When provided, all new secrets are encrypted with AES-256-GCM.
38
+ * Existing plaintext secrets are read transparently (passthrough on decrypt).
39
+ */
40
+ constructor(db: DataStore, hooks: HookBus, encryptionKey?: string);
41
+ set(input: SecretInput): Promise<SecretMeta>;
42
+ get(name: string, environment?: string): Promise<string | null>;
43
+ getMeta(name: string, environment?: string): Promise<SecretMeta | null>;
44
+ list(): Promise<SecretMeta[]>;
45
+ rotate(name: string, newValue: string, environment?: string): Promise<void>;
46
+ delete(name: string, environment?: string): Promise<void>;
47
+ /**
48
+ * Load a sync cursor by key. Returns undefined if not found.
49
+ * Cursors are stored as secrets with type='sync_cursor'.
50
+ */
51
+ loadCursor(key: string): Promise<string | undefined>;
52
+ /**
53
+ * Persist a sync cursor by key. Creates or updates the secret.
54
+ */
55
+ saveCursor(key: string, value: string): Promise<void>;
56
+ private _toMeta;
57
+ }