@hybridaione/hybridclaw 0.2.2 → 0.2.6

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/.github/workflows/ci.yml +70 -0
  2. package/.husky/pre-commit +1 -0
  3. package/CHANGELOG.md +85 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/README.md +41 -16
  6. package/SECURITY.md +17 -0
  7. package/biome.json +35 -0
  8. package/config.example.json +71 -8
  9. package/container/package-lock.json +2 -2
  10. package/container/package.json +1 -1
  11. package/container/src/approval-policy.ts +1303 -0
  12. package/container/src/browser-tools.ts +431 -136
  13. package/container/src/extensions.ts +36 -12
  14. package/container/src/hybridai-client.ts +34 -13
  15. package/container/src/index.ts +451 -109
  16. package/container/src/ipc.ts +5 -3
  17. package/container/src/token-usage.ts +20 -10
  18. package/container/src/tools.ts +599 -225
  19. package/container/src/types.ts +32 -2
  20. package/container/src/web-fetch.ts +89 -32
  21. package/dist/agent.d.ts.map +1 -1
  22. package/dist/agent.js +10 -2
  23. package/dist/agent.js.map +1 -1
  24. package/dist/audit-cli.d.ts.map +1 -1
  25. package/dist/audit-cli.js +4 -2
  26. package/dist/audit-cli.js.map +1 -1
  27. package/dist/audit-events.d.ts.map +1 -1
  28. package/dist/audit-events.js +53 -3
  29. package/dist/audit-events.js.map +1 -1
  30. package/dist/audit-trail.d.ts.map +1 -1
  31. package/dist/audit-trail.js +17 -8
  32. package/dist/audit-trail.js.map +1 -1
  33. package/dist/channels/discord/attachments.d.ts.map +1 -1
  34. package/dist/channels/discord/attachments.js +14 -7
  35. package/dist/channels/discord/attachments.js.map +1 -1
  36. package/dist/channels/discord/debounce.d.ts +9 -0
  37. package/dist/channels/discord/debounce.d.ts.map +1 -0
  38. package/dist/channels/discord/debounce.js +20 -0
  39. package/dist/channels/discord/debounce.js.map +1 -0
  40. package/dist/channels/discord/delivery.d.ts +4 -1
  41. package/dist/channels/discord/delivery.d.ts.map +1 -1
  42. package/dist/channels/discord/delivery.js +19 -3
  43. package/dist/channels/discord/delivery.js.map +1 -1
  44. package/dist/channels/discord/human-delay.d.ts +16 -0
  45. package/dist/channels/discord/human-delay.d.ts.map +1 -0
  46. package/dist/channels/discord/human-delay.js +29 -0
  47. package/dist/channels/discord/human-delay.js.map +1 -0
  48. package/dist/channels/discord/inbound.d.ts +4 -0
  49. package/dist/channels/discord/inbound.d.ts.map +1 -1
  50. package/dist/channels/discord/inbound.js +45 -4
  51. package/dist/channels/discord/inbound.js.map +1 -1
  52. package/dist/channels/discord/mentions.d.ts.map +1 -1
  53. package/dist/channels/discord/mentions.js +16 -4
  54. package/dist/channels/discord/mentions.js.map +1 -1
  55. package/dist/channels/discord/presence.d.ts +33 -0
  56. package/dist/channels/discord/presence.d.ts.map +1 -0
  57. package/dist/channels/discord/presence.js +111 -0
  58. package/dist/channels/discord/presence.js.map +1 -0
  59. package/dist/channels/discord/rate-limiter.d.ts +14 -0
  60. package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
  61. package/dist/channels/discord/rate-limiter.js +49 -0
  62. package/dist/channels/discord/rate-limiter.js.map +1 -0
  63. package/dist/channels/discord/reactions.d.ts +38 -0
  64. package/dist/channels/discord/reactions.d.ts.map +1 -0
  65. package/dist/channels/discord/reactions.js +151 -0
  66. package/dist/channels/discord/reactions.js.map +1 -0
  67. package/dist/channels/discord/runtime.d.ts +6 -3
  68. package/dist/channels/discord/runtime.d.ts.map +1 -1
  69. package/dist/channels/discord/runtime.js +621 -125
  70. package/dist/channels/discord/runtime.js.map +1 -1
  71. package/dist/channels/discord/stream.d.ts +4 -1
  72. package/dist/channels/discord/stream.d.ts.map +1 -1
  73. package/dist/channels/discord/stream.js +16 -8
  74. package/dist/channels/discord/stream.js.map +1 -1
  75. package/dist/channels/discord/tool-actions.d.ts.map +1 -1
  76. package/dist/channels/discord/tool-actions.js +24 -12
  77. package/dist/channels/discord/tool-actions.js.map +1 -1
  78. package/dist/channels/discord/typing.d.ts +15 -0
  79. package/dist/channels/discord/typing.d.ts.map +1 -0
  80. package/dist/channels/discord/typing.js +106 -0
  81. package/dist/channels/discord/typing.js.map +1 -0
  82. package/dist/chunk.d.ts.map +1 -1
  83. package/dist/chunk.js +4 -2
  84. package/dist/chunk.js.map +1 -1
  85. package/dist/cli.js +47 -22
  86. package/dist/cli.js.map +1 -1
  87. package/dist/config.d.ts +19 -0
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +103 -18
  90. package/dist/config.js.map +1 -1
  91. package/dist/container-runner.d.ts.map +1 -1
  92. package/dist/container-runner.js +58 -26
  93. package/dist/container-runner.js.map +1 -1
  94. package/dist/container-setup.d.ts.map +1 -1
  95. package/dist/container-setup.js +10 -9
  96. package/dist/container-setup.js.map +1 -1
  97. package/dist/conversation.d.ts +2 -2
  98. package/dist/conversation.d.ts.map +1 -1
  99. package/dist/conversation.js +1 -1
  100. package/dist/conversation.js.map +1 -1
  101. package/dist/db.d.ts +118 -2
  102. package/dist/db.d.ts.map +1 -1
  103. package/dist/db.js +1568 -50
  104. package/dist/db.js.map +1 -1
  105. package/dist/delegation-manager.d.ts.map +1 -1
  106. package/dist/delegation-manager.js +3 -2
  107. package/dist/delegation-manager.js.map +1 -1
  108. package/dist/gateway-client.d.ts +2 -2
  109. package/dist/gateway-client.d.ts.map +1 -1
  110. package/dist/gateway-client.js +10 -4
  111. package/dist/gateway-client.js.map +1 -1
  112. package/dist/gateway-service.d.ts +3 -3
  113. package/dist/gateway-service.d.ts.map +1 -1
  114. package/dist/gateway-service.js +563 -73
  115. package/dist/gateway-service.js.map +1 -1
  116. package/dist/gateway-types.d.ts +24 -0
  117. package/dist/gateway-types.d.ts.map +1 -1
  118. package/dist/gateway-types.js.map +1 -1
  119. package/dist/gateway.js +179 -24
  120. package/dist/gateway.js.map +1 -1
  121. package/dist/health.d.ts.map +1 -1
  122. package/dist/health.js +20 -10
  123. package/dist/health.js.map +1 -1
  124. package/dist/heartbeat.d.ts +4 -0
  125. package/dist/heartbeat.d.ts.map +1 -1
  126. package/dist/heartbeat.js +48 -20
  127. package/dist/heartbeat.js.map +1 -1
  128. package/dist/hybridai-bots.d.ts.map +1 -1
  129. package/dist/hybridai-bots.js +4 -2
  130. package/dist/hybridai-bots.js.map +1 -1
  131. package/dist/instruction-approval-audit.d.ts.map +1 -1
  132. package/dist/instruction-approval-audit.js.map +1 -1
  133. package/dist/instruction-integrity.d.ts.map +1 -1
  134. package/dist/instruction-integrity.js +8 -2
  135. package/dist/instruction-integrity.js.map +1 -1
  136. package/dist/ipc.d.ts.map +1 -1
  137. package/dist/ipc.js +6 -1
  138. package/dist/ipc.js.map +1 -1
  139. package/dist/logger.js.map +1 -1
  140. package/dist/memory-consolidation.d.ts +17 -0
  141. package/dist/memory-consolidation.d.ts.map +1 -0
  142. package/dist/memory-consolidation.js +25 -0
  143. package/dist/memory-consolidation.js.map +1 -0
  144. package/dist/memory-service.d.ts +200 -0
  145. package/dist/memory-service.d.ts.map +1 -0
  146. package/dist/memory-service.js +294 -0
  147. package/dist/memory-service.js.map +1 -0
  148. package/dist/mount-security.d.ts.map +1 -1
  149. package/dist/mount-security.js +31 -7
  150. package/dist/mount-security.js.map +1 -1
  151. package/dist/observability-ingest.d.ts.map +1 -1
  152. package/dist/observability-ingest.js +32 -11
  153. package/dist/observability-ingest.js.map +1 -1
  154. package/dist/onboarding.d.ts.map +1 -1
  155. package/dist/onboarding.js +32 -9
  156. package/dist/onboarding.js.map +1 -1
  157. package/dist/proactive-policy.d.ts.map +1 -1
  158. package/dist/proactive-policy.js +2 -1
  159. package/dist/proactive-policy.js.map +1 -1
  160. package/dist/prompt-hooks.d.ts.map +1 -1
  161. package/dist/prompt-hooks.js +9 -7
  162. package/dist/prompt-hooks.js.map +1 -1
  163. package/dist/runtime-config.d.ts +98 -1
  164. package/dist/runtime-config.d.ts.map +1 -1
  165. package/dist/runtime-config.js +477 -23
  166. package/dist/runtime-config.js.map +1 -1
  167. package/dist/scheduled-task-runner.d.ts +1 -0
  168. package/dist/scheduled-task-runner.d.ts.map +1 -1
  169. package/dist/scheduled-task-runner.js +29 -10
  170. package/dist/scheduled-task-runner.js.map +1 -1
  171. package/dist/scheduler.d.ts +43 -4
  172. package/dist/scheduler.d.ts.map +1 -1
  173. package/dist/scheduler.js +530 -56
  174. package/dist/scheduler.js.map +1 -1
  175. package/dist/session-export.d.ts +26 -0
  176. package/dist/session-export.d.ts.map +1 -0
  177. package/dist/session-export.js +149 -0
  178. package/dist/session-export.js.map +1 -0
  179. package/dist/session-maintenance.d.ts.map +1 -1
  180. package/dist/session-maintenance.js +75 -13
  181. package/dist/session-maintenance.js.map +1 -1
  182. package/dist/session-transcripts.d.ts.map +1 -1
  183. package/dist/session-transcripts.js.map +1 -1
  184. package/dist/side-effects.d.ts.map +1 -1
  185. package/dist/side-effects.js +14 -2
  186. package/dist/side-effects.js.map +1 -1
  187. package/dist/skills-guard.d.ts.map +1 -1
  188. package/dist/skills-guard.js +893 -130
  189. package/dist/skills-guard.js.map +1 -1
  190. package/dist/skills.d.ts +5 -0
  191. package/dist/skills.d.ts.map +1 -1
  192. package/dist/skills.js +29 -15
  193. package/dist/skills.js.map +1 -1
  194. package/dist/token-efficiency.d.ts.map +1 -1
  195. package/dist/token-efficiency.js.map +1 -1
  196. package/dist/tui.js +92 -11
  197. package/dist/tui.js.map +1 -1
  198. package/dist/types.d.ts +146 -0
  199. package/dist/types.d.ts.map +1 -1
  200. package/dist/types.js +24 -1
  201. package/dist/types.js.map +1 -1
  202. package/dist/update.d.ts.map +1 -1
  203. package/dist/update.js +42 -14
  204. package/dist/update.js.map +1 -1
  205. package/dist/workspace.d.ts.map +1 -1
  206. package/dist/workspace.js +49 -9
  207. package/dist/workspace.js.map +1 -1
  208. package/docs/chat.html +9 -3
  209. package/docs/index.html +37 -13
  210. package/package.json +8 -2
  211. package/src/agent.ts +16 -3
  212. package/src/audit-cli.ts +44 -16
  213. package/src/audit-events.ts +69 -5
  214. package/src/audit-trail.ts +41 -15
  215. package/src/channels/discord/attachments.ts +81 -27
  216. package/src/channels/discord/debounce.ts +25 -0
  217. package/src/channels/discord/delivery.ts +57 -13
  218. package/src/channels/discord/human-delay.ts +48 -0
  219. package/src/channels/discord/inbound.ts +66 -7
  220. package/src/channels/discord/mentions.ts +42 -18
  221. package/src/channels/discord/presence.ts +148 -0
  222. package/src/channels/discord/rate-limiter.ts +58 -0
  223. package/src/channels/discord/reactions.ts +211 -0
  224. package/src/channels/discord/runtime.ts +1048 -182
  225. package/src/channels/discord/stream.ts +73 -27
  226. package/src/channels/discord/tool-actions.ts +78 -37
  227. package/src/channels/discord/typing.ts +140 -0
  228. package/src/chunk.ts +12 -4
  229. package/src/cli.ts +141 -56
  230. package/src/config.ts +192 -34
  231. package/src/container-runner.ts +132 -42
  232. package/src/container-setup.ts +57 -22
  233. package/src/conversation.ts +9 -7
  234. package/src/db.ts +2217 -84
  235. package/src/delegation-manager.ts +6 -2
  236. package/src/gateway-client.ts +41 -17
  237. package/src/gateway-service.ts +1019 -201
  238. package/src/gateway-types.ts +33 -0
  239. package/src/gateway.ts +321 -48
  240. package/src/health.ts +66 -26
  241. package/src/heartbeat.ts +84 -22
  242. package/src/hybridai-bots.ts +14 -5
  243. package/src/instruction-approval-audit.ts +4 -1
  244. package/src/instruction-integrity.ts +30 -9
  245. package/src/ipc.ts +23 -5
  246. package/src/logger.ts +4 -1
  247. package/src/memory-consolidation.ts +41 -0
  248. package/src/memory-service.ts +606 -0
  249. package/src/mount-security.ts +58 -13
  250. package/src/observability-ingest.ts +134 -35
  251. package/src/onboarding.ts +126 -35
  252. package/src/proactive-policy.ts +3 -1
  253. package/src/prompt-hooks.ts +40 -17
  254. package/src/runtime-config.ts +1114 -99
  255. package/src/scheduled-task-runner.ts +63 -11
  256. package/src/scheduler.ts +683 -60
  257. package/src/session-export.ts +196 -0
  258. package/src/session-maintenance.ts +125 -22
  259. package/src/session-transcripts.ts +12 -3
  260. package/src/side-effects.ts +28 -5
  261. package/src/skills-guard.ts +1067 -219
  262. package/src/skills.ts +163 -65
  263. package/src/token-efficiency.ts +31 -9
  264. package/src/tui.ts +166 -25
  265. package/src/types.ts +195 -2
  266. package/src/update.ts +79 -23
  267. package/src/workspace.ts +63 -11
  268. package/tests/approval-policy.test.ts +224 -0
  269. package/tests/discord.basic.test.ts +82 -2
  270. package/tests/discord.human-presence.test.ts +85 -0
  271. package/tests/gateway-service.media-routing.test.ts +8 -2
  272. package/tests/memory-service.test.ts +1114 -0
  273. package/tests/token-efficiency.basic.test.ts +8 -2
  274. package/vitest.e2e.config.ts +3 -1
  275. package/vitest.integration.config.ts +3 -1
  276. package/vitest.live.config.ts +3 -1
  277. package/vitest.unit.config.ts +9 -0
