@hybridaione/hybridclaw 0.2.3 → 0.2.7

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