@@ -1,9 +1,17 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  export const CONFIG_FILE_NAME = 'config.json';
4
- export const CONFIG_VERSION = 3;
4
+ export const CONFIG_VERSION = 5;
5
5
  export const SECURITY_POLICY_VERSION = '2026-02-28';
6
- const KNOWN_LOG_LEVELS = new Set(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']);
6
+ const KNOWN_LOG_LEVELS = new Set([
7
+ 'fatal',
8
+ 'error',
9
+ 'warn',
10
+ 'info',
11
+ 'debug',
12
+ 'trace',
13
+ 'silent',
14
+ ]);
7
15
  const DEFAULT_RUNTIME_CONFIG = {
8
16
  version: CONFIG_VERSION,
9
17
  security: {
@@ -22,6 +30,43 @@ const DEFAULT_RUNTIME_CONFIG = {
22
30
  respondToAllMessages: false,
23
31
  commandsOnly: false,
24
32
  commandUserId: '',
33
+ groupPolicy: 'open',
34
+ freeResponseChannels: [],
35
+ humanDelay: {
36
+ mode: 'natural',
37
+ minMs: 800,
38
+ maxMs: 2_500,
39
+ },
40
+ typingMode: 'thinking',
41
+ presence: {
42
+ enabled: true,
43
+ intervalMs: 30_000,
44
+ healthyText: 'Watching the channels',
45
+ degradedText: 'Thinking slowly...',
46
+ exhaustedText: 'Taking a break',
47
+ activityType: 'watching',
48
+ },
49
+ lifecycleReactions: {
50
+ enabled: true,
51
+ removeOnComplete: true,
52
+ phases: {
53
+ queued: '⏳',
54
+ thinking: '🤔',
55
+ toolUse: '⚙️',
56
+ streaming: '✍️',
57
+ done: '✅',
58
+ error: '❌',
59
+ },
60
+ },
61
+ ackReaction: '👀',
62
+ ackReactionScope: 'group-mentions',
63
+ removeAckAfterReply: true,
64
+ debounceMs: 2_500,
65
+ rateLimitPerUser: 0,
66
+ rateLimitExemptRoles: [],
67
+ suppressPatterns: ['/stop', '/pause', 'brb', 'afk'],
68
+ maxConcurrentPerChannel: 2,
69
+ guilds: {},
25
70
  },
26
71
  hybridai: {
27
72
  baseUrl: 'https://hybridai.one',
@@ -44,6 +89,10 @@ const DEFAULT_RUNTIME_CONFIG = {
44
89
  intervalMs: 1_800_000,
45
90
  channel: '',
46
91
  },
92
+ memory: {
93
+ decayRate: 0.1,
94
+ consolidationIntervalHours: 24,
95
+ },
47
96
  ops: {
48
97
  healthHost: '127.0.0.1',
49
98
  healthPort: 9090,
@@ -67,7 +116,9 @@ const DEFAULT_RUNTIME_CONFIG = {
67
116
  },
68
117
  sessionCompaction: {
69
118
  enabled: true,
70
- threshold: 120,
119
+ tokenBudget: 100_000,
120
+ budgetRatio: 0.7,
121
+ threshold: 200,
71
122
  keepRecent: 40,
72
123
  summaryMaxChars: 8_000,
73
124
  preCompactionMemoryFlush: {
@@ -106,6 +157,9 @@ const DEFAULT_RUNTIME_CONFIG = {
106
157
  maxIterations: 0,
107
158
  },
108
159
  },
160
+ scheduler: {
161
+ jobs: [],
162
+ },
109
163
  };
110
164
  const CONFIG_PATH = path.join(process.cwd(), CONFIG_FILE_NAME);
111
165
  let currentConfig = cloneConfig(DEFAULT_RUNTIME_CONFIG);
@@ -164,6 +218,25 @@ function normalizeInteger(value, fallback, opts) {
164
218
  parsed = opts.max;
165
219
  return parsed;
166
220
  }
221
+ function normalizeNumber(value, fallback, opts) {
222
+ let parsed;
223
+ if (typeof value === 'number') {
224
+ parsed = value;
225
+ }
226
+ else if (typeof value === 'string' && value.trim()) {
227
+ parsed = Number.parseFloat(value);
228
+ }
229
+ else {
230
+ parsed = fallback;
231
+ }
232
+ if (!Number.isFinite(parsed))
233
+ parsed = fallback;
234
+ if (opts?.min != null && parsed < opts.min)
235
+ parsed = opts.min;
236
+ if (opts?.max != null && parsed > opts.max)
237
+ parsed = opts.max;
238
+ return parsed;
239
+ }
167
240
  function normalizeStringArray(value, fallback) {
168
241
  if (Array.isArray(value)) {
169
242
  const normalized = value
@@ -182,8 +255,337 @@ function normalizeStringArray(value, fallback) {
182
255
  }
183
256
  return fallback;
184
257
  }
258
+ function normalizeDiscordGroupPolicy(value, fallback) {
259
+ if (typeof value !== 'string')
260
+ return fallback;
261
+ const normalized = value.trim().toLowerCase();
262
+ if (normalized === 'open' ||
263
+ normalized === 'allowlist' ||
264
+ normalized === 'disabled') {
265
+ return normalized;
266
+ }
267
+ return fallback;
268
+ }
269
+ function normalizeDiscordChannelMode(value, fallback) {
270
+ if (typeof value !== 'string')
271
+ return fallback;
272
+ const normalized = value.trim().toLowerCase();
273
+ if (normalized === 'off' || normalized === 'mention' || normalized === 'free')
274
+ return normalized;
275
+ if (normalized === 'free-response' || normalized === 'free_response')
276
+ return 'free';
277
+ return fallback;
278
+ }
279
+ function normalizeDiscordTypingMode(value, fallback) {
280
+ if (typeof value !== 'string')
281
+ return fallback;
282
+ const normalized = value.trim().toLowerCase();
283
+ if (normalized === 'instant' ||
284
+ normalized === 'thinking' ||
285
+ normalized === 'streaming' ||
286
+ normalized === 'never') {
287
+ return normalized;
288
+ }
289
+ return fallback;
290
+ }
291
+ function normalizeDiscordHumanDelayMode(value, fallback) {
292
+ if (typeof value !== 'string')
293
+ return fallback;
294
+ const normalized = value.trim().toLowerCase();
295
+ if (normalized === 'off' ||
296
+ normalized === 'natural' ||
297
+ normalized === 'custom') {
298
+ return normalized;
299
+ }
300
+ return fallback;
301
+ }
302
+ function normalizeDiscordAckReactionScope(value, fallback) {
303
+ if (typeof value !== 'string')
304
+ return fallback;
305
+ const normalized = value.trim().toLowerCase();
306
+ if (normalized === 'all' ||
307
+ normalized === 'group-mentions' ||
308
+ normalized === 'direct' ||
309
+ normalized === 'off') {
310
+ return normalized;
311
+ }
312
+ return fallback;
313
+ }
314
+ function normalizeDiscordPresenceActivityType(value, fallback) {
315
+ if (typeof value !== 'string')
316
+ return fallback;
317
+ const normalized = value.trim().toLowerCase();
318
+ if (normalized === 'playing' ||
319
+ normalized === 'watching' ||
320
+ normalized === 'listening' ||
321
+ normalized === 'competing' ||
322
+ normalized === 'custom') {
323
+ return normalized;
324
+ }
325
+ return fallback;
326
+ }
327
+ function normalizeDiscordHumanDelayConfig(value, fallback) {
328
+ const raw = isRecord(value) ? value : {};
329
+ const mode = normalizeDiscordHumanDelayMode(raw.mode, fallback.mode);
330
+ const minMs = normalizeInteger(raw.minMs, fallback.minMs, {
331
+ min: 0,
332
+ max: 120_000,
333
+ });
334
+ const maxMsRaw = normalizeInteger(raw.maxMs, fallback.maxMs, {
335
+ min: 0,
336
+ max: 120_000,
337
+ });
338
+ const maxMs = Math.max(minMs, maxMsRaw);
339
+ return { mode, minMs, maxMs };
340
+ }
341
+ function normalizeDiscordPresenceConfig(value, fallback) {
342
+ const raw = isRecord(value) ? value : {};
343
+ return {
344
+ enabled: normalizeBoolean(raw.enabled, fallback.enabled),
345
+ intervalMs: normalizeInteger(raw.intervalMs, fallback.intervalMs, {
346
+ min: 5_000,
347
+ max: 300_000,
348
+ }),
349
+ healthyText: normalizeString(raw.healthyText, fallback.healthyText, {
350
+ allowEmpty: false,
351
+ }),
352
+ degradedText: normalizeString(raw.degradedText, fallback.degradedText, {
353
+ allowEmpty: false,
354
+ }),
355
+ exhaustedText: normalizeString(raw.exhaustedText, fallback.exhaustedText, {
356
+ allowEmpty: false,
357
+ }),
358
+ activityType: normalizeDiscordPresenceActivityType(raw.activityType, fallback.activityType),
359
+ };
360
+ }
361
+ function normalizeDiscordLifecycleReactionsConfig(value, fallback) {
362
+ const raw = isRecord(value) ? value : {};
363
+ const rawPhases = isRecord(raw.phases) ? raw.phases : {};
364
+ return {
365
+ enabled: normalizeBoolean(raw.enabled, fallback.enabled),
366
+ removeOnComplete: normalizeBoolean(raw.removeOnComplete, fallback.removeOnComplete),
367
+ phases: {
368
+ queued: normalizeString(rawPhases.queued, fallback.phases.queued, {
369
+ allowEmpty: false,
370
+ }),
371
+ thinking: normalizeString(rawPhases.thinking, fallback.phases.thinking, {
372
+ allowEmpty: false,
373
+ }),
374
+ toolUse: normalizeString(rawPhases.toolUse, fallback.phases.toolUse, {
375
+ allowEmpty: false,
376
+ }),
377
+ streaming: normalizeString(rawPhases.streaming, fallback.phases.streaming, { allowEmpty: false }),
378
+ done: normalizeString(rawPhases.done, fallback.phases.done, {
379
+ allowEmpty: false,
380
+ }),
381
+ error: normalizeString(rawPhases.error, fallback.phases.error, {
382
+ allowEmpty: false,
383
+ }),
384
+ },
385
+ };
386
+ }
387
+ function normalizeDiscordChannelConfig(value, fallback, defaultMode) {
388
+ const channelFallback = {
389
+ ...fallback,
390
+ mode: fallback.mode || defaultMode,
391
+ };
392
+ if (typeof value === 'string') {
393
+ return { mode: normalizeDiscordChannelMode(value, channelFallback.mode) };
394
+ }
395
+ if (!isRecord(value))
396
+ return null;
397
+ const channelConfig = {
398
+ mode: normalizeDiscordChannelMode(value.mode, channelFallback.mode),
399
+ };
400
+ if (value.typingMode !== undefined ||
401
+ channelFallback.typingMode !== undefined) {
402
+ channelConfig.typingMode = normalizeDiscordTypingMode(value.typingMode, channelFallback.typingMode ?? DEFAULT_RUNTIME_CONFIG.discord.typingMode);
403
+ }
404
+ if (value.debounceMs !== undefined ||
405
+ channelFallback.debounceMs !== undefined) {
406
+ channelConfig.debounceMs = normalizeInteger(value.debounceMs, channelFallback.debounceMs ?? DEFAULT_RUNTIME_CONFIG.discord.debounceMs, { min: 0, max: 120_000 });
407
+ }
408
+ if (value.ackReaction !== undefined ||
409
+ channelFallback.ackReaction !== undefined) {
410
+ channelConfig.ackReaction = normalizeString(value.ackReaction, channelFallback.ackReaction ?? DEFAULT_RUNTIME_CONFIG.discord.ackReaction, { allowEmpty: false });
411
+ }
412
+ if (value.ackReactionScope !== undefined ||
413
+ channelFallback.ackReactionScope !== undefined) {
414
+ channelConfig.ackReactionScope = normalizeDiscordAckReactionScope(value.ackReactionScope, channelFallback.ackReactionScope ??
415
+ DEFAULT_RUNTIME_CONFIG.discord.ackReactionScope);
416
+ }
417
+ if (value.removeAckAfterReply !== undefined ||
418
+ channelFallback.removeAckAfterReply !== undefined) {
419
+ channelConfig.removeAckAfterReply = normalizeBoolean(value.removeAckAfterReply, channelFallback.removeAckAfterReply ??
420
+ DEFAULT_RUNTIME_CONFIG.discord.removeAckAfterReply);
421
+ }
422
+ if (value.humanDelay !== undefined ||
423
+ channelFallback.humanDelay !== undefined) {
424
+ channelConfig.humanDelay = normalizeDiscordHumanDelayConfig(value.humanDelay, channelFallback.humanDelay ?? DEFAULT_RUNTIME_CONFIG.discord.humanDelay);
425
+ }
426
+ if (value.rateLimitPerUser !== undefined ||
427
+ channelFallback.rateLimitPerUser !== undefined) {
428
+ channelConfig.rateLimitPerUser = normalizeInteger(value.rateLimitPerUser, channelFallback.rateLimitPerUser ??
429
+ DEFAULT_RUNTIME_CONFIG.discord.rateLimitPerUser, { min: 0, max: 300 });
430
+ }
431
+ if (value.suppressPatterns !== undefined ||
432
+ channelFallback.suppressPatterns !== undefined) {
433
+ channelConfig.suppressPatterns = normalizeStringArray(value.suppressPatterns, channelFallback.suppressPatterns ??
434
+ DEFAULT_RUNTIME_CONFIG.discord.suppressPatterns);
435
+ }
436
+ if (value.maxConcurrentPerChannel !== undefined ||
437
+ channelFallback.maxConcurrentPerChannel !== undefined) {
438
+ channelConfig.maxConcurrentPerChannel = normalizeInteger(value.maxConcurrentPerChannel, channelFallback.maxConcurrentPerChannel ??
439
+ DEFAULT_RUNTIME_CONFIG.discord.maxConcurrentPerChannel, { min: 1, max: 16 });
440
+ }
441
+ return channelConfig;
442
+ }
443
+ function normalizeDiscordGuildConfig(value, fallback) {
444
+ if (!isRecord(value))
445
+ return fallback;
446
+ const defaultMode = normalizeDiscordChannelMode(value.defaultMode, fallback.defaultMode);
447
+ const rawChannels = isRecord(value.channels) ? value.channels : {};
448
+ const channels = {};
449
+ for (const [rawChannelId, rawChannelConfig] of Object.entries(rawChannels)) {
450
+ const channelId = rawChannelId.trim();
451
+ if (!channelId)
452
+ continue;
453
+ const fallbackChannel = fallback.channels[channelId] ?? {
454
+ mode: defaultMode,
455
+ };
456
+ const channelConfig = normalizeDiscordChannelConfig(rawChannelConfig, fallbackChannel, defaultMode);
457
+ if (!channelConfig)
458
+ continue;
459
+ channels[channelId] = channelConfig;
460
+ }
461
+ return { defaultMode, channels };
462
+ }
463
+ function normalizeDiscordGuildMap(value, fallback) {
464
+ if (!isRecord(value))
465
+ return fallback;
466
+ const guilds = {};
467
+ for (const [rawGuildId, rawGuildConfig] of Object.entries(value)) {
468
+ const guildId = rawGuildId.trim();
469
+ if (!guildId)
470
+ continue;
471
+ const fallbackGuild = fallback[guildId] ?? {
472
+ defaultMode: 'mention',
473
+ channels: {},
474
+ };
475
+ guilds[guildId] = normalizeDiscordGuildConfig(rawGuildConfig, fallbackGuild);
476
+ }
477
+ return guilds;
478
+ }
479
+ function normalizeSchedulerScheduleKind(value, fallback) {
480
+ if (typeof value !== 'string')
481
+ return fallback;
482
+ const normalized = value.trim().toLowerCase();
483
+ if (normalized === 'at' || normalized === 'every' || normalized === 'cron')
484
+ return normalized;
485
+ return fallback;
486
+ }
487
+ function normalizeSchedulerActionKind(value, fallback) {
488
+ if (typeof value !== 'string')
489
+ return fallback;
490
+ const normalized = value.trim().toLowerCase();
491
+ if (normalized === 'agent_turn' || normalized === 'system_event')
492
+ return normalized;
493
+ return fallback;
494
+ }
495
+ function normalizeSchedulerDeliveryKind(value, fallback) {
496
+ if (typeof value !== 'string')
497
+ return fallback;
498
+ const normalized = value.trim().toLowerCase();
499
+ if (normalized === 'channel' ||
500
+ normalized === 'last-channel' ||
501
+ normalized === 'webhook')
502
+ return normalized;
503
+ return fallback;
504
+ }
505
+ function normalizeSchedulerJobList(value, fallback) {
506
+ if (!Array.isArray(value))
507
+ return fallback;
508
+ const jobs = [];
509
+ for (const item of value) {
510
+ if (!isRecord(item))
511
+ continue;
512
+ const jobId = normalizeString(item.id, '', { allowEmpty: false });
513
+ if (!jobId)
514
+ continue;
515
+ const rawSchedule = isRecord(item.schedule) ? item.schedule : {};
516
+ const rawAction = isRecord(item.action) ? item.action : {};
517
+ const rawDelivery = isRecord(item.delivery) ? item.delivery : {};
518
+ const scheduleKind = normalizeSchedulerScheduleKind(rawSchedule.kind, 'cron');
519
+ const everyMs = scheduleKind === 'every'
520
+ ? normalizeInteger(rawSchedule.everyMs, 60_000, {
521
+ min: 10_000,
522
+ max: 86_400_000,
523
+ })
524
+ : null;
525
+ const atRaw = scheduleKind === 'at'
526
+ ? normalizeString(rawSchedule.at, '', { allowEmpty: false })
527
+ : '';
528
+ const atIso = scheduleKind === 'at'
529
+ ? (() => {
530
+ const parsed = new Date(atRaw);
531
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
532
+ })()
533
+ : null;
534
+ const expr = scheduleKind === 'cron'
535
+ ? normalizeString(rawSchedule.expr, '', { allowEmpty: false })
536
+ : '';
537
+ if (scheduleKind === 'at' && !atIso)
538
+ continue;
539
+ if (scheduleKind === 'cron' && !expr)
540
+ continue;
541
+ const deliveryKind = normalizeSchedulerDeliveryKind(rawDelivery.kind, 'channel');
542
+ const to = normalizeString(rawDelivery.to, '', { allowEmpty: true });
543
+ const webhookUrl = normalizeString(rawDelivery.webhookUrl ?? rawDelivery.url, '', { allowEmpty: true });
544
+ if (deliveryKind === 'channel' && !to)
545
+ continue;
546
+ if (deliveryKind === 'webhook' && !webhookUrl)
547
+ continue;
548
+ const actionMessage = normalizeString(rawAction.message, '', {
549
+ allowEmpty: false,
550
+ });
551
+ if (!actionMessage)
552
+ continue;
553
+ const name = normalizeString(item.name, '', { allowEmpty: true });
554
+ const description = normalizeString(item.description, '', {
555
+ allowEmpty: true,
556
+ });
557
+ jobs.push({
558
+ id: jobId,
559
+ ...(name ? { name } : {}),
560
+ ...(description ? { description } : {}),
561
+ schedule: {
562
+ kind: scheduleKind,
563
+ at: atIso,
564
+ everyMs,
565
+ expr: scheduleKind === 'cron' ? expr : null,
566
+ tz: normalizeString(rawSchedule.tz, '', { allowEmpty: true }),
567
+ },
568
+ action: {
569
+ kind: normalizeSchedulerActionKind(rawAction.kind, 'agent_turn'),
570
+ message: actionMessage,
571
+ },
572
+ delivery: {
573
+ kind: deliveryKind,
574
+ channel: normalizeString(rawDelivery.channel, 'discord', {
575
+ allowEmpty: true,
576
+ }),
577
+ to,
578
+ webhookUrl,
579
+ },
580
+ enabled: normalizeBoolean(item.enabled, true),
581
+ });
582
+ }
583
+ return jobs;
584
+ }
185
585
  function normalizeLogLevel(value, fallback) {
186
- const normalized = normalizeString(value, fallback, { allowEmpty: false }).toLowerCase();
586
+ const normalized = normalizeString(value, fallback, {
587
+ allowEmpty: false,
588
+ }).toLowerCase();
187
589
  if (KNOWN_LOG_LEVELS.has(normalized))
188
590
  return normalized;
189
591
  return fallback;
@@ -193,7 +595,10 @@ function normalizeBaseUrl(value, fallback) {
193
595
  return candidate.replace(/\/+$/, '') || fallback;
194
596
  }
195
597
  function normalizeApiPath(value, fallback) {
196
- const normalized = normalizeString(value, fallback, { allowEmpty: false, trim: true });
598
+ const normalized = normalizeString(value, fallback, {
599
+ allowEmpty: false,
600
+ trim: true,
601
+ });
197
602
  if (/^https?:\/\//i.test(normalized)) {
198
603
  return normalized.replace(/\/+$/, '');
199
604
  }
@@ -214,24 +619,36 @@ function normalizeRuntimeConfig(patch) {
214
619
  const rawHybridAi = isRecord(raw.hybridai) ? raw.hybridai : {};
215
620
  const rawContainer = isRecord(raw.container) ? raw.container : {};
216
621
  const rawHeartbeat = isRecord(raw.heartbeat) ? raw.heartbeat : {};
622
+ const rawMemory = isRecord(raw.memory) ? raw.memory : {};
217
623
  const rawOps = isRecord(raw.ops) ? raw.ops : {};
218
624
  const rawObservability = isRecord(raw.observability) ? raw.observability : {};
219
- const rawSessionCompaction = isRecord(raw.sessionCompaction) ? raw.sessionCompaction : {};
625
+ const rawSessionCompaction = isRecord(raw.sessionCompaction)
626
+ ? raw.sessionCompaction
627
+ : {};
220
628
  const rawPreFlush = isRecord(rawSessionCompaction.preCompactionMemoryFlush)
221
629
  ? rawSessionCompaction.preCompactionMemoryFlush
222
630
  : {};
223
631
  const rawPromptHooks = isRecord(raw.promptHooks) ? raw.promptHooks : {};
224
632
  const rawProactive = isRecord(raw.proactive) ? raw.proactive : {};
225
- const rawActiveHours = isRecord(rawProactive.activeHours) ? rawProactive.activeHours : {};
226
- const rawDelegation = isRecord(rawProactive.delegation) ? rawProactive.delegation : {};
227
- const rawAutoRetry = isRecord(rawProactive.autoRetry) ? rawProactive.autoRetry : {};
633
+ const rawActiveHours = isRecord(rawProactive.activeHours)
634
+ ? rawProactive.activeHours
635
+ : {};
636
+ const rawDelegation = isRecord(rawProactive.delegation)
637
+ ? rawProactive.delegation
638
+ : {};
639
+ const rawAutoRetry = isRecord(rawProactive.autoRetry)
640
+ ? rawProactive.autoRetry
641
+ : {};
228
642
  const rawRalph = isRecord(rawProactive.ralph) ? rawProactive.ralph : {};
643
+ const rawScheduler = isRecord(raw.scheduler) ? raw.scheduler : {};
229
644
  const defaultOps = DEFAULT_RUNTIME_CONFIG.ops;
230
645
  const healthPort = normalizeInteger(rawOps.healthPort, defaultOps.healthPort, { min: 1, max: 65_535 });
231
646
  const webApiToken = normalizeString(rawOps.webApiToken, defaultOps.webApiToken, { allowEmpty: true });
232
647
  const hybridBaseUrl = normalizeBaseUrl(rawHybridAi.baseUrl, DEFAULT_RUNTIME_CONFIG.hybridai.baseUrl);
233
648
  const hybridDefaultChatbotId = normalizeString(rawHybridAi.defaultChatbotId, DEFAULT_RUNTIME_CONFIG.hybridai.defaultChatbotId, { allowEmpty: true });
234
649
  const threshold = normalizeInteger(rawSessionCompaction.threshold, DEFAULT_RUNTIME_CONFIG.sessionCompaction.threshold, { min: 20 });
650
+ const tokenBudget = normalizeInteger(rawSessionCompaction.tokenBudget, DEFAULT_RUNTIME_CONFIG.sessionCompaction.tokenBudget, { min: 1_000 });
651
+ const budgetRatio = normalizeNumber(rawSessionCompaction.budgetRatio, DEFAULT_RUNTIME_CONFIG.sessionCompaction.budgetRatio, { min: 0.05, max: 1 });
235
652
  const keepRecentRaw = normalizeInteger(rawSessionCompaction.keepRecent, DEFAULT_RUNTIME_CONFIG.sessionCompaction.keepRecent, { min: 1 });
236
653
  const keepRecent = Math.min(keepRecentRaw, Math.max(1, threshold - 1));
237
654
  const modelList = normalizeStringArray(rawHybridAi.models, DEFAULT_RUNTIME_CONFIG.hybridai.models);
@@ -253,6 +670,21 @@ function normalizeRuntimeConfig(patch) {
253
670
  respondToAllMessages: normalizeBoolean(rawDiscord.respondToAllMessages, DEFAULT_RUNTIME_CONFIG.discord.respondToAllMessages),
254
671
  commandsOnly: normalizeBoolean(rawDiscord.commandsOnly, DEFAULT_RUNTIME_CONFIG.discord.commandsOnly),
255
672
  commandUserId: normalizeString(rawDiscord.commandUserId, DEFAULT_RUNTIME_CONFIG.discord.commandUserId, { allowEmpty: true }),
673
+ groupPolicy: normalizeDiscordGroupPolicy(rawDiscord.groupPolicy, DEFAULT_RUNTIME_CONFIG.discord.groupPolicy),
674
+ freeResponseChannels: normalizeStringArray(rawDiscord.freeResponseChannels, DEFAULT_RUNTIME_CONFIG.discord.freeResponseChannels),
675
+ humanDelay: normalizeDiscordHumanDelayConfig(rawDiscord.humanDelay, DEFAULT_RUNTIME_CONFIG.discord.humanDelay),
676
+ typingMode: normalizeDiscordTypingMode(rawDiscord.typingMode, DEFAULT_RUNTIME_CONFIG.discord.typingMode),
677
+ presence: normalizeDiscordPresenceConfig(rawDiscord.presence, DEFAULT_RUNTIME_CONFIG.discord.presence),
678
+ lifecycleReactions: normalizeDiscordLifecycleReactionsConfig(rawDiscord.lifecycleReactions, DEFAULT_RUNTIME_CONFIG.discord.lifecycleReactions),
679
+ ackReaction: normalizeString(rawDiscord.ackReaction, DEFAULT_RUNTIME_CONFIG.discord.ackReaction, { allowEmpty: false }),
680
+ ackReactionScope: normalizeDiscordAckReactionScope(rawDiscord.ackReactionScope, DEFAULT_RUNTIME_CONFIG.discord.ackReactionScope),
681
+ removeAckAfterReply: normalizeBoolean(rawDiscord.removeAckAfterReply, DEFAULT_RUNTIME_CONFIG.discord.removeAckAfterReply),
682
+ debounceMs: normalizeInteger(rawDiscord.debounceMs, DEFAULT_RUNTIME_CONFIG.discord.debounceMs, { min: 0, max: 120_000 }),
683
+ rateLimitPerUser: normalizeInteger(rawDiscord.rateLimitPerUser, DEFAULT_RUNTIME_CONFIG.discord.rateLimitPerUser, { min: 0, max: 300 }),
684
+ rateLimitExemptRoles: normalizeStringArray(rawDiscord.rateLimitExemptRoles, DEFAULT_RUNTIME_CONFIG.discord.rateLimitExemptRoles),
685
+ suppressPatterns: normalizeStringArray(rawDiscord.suppressPatterns, DEFAULT_RUNTIME_CONFIG.discord.suppressPatterns),
686
+ maxConcurrentPerChannel: normalizeInteger(rawDiscord.maxConcurrentPerChannel, DEFAULT_RUNTIME_CONFIG.discord.maxConcurrentPerChannel, { min: 1, max: 16 }),
687
+ guilds: normalizeDiscordGuildMap(rawDiscord.guilds, DEFAULT_RUNTIME_CONFIG.discord.guilds),
256
688
  },
257
689
  hybridai: {
258
690
  baseUrl: hybridBaseUrl,
@@ -275,13 +707,23 @@ function normalizeRuntimeConfig(patch) {
275
707
  intervalMs: normalizeInteger(rawHeartbeat.intervalMs, DEFAULT_RUNTIME_CONFIG.heartbeat.intervalMs, { min: 10_000 }),
276
708
  channel: normalizeString(rawHeartbeat.channel, DEFAULT_RUNTIME_CONFIG.heartbeat.channel, { allowEmpty: true }),
277
709
  },
710
+ memory: {
711
+ decayRate: normalizeNumber(rawMemory.decayRate, DEFAULT_RUNTIME_CONFIG.memory.decayRate, { min: 0, max: 0.95 }),
712
+ consolidationIntervalHours: normalizeInteger(rawMemory.consolidationIntervalHours, DEFAULT_RUNTIME_CONFIG.memory.consolidationIntervalHours, { min: 0, max: 24 * 30 }),
713
+ },
278
714
  ops: {
279
- healthHost: normalizeString(rawOps.healthHost, defaultOps.healthHost, { allowEmpty: false }),
715
+ healthHost: normalizeString(rawOps.healthHost, defaultOps.healthHost, {
716
+ allowEmpty: false,
717
+ }),
280
718
  healthPort,
281
719
  webApiToken,
282
720
  gatewayBaseUrl: normalizeBaseUrl(rawOps.gatewayBaseUrl, `http://127.0.0.1:${healthPort}`),
283
- gatewayApiToken: normalizeString(rawOps.gatewayApiToken, webApiToken, { allowEmpty: true }),
284
- dbPath: normalizeString(rawOps.dbPath, defaultOps.dbPath, { allowEmpty: false }),
721
+ gatewayApiToken: normalizeString(rawOps.gatewayApiToken, webApiToken, {
722
+ allowEmpty: true,
723
+ }),
724
+ dbPath: normalizeString(rawOps.dbPath, defaultOps.dbPath, {
725
+ allowEmpty: false,
726
+ }),
285
727
  logLevel: normalizeLogLevel(rawOps.logLevel, defaultOps.logLevel),
286
728
  },
287
729
  observability: {
@@ -289,7 +731,9 @@ function normalizeRuntimeConfig(patch) {
289
731
  baseUrl: normalizeBaseUrl(rawObservability.baseUrl, hybridBaseUrl),
290
732
  ingestPath: normalizeApiPath(rawObservability.ingestPath, DEFAULT_RUNTIME_CONFIG.observability.ingestPath),
291
733
  statusPath: normalizeApiPath(rawObservability.statusPath, DEFAULT_RUNTIME_CONFIG.observability.statusPath),
292
- botId: normalizeString(rawObservability.botId, hybridDefaultChatbotId, { allowEmpty: true }),
734
+ botId: normalizeString(rawObservability.botId, hybridDefaultChatbotId, {
735
+ allowEmpty: true,
736
+ }),
293
737
  agentId: normalizeString(rawObservability.agentId, DEFAULT_RUNTIME_CONFIG.observability.agentId, { allowEmpty: false }),
294
738
  label: normalizeString(rawObservability.label, DEFAULT_RUNTIME_CONFIG.observability.label, { allowEmpty: true }),
295
739
  environment: normalizeString(rawObservability.environment, DEFAULT_RUNTIME_CONFIG.observability.environment, { allowEmpty: false }),
@@ -298,13 +742,18 @@ function normalizeRuntimeConfig(patch) {
298
742
  },
299
743
  sessionCompaction: {
300
744
  enabled: normalizeBoolean(rawSessionCompaction.enabled, DEFAULT_RUNTIME_CONFIG.sessionCompaction.enabled),
745
+ tokenBudget,
746
+ budgetRatio,
301
747
  threshold,
302
748
  keepRecent,
303
749
  summaryMaxChars: normalizeInteger(rawSessionCompaction.summaryMaxChars, DEFAULT_RUNTIME_CONFIG.sessionCompaction.summaryMaxChars, { min: 1_000 }),
304
750
  preCompactionMemoryFlush: {
305
- enabled: normalizeBoolean(rawPreFlush.enabled, DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush.enabled),
306
- maxMessages: normalizeInteger(rawPreFlush.maxMessages, DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush.maxMessages, { min: 8 }),
307
- maxChars: normalizeInteger(rawPreFlush.maxChars, DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush.maxChars, { min: 4_000 }),
751
+ enabled: normalizeBoolean(rawPreFlush.enabled, DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush
752
+ .enabled),
753
+ maxMessages: normalizeInteger(rawPreFlush.maxMessages, DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush
754
+ .maxMessages, { min: 8 }),
755
+ maxChars: normalizeInteger(rawPreFlush.maxChars, DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush
756
+ .maxChars, { min: 4_000 }),
308
757
  },
309
758
  },
310
759
  promptHooks: {
@@ -337,6 +786,9 @@ function normalizeRuntimeConfig(patch) {
337
786
  maxIterations: normalizeInteger(rawRalph.maxIterations, DEFAULT_RUNTIME_CONFIG.proactive.ralph.maxIterations, { min: -1, max: 64 }),
338
787
  },
339
788
  },
789
+ scheduler: {
790
+ jobs: normalizeSchedulerJobList(rawScheduler.jobs, DEFAULT_RUNTIME_CONFIG.scheduler.jobs),
791
+ },
340
792
  };
341
793
  }
342
794
  function loadConfigPatchFromDisk() {
@@ -397,7 +849,7 @@ function scheduleWatcherRestart(reason) {
397
849
  return;
398
850
  }
399
851
  watcherRetryAttempt += 1;
400
- const delay = Math.min(WATCHER_RETRY_BASE_DELAY_MS * (2 ** (watcherRetryAttempt - 1)), WATCHER_RETRY_MAX_DELAY_MS);
852
+ const delay = Math.min(WATCHER_RETRY_BASE_DELAY_MS * 2 ** (watcherRetryAttempt - 1), WATCHER_RETRY_MAX_DELAY_MS);
401
853
  console.warn(`[runtime-config] watcher restart in ${delay}ms (attempt ${watcherRetryAttempt}/${WATCHER_RETRY_MAX_ATTEMPTS})`);
402
854
  watcherRestartTimer = setTimeout(() => {
403
855
  watcherRestartTimer = null;
@@ -521,13 +973,15 @@ export function updateRuntimeConfig(mutator) {
521
973
  return saveRuntimeConfig(draft);
522
974
  }
523
975
  export function isSecurityTrustAccepted(config = currentConfig) {
524
- return Boolean(config.security.trustModelAccepted
525
- && config.security.trustModelAcceptedAt
526
- && config.security.trustModelVersion === SECURITY_POLICY_VERSION);
976
+ return Boolean(config.security.trustModelAccepted &&
977
+ config.security.trustModelAcceptedAt &&
978
+ config.security.trustModelVersion === SECURITY_POLICY_VERSION);
527
979
  }
528
980
  export function acceptSecurityTrustModel(params) {
529
981
  const acceptedAt = normalizeString(params?.acceptedAt, new Date().toISOString(), { allowEmpty: false });
530
- const acceptedBy = normalizeString(params?.acceptedBy ?? '', '', { allowEmpty: true });
982
+ const acceptedBy = normalizeString(params?.acceptedBy ?? '', '', {
983
+ allowEmpty: true,
984
+ });
531
985
  const policyVersion = normalizeString(params?.policyVersion, SECURITY_POLICY_VERSION, { allowEmpty: false });
532
986
  return updateRuntimeConfig((draft) => {
533
987
  draft.security.trustModelAccepted = true;