@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,13 +1,28 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
 
4
4
  export const CONFIG_FILE_NAME = 'config.json';
5
- export const CONFIG_VERSION = 3;
5
+ export const CONFIG_VERSION = 5;
6
6
  export const SECURITY_POLICY_VERSION = '2026-02-28';
7
7
 
8
- const KNOWN_LOG_LEVELS = new Set(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']);
8
+ const KNOWN_LOG_LEVELS = new Set([
9
+ 'fatal',
10
+ 'error',
11
+ 'warn',
12
+ 'info',
13
+ 'debug',
14
+ 'trace',
15
+ 'silent',
16
+ ]);
9
17
 
10
- type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
18
+ type LogLevel =
19
+ | 'fatal'
20
+ | 'error'
21
+ | 'warn'
22
+ | 'info'
23
+ | 'debug'
24
+ | 'trace'
25
+ | 'silent';
11
26
 
12
27
  type DeepPartial<T> = {
13
28
  [K in keyof T]?: T[K] extends Array<infer U>
@@ -24,6 +39,95 @@ export interface RuntimeSecurityConfig {
24
39
  trustModelAcceptedBy: string;
25
40
  }
26
41
 
42
+ export type DiscordGroupPolicy = 'open' | 'allowlist' | 'disabled';
43
+ export type DiscordChannelMode = 'off' | 'mention' | 'free';
44
+ export type DiscordTypingMode = 'instant' | 'thinking' | 'streaming' | 'never';
45
+ export type DiscordHumanDelayMode = 'off' | 'natural' | 'custom';
46
+ export type DiscordAckReactionScope =
47
+ | 'all'
48
+ | 'group-mentions'
49
+ | 'direct'
50
+ | 'off';
51
+ export type DiscordPresenceActivityType =
52
+ | 'playing'
53
+ | 'watching'
54
+ | 'listening'
55
+ | 'competing'
56
+ | 'custom';
57
+ export type SchedulerScheduleKind = 'at' | 'every' | 'cron';
58
+ export type SchedulerActionKind = 'agent_turn' | 'system_event';
59
+ export type SchedulerDeliveryKind = 'channel' | 'last-channel' | 'webhook';
60
+
61
+ export interface RuntimeDiscordHumanDelayConfig {
62
+ mode: DiscordHumanDelayMode;
63
+ minMs: number;
64
+ maxMs: number;
65
+ }
66
+
67
+ export interface RuntimeDiscordPresenceConfig {
68
+ enabled: boolean;
69
+ intervalMs: number;
70
+ healthyText: string;
71
+ degradedText: string;
72
+ exhaustedText: string;
73
+ activityType: DiscordPresenceActivityType;
74
+ }
75
+
76
+ export interface RuntimeDiscordLifecycleReactionsConfig {
77
+ enabled: boolean;
78
+ removeOnComplete: boolean;
79
+ phases: {
80
+ queued: string;
81
+ thinking: string;
82
+ toolUse: string;
83
+ streaming: string;
84
+ done: string;
85
+ error: string;
86
+ };
87
+ }
88
+
89
+ export interface RuntimeDiscordChannelConfig {
90
+ mode: DiscordChannelMode;
91
+ typingMode?: DiscordTypingMode;
92
+ debounceMs?: number;
93
+ ackReaction?: string;
94
+ ackReactionScope?: DiscordAckReactionScope;
95
+ removeAckAfterReply?: boolean;
96
+ humanDelay?: RuntimeDiscordHumanDelayConfig;
97
+ rateLimitPerUser?: number;
98
+ suppressPatterns?: string[];
99
+ maxConcurrentPerChannel?: number;
100
+ }
101
+
102
+ export interface RuntimeDiscordGuildConfig {
103
+ defaultMode: DiscordChannelMode;
104
+ channels: Record<string, RuntimeDiscordChannelConfig>;
105
+ }
106
+
107
+ export interface RuntimeSchedulerJob {
108
+ id: string;
109
+ name?: string;
110
+ description?: string;
111
+ schedule: {
112
+ kind: SchedulerScheduleKind;
113
+ at: string | null;
114
+ everyMs: number | null;
115
+ expr: string | null;
116
+ tz: string;
117
+ };
118
+ action: {
119
+ kind: SchedulerActionKind;
120
+ message: string;
121
+ };
122
+ delivery: {
123
+ kind: SchedulerDeliveryKind;
124
+ channel: string;
125
+ to: string;
126
+ webhookUrl: string;
127
+ };
128
+ enabled: boolean;
129
+ }
130
+
27
131
  export interface RuntimeConfig {
28
132
  version: number;
29
133
  security: RuntimeSecurityConfig;
@@ -37,6 +141,21 @@ export interface RuntimeConfig {
37
141
  respondToAllMessages: boolean;
38
142
  commandsOnly: boolean;
39
143
  commandUserId: string;
144
+ groupPolicy: DiscordGroupPolicy;
145
+ freeResponseChannels: string[];
146
+ humanDelay: RuntimeDiscordHumanDelayConfig;
147
+ typingMode: DiscordTypingMode;
148
+ presence: RuntimeDiscordPresenceConfig;
149
+ lifecycleReactions: RuntimeDiscordLifecycleReactionsConfig;
150
+ ackReaction: string;
151
+ ackReactionScope: DiscordAckReactionScope;
152
+ removeAckAfterReply: boolean;
153
+ debounceMs: number;
154
+ rateLimitPerUser: number;
155
+ rateLimitExemptRoles: string[];
156
+ suppressPatterns: string[];
157
+ maxConcurrentPerChannel: number;
158
+ guilds: Record<string, RuntimeDiscordGuildConfig>;
40
159
  };
41
160
  hybridai: {
42
161
  baseUrl: string;
@@ -59,6 +178,10 @@ export interface RuntimeConfig {
59
178
  intervalMs: number;
60
179
  channel: string;
61
180
  };
181
+ memory: {
182
+ decayRate: number;
183
+ consolidationIntervalHours: number;
184
+ };
62
185
  ops: {
63
186
  healthHost: string;
64
187
  healthPort: number;
@@ -82,6 +205,8 @@ export interface RuntimeConfig {
82
205
  };
83
206
  sessionCompaction: {
84
207
  enabled: boolean;
208
+ tokenBudget: number;
209
+ budgetRatio: number;
85
210
  threshold: number;
86
211
  keepRecent: number;
87
212
  summaryMaxChars: number;
@@ -121,9 +246,15 @@ export interface RuntimeConfig {
121
246
  maxIterations: number;
122
247
  };
123
248
  };
249
+ scheduler: {
250
+ jobs: RuntimeSchedulerJob[];
251
+ };
124
252
  }
125
253
 
126
- export type RuntimeConfigChangeListener = (next: RuntimeConfig, prev: RuntimeConfig) => void;
254
+ export type RuntimeConfigChangeListener = (
255
+ next: RuntimeConfig,
256
+ prev: RuntimeConfig,
257
+ ) => void;
127
258
 
128
259
  const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
129
260
  version: CONFIG_VERSION,
@@ -143,6 +274,43 @@ const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
143
274
  respondToAllMessages: false,
144
275
  commandsOnly: false,
145
276
  commandUserId: '',
277
+ groupPolicy: 'open',
278
+ freeResponseChannels: [],
279
+ humanDelay: {
280
+ mode: 'natural',
281
+ minMs: 800,
282
+ maxMs: 2_500,
283
+ },
284
+ typingMode: 'thinking',
285
+ presence: {
286
+ enabled: true,
287
+ intervalMs: 30_000,
288
+ healthyText: 'Watching the channels',
289
+ degradedText: 'Thinking slowly...',
290
+ exhaustedText: 'Taking a break',
291
+ activityType: 'watching',
292
+ },
293
+ lifecycleReactions: {
294
+ enabled: true,
295
+ removeOnComplete: true,
296
+ phases: {
297
+ queued: '⏳',
298
+ thinking: '🤔',
299
+ toolUse: '⚙️',
300
+ streaming: '✍️',
301
+ done: '✅',
302
+ error: '❌',
303
+ },
304
+ },
305
+ ackReaction: '👀',
306
+ ackReactionScope: 'group-mentions',
307
+ removeAckAfterReply: true,
308
+ debounceMs: 2_500,
309
+ rateLimitPerUser: 0,
310
+ rateLimitExemptRoles: [],
311
+ suppressPatterns: ['/stop', '/pause', 'brb', 'afk'],
312
+ maxConcurrentPerChannel: 2,
313
+ guilds: {},
146
314
  },
147
315
  hybridai: {
148
316
  baseUrl: 'https://hybridai.one',
@@ -165,6 +333,10 @@ const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
165
333
  intervalMs: 1_800_000,
166
334
  channel: '',
167
335
  },
336
+ memory: {
337
+ decayRate: 0.1,
338
+ consolidationIntervalHours: 24,
339
+ },
168
340
  ops: {
169
341
  healthHost: '127.0.0.1',
170
342
  healthPort: 9090,
@@ -188,7 +360,9 @@ const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
188
360
  },
189
361
  sessionCompaction: {
190
362
  enabled: true,
191
- threshold: 120,
363
+ tokenBudget: 100_000,
364
+ budgetRatio: 0.7,
365
+ threshold: 200,
192
366
  keepRecent: 40,
193
367
  summaryMaxChars: 8_000,
194
368
  preCompactionMemoryFlush: {
@@ -227,6 +401,9 @@ const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
227
401
  maxIterations: 0,
228
402
  },
229
403
  },
404
+ scheduler: {
405
+ jobs: [],
406
+ },
230
407
  };
231
408
 
232
409
  const CONFIG_PATH = path.join(process.cwd(), CONFIG_FILE_NAME);
@@ -292,6 +469,26 @@ function normalizeInteger(
292
469
  return parsed;
293
470
  }
294
471
 
472
+ function normalizeNumber(
473
+ value: unknown,
474
+ fallback: number,
475
+ opts?: { min?: number; max?: number },
476
+ ): number {
477
+ let parsed: number;
478
+ if (typeof value === 'number') {
479
+ parsed = value;
480
+ } else if (typeof value === 'string' && value.trim()) {
481
+ parsed = Number.parseFloat(value);
482
+ } else {
483
+ parsed = fallback;
484
+ }
485
+
486
+ if (!Number.isFinite(parsed)) parsed = fallback;
487
+ if (opts?.min != null && parsed < opts.min) parsed = opts.min;
488
+ if (opts?.max != null && parsed > opts.max) parsed = opts.max;
489
+ return parsed;
490
+ }
491
+
295
492
  function normalizeStringArray(value: unknown, fallback: string[]): string[] {
296
493
  if (Array.isArray(value)) {
297
494
  const normalized = value
@@ -312,8 +509,483 @@ function normalizeStringArray(value: unknown, fallback: string[]): string[] {
312
509
  return fallback;
313
510
  }
314
511
 
512
+ function normalizeDiscordGroupPolicy(
513
+ value: unknown,
514
+ fallback: DiscordGroupPolicy,
515
+ ): DiscordGroupPolicy {
516
+ if (typeof value !== 'string') return fallback;
517
+ const normalized = value.trim().toLowerCase();
518
+ if (
519
+ normalized === 'open' ||
520
+ normalized === 'allowlist' ||
521
+ normalized === 'disabled'
522
+ ) {
523
+ return normalized;
524
+ }
525
+ return fallback;
526
+ }
527
+
528
+ function normalizeDiscordChannelMode(
529
+ value: unknown,
530
+ fallback: DiscordChannelMode,
531
+ ): DiscordChannelMode {
532
+ if (typeof value !== 'string') return fallback;
533
+ const normalized = value.trim().toLowerCase();
534
+ if (normalized === 'off' || normalized === 'mention' || normalized === 'free')
535
+ return normalized;
536
+ if (normalized === 'free-response' || normalized === 'free_response')
537
+ return 'free';
538
+ return fallback;
539
+ }
540
+
541
+ function normalizeDiscordTypingMode(
542
+ value: unknown,
543
+ fallback: DiscordTypingMode,
544
+ ): DiscordTypingMode {
545
+ if (typeof value !== 'string') return fallback;
546
+ const normalized = value.trim().toLowerCase();
547
+ if (
548
+ normalized === 'instant' ||
549
+ normalized === 'thinking' ||
550
+ normalized === 'streaming' ||
551
+ normalized === 'never'
552
+ ) {
553
+ return normalized;
554
+ }
555
+ return fallback;
556
+ }
557
+
558
+ function normalizeDiscordHumanDelayMode(
559
+ value: unknown,
560
+ fallback: DiscordHumanDelayMode,
561
+ ): DiscordHumanDelayMode {
562
+ if (typeof value !== 'string') return fallback;
563
+ const normalized = value.trim().toLowerCase();
564
+ if (
565
+ normalized === 'off' ||
566
+ normalized === 'natural' ||
567
+ normalized === 'custom'
568
+ ) {
569
+ return normalized;
570
+ }
571
+ return fallback;
572
+ }
573
+
574
+ function normalizeDiscordAckReactionScope(
575
+ value: unknown,
576
+ fallback: DiscordAckReactionScope,
577
+ ): DiscordAckReactionScope {
578
+ if (typeof value !== 'string') return fallback;
579
+ const normalized = value.trim().toLowerCase();
580
+ if (
581
+ normalized === 'all' ||
582
+ normalized === 'group-mentions' ||
583
+ normalized === 'direct' ||
584
+ normalized === 'off'
585
+ ) {
586
+ return normalized;
587
+ }
588
+ return fallback;
589
+ }
590
+
591
+ function normalizeDiscordPresenceActivityType(
592
+ value: unknown,
593
+ fallback: DiscordPresenceActivityType,
594
+ ): DiscordPresenceActivityType {
595
+ if (typeof value !== 'string') return fallback;
596
+ const normalized = value.trim().toLowerCase();
597
+ if (
598
+ normalized === 'playing' ||
599
+ normalized === 'watching' ||
600
+ normalized === 'listening' ||
601
+ normalized === 'competing' ||
602
+ normalized === 'custom'
603
+ ) {
604
+ return normalized;
605
+ }
606
+ return fallback;
607
+ }
608
+
609
+ function normalizeDiscordHumanDelayConfig(
610
+ value: unknown,
611
+ fallback: RuntimeDiscordHumanDelayConfig,
612
+ ): RuntimeDiscordHumanDelayConfig {
613
+ const raw = isRecord(value) ? value : {};
614
+ const mode = normalizeDiscordHumanDelayMode(raw.mode, fallback.mode);
615
+ const minMs = normalizeInteger(raw.minMs, fallback.minMs, {
616
+ min: 0,
617
+ max: 120_000,
618
+ });
619
+ const maxMsRaw = normalizeInteger(raw.maxMs, fallback.maxMs, {
620
+ min: 0,
621
+ max: 120_000,
622
+ });
623
+ const maxMs = Math.max(minMs, maxMsRaw);
624
+ return { mode, minMs, maxMs };
625
+ }
626
+
627
+ function normalizeDiscordPresenceConfig(
628
+ value: unknown,
629
+ fallback: RuntimeDiscordPresenceConfig,
630
+ ): RuntimeDiscordPresenceConfig {
631
+ const raw = isRecord(value) ? value : {};
632
+ return {
633
+ enabled: normalizeBoolean(raw.enabled, fallback.enabled),
634
+ intervalMs: normalizeInteger(raw.intervalMs, fallback.intervalMs, {
635
+ min: 5_000,
636
+ max: 300_000,
637
+ }),
638
+ healthyText: normalizeString(raw.healthyText, fallback.healthyText, {
639
+ allowEmpty: false,
640
+ }),
641
+ degradedText: normalizeString(raw.degradedText, fallback.degradedText, {
642
+ allowEmpty: false,
643
+ }),
644
+ exhaustedText: normalizeString(raw.exhaustedText, fallback.exhaustedText, {
645
+ allowEmpty: false,
646
+ }),
647
+ activityType: normalizeDiscordPresenceActivityType(
648
+ raw.activityType,
649
+ fallback.activityType,
650
+ ),
651
+ };
652
+ }
653
+
654
+ function normalizeDiscordLifecycleReactionsConfig(
655
+ value: unknown,
656
+ fallback: RuntimeDiscordLifecycleReactionsConfig,
657
+ ): RuntimeDiscordLifecycleReactionsConfig {
658
+ const raw = isRecord(value) ? value : {};
659
+ const rawPhases = isRecord(raw.phases) ? raw.phases : {};
660
+ return {
661
+ enabled: normalizeBoolean(raw.enabled, fallback.enabled),
662
+ removeOnComplete: normalizeBoolean(
663
+ raw.removeOnComplete,
664
+ fallback.removeOnComplete,
665
+ ),
666
+ phases: {
667
+ queued: normalizeString(rawPhases.queued, fallback.phases.queued, {
668
+ allowEmpty: false,
669
+ }),
670
+ thinking: normalizeString(rawPhases.thinking, fallback.phases.thinking, {
671
+ allowEmpty: false,
672
+ }),
673
+ toolUse: normalizeString(rawPhases.toolUse, fallback.phases.toolUse, {
674
+ allowEmpty: false,
675
+ }),
676
+ streaming: normalizeString(
677
+ rawPhases.streaming,
678
+ fallback.phases.streaming,
679
+ { allowEmpty: false },
680
+ ),
681
+ done: normalizeString(rawPhases.done, fallback.phases.done, {
682
+ allowEmpty: false,
683
+ }),
684
+ error: normalizeString(rawPhases.error, fallback.phases.error, {
685
+ allowEmpty: false,
686
+ }),
687
+ },
688
+ };
689
+ }
690
+
691
+ function normalizeDiscordChannelConfig(
692
+ value: unknown,
693
+ fallback: RuntimeDiscordChannelConfig,
694
+ defaultMode: DiscordChannelMode,
695
+ ): RuntimeDiscordChannelConfig | null {
696
+ const channelFallback = {
697
+ ...fallback,
698
+ mode: fallback.mode || defaultMode,
699
+ };
700
+
701
+ if (typeof value === 'string') {
702
+ return { mode: normalizeDiscordChannelMode(value, channelFallback.mode) };
703
+ }
704
+ if (!isRecord(value)) return null;
705
+
706
+ const channelConfig: RuntimeDiscordChannelConfig = {
707
+ mode: normalizeDiscordChannelMode(value.mode, channelFallback.mode),
708
+ };
709
+
710
+ if (
711
+ value.typingMode !== undefined ||
712
+ channelFallback.typingMode !== undefined
713
+ ) {
714
+ channelConfig.typingMode = normalizeDiscordTypingMode(
715
+ value.typingMode,
716
+ channelFallback.typingMode ?? DEFAULT_RUNTIME_CONFIG.discord.typingMode,
717
+ );
718
+ }
719
+ if (
720
+ value.debounceMs !== undefined ||
721
+ channelFallback.debounceMs !== undefined
722
+ ) {
723
+ channelConfig.debounceMs = normalizeInteger(
724
+ value.debounceMs,
725
+ channelFallback.debounceMs ?? DEFAULT_RUNTIME_CONFIG.discord.debounceMs,
726
+ { min: 0, max: 120_000 },
727
+ );
728
+ }
729
+ if (
730
+ value.ackReaction !== undefined ||
731
+ channelFallback.ackReaction !== undefined
732
+ ) {
733
+ channelConfig.ackReaction = normalizeString(
734
+ value.ackReaction,
735
+ channelFallback.ackReaction ?? DEFAULT_RUNTIME_CONFIG.discord.ackReaction,
736
+ { allowEmpty: false },
737
+ );
738
+ }
739
+ if (
740
+ value.ackReactionScope !== undefined ||
741
+ channelFallback.ackReactionScope !== undefined
742
+ ) {
743
+ channelConfig.ackReactionScope = normalizeDiscordAckReactionScope(
744
+ value.ackReactionScope,
745
+ channelFallback.ackReactionScope ??
746
+ DEFAULT_RUNTIME_CONFIG.discord.ackReactionScope,
747
+ );
748
+ }
749
+ if (
750
+ value.removeAckAfterReply !== undefined ||
751
+ channelFallback.removeAckAfterReply !== undefined
752
+ ) {
753
+ channelConfig.removeAckAfterReply = normalizeBoolean(
754
+ value.removeAckAfterReply,
755
+ channelFallback.removeAckAfterReply ??
756
+ DEFAULT_RUNTIME_CONFIG.discord.removeAckAfterReply,
757
+ );
758
+ }
759
+ if (
760
+ value.humanDelay !== undefined ||
761
+ channelFallback.humanDelay !== undefined
762
+ ) {
763
+ channelConfig.humanDelay = normalizeDiscordHumanDelayConfig(
764
+ value.humanDelay,
765
+ channelFallback.humanDelay ?? DEFAULT_RUNTIME_CONFIG.discord.humanDelay,
766
+ );
767
+ }
768
+ if (
769
+ value.rateLimitPerUser !== undefined ||
770
+ channelFallback.rateLimitPerUser !== undefined
771
+ ) {
772
+ channelConfig.rateLimitPerUser = normalizeInteger(
773
+ value.rateLimitPerUser,
774
+ channelFallback.rateLimitPerUser ??
775
+ DEFAULT_RUNTIME_CONFIG.discord.rateLimitPerUser,
776
+ { min: 0, max: 300 },
777
+ );
778
+ }
779
+ if (
780
+ value.suppressPatterns !== undefined ||
781
+ channelFallback.suppressPatterns !== undefined
782
+ ) {
783
+ channelConfig.suppressPatterns = normalizeStringArray(
784
+ value.suppressPatterns,
785
+ channelFallback.suppressPatterns ??
786
+ DEFAULT_RUNTIME_CONFIG.discord.suppressPatterns,
787
+ );
788
+ }
789
+ if (
790
+ value.maxConcurrentPerChannel !== undefined ||
791
+ channelFallback.maxConcurrentPerChannel !== undefined
792
+ ) {
793
+ channelConfig.maxConcurrentPerChannel = normalizeInteger(
794
+ value.maxConcurrentPerChannel,
795
+ channelFallback.maxConcurrentPerChannel ??
796
+ DEFAULT_RUNTIME_CONFIG.discord.maxConcurrentPerChannel,
797
+ { min: 1, max: 16 },
798
+ );
799
+ }
800
+
801
+ return channelConfig;
802
+ }
803
+
804
+ function normalizeDiscordGuildConfig(
805
+ value: unknown,
806
+ fallback: RuntimeDiscordGuildConfig,
807
+ ): RuntimeDiscordGuildConfig {
808
+ if (!isRecord(value)) return fallback;
809
+ const defaultMode = normalizeDiscordChannelMode(
810
+ value.defaultMode,
811
+ fallback.defaultMode,
812
+ );
813
+ const rawChannels = isRecord(value.channels) ? value.channels : {};
814
+ const channels: Record<string, RuntimeDiscordChannelConfig> = {};
815
+ for (const [rawChannelId, rawChannelConfig] of Object.entries(rawChannels)) {
816
+ const channelId = rawChannelId.trim();
817
+ if (!channelId) continue;
818
+ const fallbackChannel = fallback.channels[channelId] ?? {
819
+ mode: defaultMode,
820
+ };
821
+ const channelConfig = normalizeDiscordChannelConfig(
822
+ rawChannelConfig,
823
+ fallbackChannel,
824
+ defaultMode,
825
+ );
826
+ if (!channelConfig) continue;
827
+ channels[channelId] = channelConfig;
828
+ }
829
+
830
+ return { defaultMode, channels };
831
+ }
832
+
833
+ function normalizeDiscordGuildMap(
834
+ value: unknown,
835
+ fallback: Record<string, RuntimeDiscordGuildConfig>,
836
+ ): Record<string, RuntimeDiscordGuildConfig> {
837
+ if (!isRecord(value)) return fallback;
838
+ const guilds: Record<string, RuntimeDiscordGuildConfig> = {};
839
+ for (const [rawGuildId, rawGuildConfig] of Object.entries(value)) {
840
+ const guildId = rawGuildId.trim();
841
+ if (!guildId) continue;
842
+ const fallbackGuild = fallback[guildId] ?? {
843
+ defaultMode: 'mention',
844
+ channels: {},
845
+ };
846
+ guilds[guildId] = normalizeDiscordGuildConfig(
847
+ rawGuildConfig,
848
+ fallbackGuild,
849
+ );
850
+ }
851
+ return guilds;
852
+ }
853
+
854
+ function normalizeSchedulerScheduleKind(
855
+ value: unknown,
856
+ fallback: SchedulerScheduleKind,
857
+ ): SchedulerScheduleKind {
858
+ if (typeof value !== 'string') return fallback;
859
+ const normalized = value.trim().toLowerCase();
860
+ if (normalized === 'at' || normalized === 'every' || normalized === 'cron')
861
+ return normalized;
862
+ return fallback;
863
+ }
864
+
865
+ function normalizeSchedulerActionKind(
866
+ value: unknown,
867
+ fallback: SchedulerActionKind,
868
+ ): SchedulerActionKind {
869
+ if (typeof value !== 'string') return fallback;
870
+ const normalized = value.trim().toLowerCase();
871
+ if (normalized === 'agent_turn' || normalized === 'system_event')
872
+ return normalized;
873
+ return fallback;
874
+ }
875
+
876
+ function normalizeSchedulerDeliveryKind(
877
+ value: unknown,
878
+ fallback: SchedulerDeliveryKind,
879
+ ): SchedulerDeliveryKind {
880
+ if (typeof value !== 'string') return fallback;
881
+ const normalized = value.trim().toLowerCase();
882
+ if (
883
+ normalized === 'channel' ||
884
+ normalized === 'last-channel' ||
885
+ normalized === 'webhook'
886
+ )
887
+ return normalized;
888
+ return fallback;
889
+ }
890
+
891
+ function normalizeSchedulerJobList(
892
+ value: unknown,
893
+ fallback: RuntimeSchedulerJob[],
894
+ ): RuntimeSchedulerJob[] {
895
+ if (!Array.isArray(value)) return fallback;
896
+ const jobs: RuntimeSchedulerJob[] = [];
897
+ for (const item of value) {
898
+ if (!isRecord(item)) continue;
899
+ const jobId = normalizeString(item.id, '', { allowEmpty: false });
900
+ if (!jobId) continue;
901
+
902
+ const rawSchedule = isRecord(item.schedule) ? item.schedule : {};
903
+ const rawAction = isRecord(item.action) ? item.action : {};
904
+ const rawDelivery = isRecord(item.delivery) ? item.delivery : {};
905
+
906
+ const scheduleKind = normalizeSchedulerScheduleKind(
907
+ rawSchedule.kind,
908
+ 'cron',
909
+ );
910
+ const everyMs =
911
+ scheduleKind === 'every'
912
+ ? normalizeInteger(rawSchedule.everyMs, 60_000, {
913
+ min: 10_000,
914
+ max: 86_400_000,
915
+ })
916
+ : null;
917
+ const atRaw =
918
+ scheduleKind === 'at'
919
+ ? normalizeString(rawSchedule.at, '', { allowEmpty: false })
920
+ : '';
921
+ const atIso =
922
+ scheduleKind === 'at'
923
+ ? (() => {
924
+ const parsed = new Date(atRaw);
925
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
926
+ })()
927
+ : null;
928
+ const expr =
929
+ scheduleKind === 'cron'
930
+ ? normalizeString(rawSchedule.expr, '', { allowEmpty: false })
931
+ : '';
932
+ if (scheduleKind === 'at' && !atIso) continue;
933
+ if (scheduleKind === 'cron' && !expr) continue;
934
+
935
+ const deliveryKind = normalizeSchedulerDeliveryKind(
936
+ rawDelivery.kind,
937
+ 'channel',
938
+ );
939
+ const to = normalizeString(rawDelivery.to, '', { allowEmpty: true });
940
+ const webhookUrl = normalizeString(
941
+ rawDelivery.webhookUrl ?? rawDelivery.url,
942
+ '',
943
+ { allowEmpty: true },
944
+ );
945
+ if (deliveryKind === 'channel' && !to) continue;
946
+ if (deliveryKind === 'webhook' && !webhookUrl) continue;
947
+ const actionMessage = normalizeString(rawAction.message, '', {
948
+ allowEmpty: false,
949
+ });
950
+ if (!actionMessage) continue;
951
+ const name = normalizeString(item.name, '', { allowEmpty: true });
952
+ const description = normalizeString(item.description, '', {
953
+ allowEmpty: true,
954
+ });
955
+
956
+ jobs.push({
957
+ id: jobId,
958
+ ...(name ? { name } : {}),
959
+ ...(description ? { description } : {}),
960
+ schedule: {
961
+ kind: scheduleKind,
962
+ at: atIso,
963
+ everyMs,
964
+ expr: scheduleKind === 'cron' ? expr : null,
965
+ tz: normalizeString(rawSchedule.tz, '', { allowEmpty: true }),
966
+ },
967
+ action: {
968
+ kind: normalizeSchedulerActionKind(rawAction.kind, 'agent_turn'),
969
+ message: actionMessage,
970
+ },
971
+ delivery: {
972
+ kind: deliveryKind,
973
+ channel: normalizeString(rawDelivery.channel, 'discord', {
974
+ allowEmpty: true,
975
+ }),
976
+ to,
977
+ webhookUrl,
978
+ },
979
+ enabled: normalizeBoolean(item.enabled, true),
980
+ });
981
+ }
982
+ return jobs;
983
+ }
984
+
315
985
  function normalizeLogLevel(value: unknown, fallback: LogLevel): LogLevel {
316
- const normalized = normalizeString(value, fallback, { allowEmpty: false }).toLowerCase();
986
+ const normalized = normalizeString(value, fallback, {
987
+ allowEmpty: false,
988
+ }).toLowerCase();
317
989
  if (KNOWN_LOG_LEVELS.has(normalized)) return normalized as LogLevel;
318
990
  return fallback;
319
991
  }
@@ -324,7 +996,10 @@ function normalizeBaseUrl(value: unknown, fallback: string): string {
324
996
  }
325
997
 
326
998
  function normalizeApiPath(value: unknown, fallback: string): string {
327
- const normalized = normalizeString(value, fallback, { allowEmpty: false, trim: true });
999
+ const normalized = normalizeString(value, fallback, {
1000
+ allowEmpty: false,
1001
+ trim: true,
1002
+ });
328
1003
  if (/^https?:\/\//i.test(normalized)) {
329
1004
  return normalized.replace(/\/+$/, '');
330
1005
  }
@@ -339,7 +1014,9 @@ function parseConfigPatch(payload: unknown): DeepPartial<RuntimeConfig> {
339
1014
  return payload as DeepPartial<RuntimeConfig>;
340
1015
  }
341
1016
 
342
- function normalizeRuntimeConfig(patch?: DeepPartial<RuntimeConfig>): RuntimeConfig {
1017
+ function normalizeRuntimeConfig(
1018
+ patch?: DeepPartial<RuntimeConfig>,
1019
+ ): RuntimeConfig {
343
1020
  const raw = patch ?? {};
344
1021
 
345
1022
  const rawSecurity = isRecord(raw.security) ? raw.security : {};
@@ -348,23 +1025,44 @@ function normalizeRuntimeConfig(patch?: DeepPartial<RuntimeConfig>): RuntimeConf
348
1025
  const rawHybridAi = isRecord(raw.hybridai) ? raw.hybridai : {};
349
1026
  const rawContainer = isRecord(raw.container) ? raw.container : {};
350
1027
  const rawHeartbeat = isRecord(raw.heartbeat) ? raw.heartbeat : {};
1028
+ const rawMemory = isRecord(raw.memory) ? raw.memory : {};
351
1029
  const rawOps = isRecord(raw.ops) ? raw.ops : {};
352
1030
  const rawObservability = isRecord(raw.observability) ? raw.observability : {};
353
- const rawSessionCompaction = isRecord(raw.sessionCompaction) ? raw.sessionCompaction : {};
1031
+ const rawSessionCompaction = isRecord(raw.sessionCompaction)
1032
+ ? raw.sessionCompaction
1033
+ : {};
354
1034
  const rawPreFlush = isRecord(rawSessionCompaction.preCompactionMemoryFlush)
355
1035
  ? rawSessionCompaction.preCompactionMemoryFlush
356
1036
  : {};
357
1037
  const rawPromptHooks = isRecord(raw.promptHooks) ? raw.promptHooks : {};
358
1038
  const rawProactive = isRecord(raw.proactive) ? raw.proactive : {};
359
- const rawActiveHours = isRecord(rawProactive.activeHours) ? rawProactive.activeHours : {};
360
- const rawDelegation = isRecord(rawProactive.delegation) ? rawProactive.delegation : {};
361
- const rawAutoRetry = isRecord(rawProactive.autoRetry) ? rawProactive.autoRetry : {};
1039
+ const rawActiveHours = isRecord(rawProactive.activeHours)
1040
+ ? rawProactive.activeHours
1041
+ : {};
1042
+ const rawDelegation = isRecord(rawProactive.delegation)
1043
+ ? rawProactive.delegation
1044
+ : {};
1045
+ const rawAutoRetry = isRecord(rawProactive.autoRetry)
1046
+ ? rawProactive.autoRetry
1047
+ : {};
362
1048
  const rawRalph = isRecord(rawProactive.ralph) ? rawProactive.ralph : {};
1049
+ const rawScheduler = isRecord(raw.scheduler) ? raw.scheduler : {};
363
1050
 
364
1051
  const defaultOps = DEFAULT_RUNTIME_CONFIG.ops;
365
- const healthPort = normalizeInteger(rawOps.healthPort, defaultOps.healthPort, { min: 1, max: 65_535 });
366
- const webApiToken = normalizeString(rawOps.webApiToken, defaultOps.webApiToken, { allowEmpty: true });
367
- const hybridBaseUrl = normalizeBaseUrl(rawHybridAi.baseUrl, DEFAULT_RUNTIME_CONFIG.hybridai.baseUrl);
1052
+ const healthPort = normalizeInteger(
1053
+ rawOps.healthPort,
1054
+ defaultOps.healthPort,
1055
+ { min: 1, max: 65_535 },
1056
+ );
1057
+ const webApiToken = normalizeString(
1058
+ rawOps.webApiToken,
1059
+ defaultOps.webApiToken,
1060
+ { allowEmpty: true },
1061
+ );
1062
+ const hybridBaseUrl = normalizeBaseUrl(
1063
+ rawHybridAi.baseUrl,
1064
+ DEFAULT_RUNTIME_CONFIG.hybridai.baseUrl,
1065
+ );
368
1066
  const hybridDefaultChatbotId = normalizeString(
369
1067
  rawHybridAi.defaultChatbotId,
370
1068
  DEFAULT_RUNTIME_CONFIG.hybridai.defaultChatbotId,
@@ -376,6 +1074,16 @@ function normalizeRuntimeConfig(patch?: DeepPartial<RuntimeConfig>): RuntimeConf
376
1074
  DEFAULT_RUNTIME_CONFIG.sessionCompaction.threshold,
377
1075
  { min: 20 },
378
1076
  );
1077
+ const tokenBudget = normalizeInteger(
1078
+ rawSessionCompaction.tokenBudget,
1079
+ DEFAULT_RUNTIME_CONFIG.sessionCompaction.tokenBudget,
1080
+ { min: 1_000 },
1081
+ );
1082
+ const budgetRatio = normalizeNumber(
1083
+ rawSessionCompaction.budgetRatio,
1084
+ DEFAULT_RUNTIME_CONFIG.sessionCompaction.budgetRatio,
1085
+ { min: 0.05, max: 1 },
1086
+ );
379
1087
  const keepRecentRaw = normalizeInteger(
380
1088
  rawSessionCompaction.keepRecent,
381
1089
  DEFAULT_RUNTIME_CONFIG.sessionCompaction.keepRecent,
@@ -383,21 +1091,46 @@ function normalizeRuntimeConfig(patch?: DeepPartial<RuntimeConfig>): RuntimeConf
383
1091
  );
384
1092
  const keepRecent = Math.min(keepRecentRaw, Math.max(1, threshold - 1));
385
1093
 
386
- const modelList = normalizeStringArray(rawHybridAi.models, DEFAULT_RUNTIME_CONFIG.hybridai.models);
1094
+ const modelList = normalizeStringArray(
1095
+ rawHybridAi.models,
1096
+ DEFAULT_RUNTIME_CONFIG.hybridai.models,
1097
+ );
387
1098
 
388
1099
  return {
389
1100
  version: CONFIG_VERSION,
390
1101
  security: {
391
- trustModelAccepted: normalizeBoolean(rawSecurity.trustModelAccepted, DEFAULT_RUNTIME_CONFIG.security.trustModelAccepted),
392
- trustModelAcceptedAt: normalizeString(rawSecurity.trustModelAcceptedAt, DEFAULT_RUNTIME_CONFIG.security.trustModelAcceptedAt, { allowEmpty: true }),
393
- trustModelVersion: normalizeString(rawSecurity.trustModelVersion, DEFAULT_RUNTIME_CONFIG.security.trustModelVersion, { allowEmpty: true }),
394
- trustModelAcceptedBy: normalizeString(rawSecurity.trustModelAcceptedBy, DEFAULT_RUNTIME_CONFIG.security.trustModelAcceptedBy, { allowEmpty: true }),
1102
+ trustModelAccepted: normalizeBoolean(
1103
+ rawSecurity.trustModelAccepted,
1104
+ DEFAULT_RUNTIME_CONFIG.security.trustModelAccepted,
1105
+ ),
1106
+ trustModelAcceptedAt: normalizeString(
1107
+ rawSecurity.trustModelAcceptedAt,
1108
+ DEFAULT_RUNTIME_CONFIG.security.trustModelAcceptedAt,
1109
+ { allowEmpty: true },
1110
+ ),
1111
+ trustModelVersion: normalizeString(
1112
+ rawSecurity.trustModelVersion,
1113
+ DEFAULT_RUNTIME_CONFIG.security.trustModelVersion,
1114
+ { allowEmpty: true },
1115
+ ),
1116
+ trustModelAcceptedBy: normalizeString(
1117
+ rawSecurity.trustModelAcceptedBy,
1118
+ DEFAULT_RUNTIME_CONFIG.security.trustModelAcceptedBy,
1119
+ { allowEmpty: true },
1120
+ ),
395
1121
  },
396
1122
  skills: {
397
- extraDirs: normalizeStringArray(rawSkills.extraDirs, DEFAULT_RUNTIME_CONFIG.skills.extraDirs),
1123
+ extraDirs: normalizeStringArray(
1124
+ rawSkills.extraDirs,
1125
+ DEFAULT_RUNTIME_CONFIG.skills.extraDirs,
1126
+ ),
398
1127
  },
399
1128
  discord: {
400
- prefix: normalizeString(rawDiscord.prefix, DEFAULT_RUNTIME_CONFIG.discord.prefix, { allowEmpty: false }),
1129
+ prefix: normalizeString(
1130
+ rawDiscord.prefix,
1131
+ DEFAULT_RUNTIME_CONFIG.discord.prefix,
1132
+ { allowEmpty: false },
1133
+ ),
401
1134
  guildMembersIntent: normalizeBoolean(
402
1135
  rawDiscord.guildMembersIntent,
403
1136
  DEFAULT_RUNTIME_CONFIG.discord.guildMembersIntent,
@@ -419,51 +1152,218 @@ function normalizeRuntimeConfig(patch?: DeepPartial<RuntimeConfig>): RuntimeConf
419
1152
  DEFAULT_RUNTIME_CONFIG.discord.commandUserId,
420
1153
  { allowEmpty: true },
421
1154
  ),
1155
+ groupPolicy: normalizeDiscordGroupPolicy(
1156
+ rawDiscord.groupPolicy,
1157
+ DEFAULT_RUNTIME_CONFIG.discord.groupPolicy,
1158
+ ),
1159
+ freeResponseChannels: normalizeStringArray(
1160
+ rawDiscord.freeResponseChannels,
1161
+ DEFAULT_RUNTIME_CONFIG.discord.freeResponseChannels,
1162
+ ),
1163
+ humanDelay: normalizeDiscordHumanDelayConfig(
1164
+ rawDiscord.humanDelay,
1165
+ DEFAULT_RUNTIME_CONFIG.discord.humanDelay,
1166
+ ),
1167
+ typingMode: normalizeDiscordTypingMode(
1168
+ rawDiscord.typingMode,
1169
+ DEFAULT_RUNTIME_CONFIG.discord.typingMode,
1170
+ ),
1171
+ presence: normalizeDiscordPresenceConfig(
1172
+ rawDiscord.presence,
1173
+ DEFAULT_RUNTIME_CONFIG.discord.presence,
1174
+ ),
1175
+ lifecycleReactions: normalizeDiscordLifecycleReactionsConfig(
1176
+ rawDiscord.lifecycleReactions,
1177
+ DEFAULT_RUNTIME_CONFIG.discord.lifecycleReactions,
1178
+ ),
1179
+ ackReaction: normalizeString(
1180
+ rawDiscord.ackReaction,
1181
+ DEFAULT_RUNTIME_CONFIG.discord.ackReaction,
1182
+ { allowEmpty: false },
1183
+ ),
1184
+ ackReactionScope: normalizeDiscordAckReactionScope(
1185
+ rawDiscord.ackReactionScope,
1186
+ DEFAULT_RUNTIME_CONFIG.discord.ackReactionScope,
1187
+ ),
1188
+ removeAckAfterReply: normalizeBoolean(
1189
+ rawDiscord.removeAckAfterReply,
1190
+ DEFAULT_RUNTIME_CONFIG.discord.removeAckAfterReply,
1191
+ ),
1192
+ debounceMs: normalizeInteger(
1193
+ rawDiscord.debounceMs,
1194
+ DEFAULT_RUNTIME_CONFIG.discord.debounceMs,
1195
+ { min: 0, max: 120_000 },
1196
+ ),
1197
+ rateLimitPerUser: normalizeInteger(
1198
+ rawDiscord.rateLimitPerUser,
1199
+ DEFAULT_RUNTIME_CONFIG.discord.rateLimitPerUser,
1200
+ { min: 0, max: 300 },
1201
+ ),
1202
+ rateLimitExemptRoles: normalizeStringArray(
1203
+ rawDiscord.rateLimitExemptRoles,
1204
+ DEFAULT_RUNTIME_CONFIG.discord.rateLimitExemptRoles,
1205
+ ),
1206
+ suppressPatterns: normalizeStringArray(
1207
+ rawDiscord.suppressPatterns,
1208
+ DEFAULT_RUNTIME_CONFIG.discord.suppressPatterns,
1209
+ ),
1210
+ maxConcurrentPerChannel: normalizeInteger(
1211
+ rawDiscord.maxConcurrentPerChannel,
1212
+ DEFAULT_RUNTIME_CONFIG.discord.maxConcurrentPerChannel,
1213
+ { min: 1, max: 16 },
1214
+ ),
1215
+ guilds: normalizeDiscordGuildMap(
1216
+ rawDiscord.guilds,
1217
+ DEFAULT_RUNTIME_CONFIG.discord.guilds,
1218
+ ),
422
1219
  },
423
1220
  hybridai: {
424
1221
  baseUrl: hybridBaseUrl,
425
- defaultModel: normalizeString(rawHybridAi.defaultModel, DEFAULT_RUNTIME_CONFIG.hybridai.defaultModel, { allowEmpty: false }),
1222
+ defaultModel: normalizeString(
1223
+ rawHybridAi.defaultModel,
1224
+ DEFAULT_RUNTIME_CONFIG.hybridai.defaultModel,
1225
+ { allowEmpty: false },
1226
+ ),
426
1227
  defaultChatbotId: hybridDefaultChatbotId,
427
- enableRag: normalizeBoolean(rawHybridAi.enableRag, DEFAULT_RUNTIME_CONFIG.hybridai.enableRag),
1228
+ enableRag: normalizeBoolean(
1229
+ rawHybridAi.enableRag,
1230
+ DEFAULT_RUNTIME_CONFIG.hybridai.enableRag,
1231
+ ),
428
1232
  models: modelList,
429
1233
  },
430
1234
  container: {
431
- image: normalizeString(rawContainer.image, DEFAULT_RUNTIME_CONFIG.container.image, { allowEmpty: false }),
432
- memory: normalizeString(rawContainer.memory, DEFAULT_RUNTIME_CONFIG.container.memory, { allowEmpty: false }),
433
- cpus: normalizeString(rawContainer.cpus, DEFAULT_RUNTIME_CONFIG.container.cpus, { allowEmpty: false }),
434
- timeoutMs: normalizeInteger(rawContainer.timeoutMs, DEFAULT_RUNTIME_CONFIG.container.timeoutMs, { min: 1_000 }),
435
- additionalMounts: normalizeString(rawContainer.additionalMounts, DEFAULT_RUNTIME_CONFIG.container.additionalMounts, { allowEmpty: true }),
436
- maxOutputBytes: normalizeInteger(rawContainer.maxOutputBytes, DEFAULT_RUNTIME_CONFIG.container.maxOutputBytes, { min: 1_024 }),
437
- maxConcurrent: normalizeInteger(rawContainer.maxConcurrent, DEFAULT_RUNTIME_CONFIG.container.maxConcurrent, { min: 1 }),
1235
+ image: normalizeString(
1236
+ rawContainer.image,
1237
+ DEFAULT_RUNTIME_CONFIG.container.image,
1238
+ { allowEmpty: false },
1239
+ ),
1240
+ memory: normalizeString(
1241
+ rawContainer.memory,
1242
+ DEFAULT_RUNTIME_CONFIG.container.memory,
1243
+ { allowEmpty: false },
1244
+ ),
1245
+ cpus: normalizeString(
1246
+ rawContainer.cpus,
1247
+ DEFAULT_RUNTIME_CONFIG.container.cpus,
1248
+ { allowEmpty: false },
1249
+ ),
1250
+ timeoutMs: normalizeInteger(
1251
+ rawContainer.timeoutMs,
1252
+ DEFAULT_RUNTIME_CONFIG.container.timeoutMs,
1253
+ { min: 1_000 },
1254
+ ),
1255
+ additionalMounts: normalizeString(
1256
+ rawContainer.additionalMounts,
1257
+ DEFAULT_RUNTIME_CONFIG.container.additionalMounts,
1258
+ { allowEmpty: true },
1259
+ ),
1260
+ maxOutputBytes: normalizeInteger(
1261
+ rawContainer.maxOutputBytes,
1262
+ DEFAULT_RUNTIME_CONFIG.container.maxOutputBytes,
1263
+ { min: 1_024 },
1264
+ ),
1265
+ maxConcurrent: normalizeInteger(
1266
+ rawContainer.maxConcurrent,
1267
+ DEFAULT_RUNTIME_CONFIG.container.maxConcurrent,
1268
+ { min: 1 },
1269
+ ),
438
1270
  },
439
1271
  heartbeat: {
440
- enabled: normalizeBoolean(rawHeartbeat.enabled, DEFAULT_RUNTIME_CONFIG.heartbeat.enabled),
441
- intervalMs: normalizeInteger(rawHeartbeat.intervalMs, DEFAULT_RUNTIME_CONFIG.heartbeat.intervalMs, { min: 10_000 }),
442
- channel: normalizeString(rawHeartbeat.channel, DEFAULT_RUNTIME_CONFIG.heartbeat.channel, { allowEmpty: true }),
1272
+ enabled: normalizeBoolean(
1273
+ rawHeartbeat.enabled,
1274
+ DEFAULT_RUNTIME_CONFIG.heartbeat.enabled,
1275
+ ),
1276
+ intervalMs: normalizeInteger(
1277
+ rawHeartbeat.intervalMs,
1278
+ DEFAULT_RUNTIME_CONFIG.heartbeat.intervalMs,
1279
+ { min: 10_000 },
1280
+ ),
1281
+ channel: normalizeString(
1282
+ rawHeartbeat.channel,
1283
+ DEFAULT_RUNTIME_CONFIG.heartbeat.channel,
1284
+ { allowEmpty: true },
1285
+ ),
1286
+ },
1287
+ memory: {
1288
+ decayRate: normalizeNumber(
1289
+ rawMemory.decayRate,
1290
+ DEFAULT_RUNTIME_CONFIG.memory.decayRate,
1291
+ { min: 0, max: 0.95 },
1292
+ ),
1293
+ consolidationIntervalHours: normalizeInteger(
1294
+ rawMemory.consolidationIntervalHours,
1295
+ DEFAULT_RUNTIME_CONFIG.memory.consolidationIntervalHours,
1296
+ { min: 0, max: 24 * 30 },
1297
+ ),
443
1298
  },
444
1299
  ops: {
445
- healthHost: normalizeString(rawOps.healthHost, defaultOps.healthHost, { allowEmpty: false }),
1300
+ healthHost: normalizeString(rawOps.healthHost, defaultOps.healthHost, {
1301
+ allowEmpty: false,
1302
+ }),
446
1303
  healthPort,
447
1304
  webApiToken,
448
- gatewayBaseUrl: normalizeBaseUrl(rawOps.gatewayBaseUrl, `http://127.0.0.1:${healthPort}`),
449
- gatewayApiToken: normalizeString(rawOps.gatewayApiToken, webApiToken, { allowEmpty: true }),
450
- dbPath: normalizeString(rawOps.dbPath, defaultOps.dbPath, { allowEmpty: false }),
1305
+ gatewayBaseUrl: normalizeBaseUrl(
1306
+ rawOps.gatewayBaseUrl,
1307
+ `http://127.0.0.1:${healthPort}`,
1308
+ ),
1309
+ gatewayApiToken: normalizeString(rawOps.gatewayApiToken, webApiToken, {
1310
+ allowEmpty: true,
1311
+ }),
1312
+ dbPath: normalizeString(rawOps.dbPath, defaultOps.dbPath, {
1313
+ allowEmpty: false,
1314
+ }),
451
1315
  logLevel: normalizeLogLevel(rawOps.logLevel, defaultOps.logLevel),
452
1316
  },
453
1317
  observability: {
454
- enabled: normalizeBoolean(rawObservability.enabled, DEFAULT_RUNTIME_CONFIG.observability.enabled),
1318
+ enabled: normalizeBoolean(
1319
+ rawObservability.enabled,
1320
+ DEFAULT_RUNTIME_CONFIG.observability.enabled,
1321
+ ),
455
1322
  baseUrl: normalizeBaseUrl(rawObservability.baseUrl, hybridBaseUrl),
456
- ingestPath: normalizeApiPath(rawObservability.ingestPath, DEFAULT_RUNTIME_CONFIG.observability.ingestPath),
457
- statusPath: normalizeApiPath(rawObservability.statusPath, DEFAULT_RUNTIME_CONFIG.observability.statusPath),
458
- botId: normalizeString(rawObservability.botId, hybridDefaultChatbotId, { allowEmpty: true }),
459
- agentId: normalizeString(rawObservability.agentId, DEFAULT_RUNTIME_CONFIG.observability.agentId, { allowEmpty: false }),
460
- label: normalizeString(rawObservability.label, DEFAULT_RUNTIME_CONFIG.observability.label, { allowEmpty: true }),
461
- environment: normalizeString(rawObservability.environment, DEFAULT_RUNTIME_CONFIG.observability.environment, { allowEmpty: false }),
462
- flushIntervalMs: normalizeInteger(rawObservability.flushIntervalMs, DEFAULT_RUNTIME_CONFIG.observability.flushIntervalMs, { min: 1_000, max: 3_600_000 }),
463
- batchMaxEvents: normalizeInteger(rawObservability.batchMaxEvents, DEFAULT_RUNTIME_CONFIG.observability.batchMaxEvents, { min: 1, max: 1_000 }),
1323
+ ingestPath: normalizeApiPath(
1324
+ rawObservability.ingestPath,
1325
+ DEFAULT_RUNTIME_CONFIG.observability.ingestPath,
1326
+ ),
1327
+ statusPath: normalizeApiPath(
1328
+ rawObservability.statusPath,
1329
+ DEFAULT_RUNTIME_CONFIG.observability.statusPath,
1330
+ ),
1331
+ botId: normalizeString(rawObservability.botId, hybridDefaultChatbotId, {
1332
+ allowEmpty: true,
1333
+ }),
1334
+ agentId: normalizeString(
1335
+ rawObservability.agentId,
1336
+ DEFAULT_RUNTIME_CONFIG.observability.agentId,
1337
+ { allowEmpty: false },
1338
+ ),
1339
+ label: normalizeString(
1340
+ rawObservability.label,
1341
+ DEFAULT_RUNTIME_CONFIG.observability.label,
1342
+ { allowEmpty: true },
1343
+ ),
1344
+ environment: normalizeString(
1345
+ rawObservability.environment,
1346
+ DEFAULT_RUNTIME_CONFIG.observability.environment,
1347
+ { allowEmpty: false },
1348
+ ),
1349
+ flushIntervalMs: normalizeInteger(
1350
+ rawObservability.flushIntervalMs,
1351
+ DEFAULT_RUNTIME_CONFIG.observability.flushIntervalMs,
1352
+ { min: 1_000, max: 3_600_000 },
1353
+ ),
1354
+ batchMaxEvents: normalizeInteger(
1355
+ rawObservability.batchMaxEvents,
1356
+ DEFAULT_RUNTIME_CONFIG.observability.batchMaxEvents,
1357
+ { min: 1, max: 1_000 },
1358
+ ),
464
1359
  },
465
1360
  sessionCompaction: {
466
- enabled: normalizeBoolean(rawSessionCompaction.enabled, DEFAULT_RUNTIME_CONFIG.sessionCompaction.enabled),
1361
+ enabled: normalizeBoolean(
1362
+ rawSessionCompaction.enabled,
1363
+ DEFAULT_RUNTIME_CONFIG.sessionCompaction.enabled,
1364
+ ),
1365
+ tokenBudget,
1366
+ budgetRatio,
467
1367
  threshold,
468
1368
  keepRecent,
469
1369
  summaryMaxChars: normalizeInteger(
@@ -472,49 +1372,125 @@ function normalizeRuntimeConfig(patch?: DeepPartial<RuntimeConfig>): RuntimeConf
472
1372
  { min: 1_000 },
473
1373
  ),
474
1374
  preCompactionMemoryFlush: {
475
- enabled: normalizeBoolean(rawPreFlush.enabled, DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush.enabled),
1375
+ enabled: normalizeBoolean(
1376
+ rawPreFlush.enabled,
1377
+ DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush
1378
+ .enabled,
1379
+ ),
476
1380
  maxMessages: normalizeInteger(
477
1381
  rawPreFlush.maxMessages,
478
- DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush.maxMessages,
1382
+ DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush
1383
+ .maxMessages,
479
1384
  { min: 8 },
480
1385
  ),
481
1386
  maxChars: normalizeInteger(
482
1387
  rawPreFlush.maxChars,
483
- DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush.maxChars,
1388
+ DEFAULT_RUNTIME_CONFIG.sessionCompaction.preCompactionMemoryFlush
1389
+ .maxChars,
484
1390
  { min: 4_000 },
485
1391
  ),
486
1392
  },
487
1393
  },
488
1394
  promptHooks: {
489
- bootstrapEnabled: normalizeBoolean(rawPromptHooks.bootstrapEnabled, DEFAULT_RUNTIME_CONFIG.promptHooks.bootstrapEnabled),
490
- memoryEnabled: normalizeBoolean(rawPromptHooks.memoryEnabled, DEFAULT_RUNTIME_CONFIG.promptHooks.memoryEnabled),
491
- safetyEnabled: normalizeBoolean(rawPromptHooks.safetyEnabled, DEFAULT_RUNTIME_CONFIG.promptHooks.safetyEnabled),
492
- proactivityEnabled: normalizeBoolean(rawPromptHooks.proactivityEnabled, DEFAULT_RUNTIME_CONFIG.promptHooks.proactivityEnabled),
1395
+ bootstrapEnabled: normalizeBoolean(
1396
+ rawPromptHooks.bootstrapEnabled,
1397
+ DEFAULT_RUNTIME_CONFIG.promptHooks.bootstrapEnabled,
1398
+ ),
1399
+ memoryEnabled: normalizeBoolean(
1400
+ rawPromptHooks.memoryEnabled,
1401
+ DEFAULT_RUNTIME_CONFIG.promptHooks.memoryEnabled,
1402
+ ),
1403
+ safetyEnabled: normalizeBoolean(
1404
+ rawPromptHooks.safetyEnabled,
1405
+ DEFAULT_RUNTIME_CONFIG.promptHooks.safetyEnabled,
1406
+ ),
1407
+ proactivityEnabled: normalizeBoolean(
1408
+ rawPromptHooks.proactivityEnabled,
1409
+ DEFAULT_RUNTIME_CONFIG.promptHooks.proactivityEnabled,
1410
+ ),
493
1411
  },
494
1412
  proactive: {
495
1413
  activeHours: {
496
- enabled: normalizeBoolean(rawActiveHours.enabled, DEFAULT_RUNTIME_CONFIG.proactive.activeHours.enabled),
497
- timezone: normalizeString(rawActiveHours.timezone, DEFAULT_RUNTIME_CONFIG.proactive.activeHours.timezone, { allowEmpty: true }),
498
- startHour: normalizeInteger(rawActiveHours.startHour, DEFAULT_RUNTIME_CONFIG.proactive.activeHours.startHour, { min: 0, max: 23 }),
499
- endHour: normalizeInteger(rawActiveHours.endHour, DEFAULT_RUNTIME_CONFIG.proactive.activeHours.endHour, { min: 0, max: 23 }),
500
- queueOutsideHours: normalizeBoolean(rawActiveHours.queueOutsideHours, DEFAULT_RUNTIME_CONFIG.proactive.activeHours.queueOutsideHours),
1414
+ enabled: normalizeBoolean(
1415
+ rawActiveHours.enabled,
1416
+ DEFAULT_RUNTIME_CONFIG.proactive.activeHours.enabled,
1417
+ ),
1418
+ timezone: normalizeString(
1419
+ rawActiveHours.timezone,
1420
+ DEFAULT_RUNTIME_CONFIG.proactive.activeHours.timezone,
1421
+ { allowEmpty: true },
1422
+ ),
1423
+ startHour: normalizeInteger(
1424
+ rawActiveHours.startHour,
1425
+ DEFAULT_RUNTIME_CONFIG.proactive.activeHours.startHour,
1426
+ { min: 0, max: 23 },
1427
+ ),
1428
+ endHour: normalizeInteger(
1429
+ rawActiveHours.endHour,
1430
+ DEFAULT_RUNTIME_CONFIG.proactive.activeHours.endHour,
1431
+ { min: 0, max: 23 },
1432
+ ),
1433
+ queueOutsideHours: normalizeBoolean(
1434
+ rawActiveHours.queueOutsideHours,
1435
+ DEFAULT_RUNTIME_CONFIG.proactive.activeHours.queueOutsideHours,
1436
+ ),
501
1437
  },
502
1438
  delegation: {
503
- enabled: normalizeBoolean(rawDelegation.enabled, DEFAULT_RUNTIME_CONFIG.proactive.delegation.enabled),
504
- maxConcurrent: normalizeInteger(rawDelegation.maxConcurrent, DEFAULT_RUNTIME_CONFIG.proactive.delegation.maxConcurrent, { min: 1, max: 8 }),
505
- maxDepth: normalizeInteger(rawDelegation.maxDepth, DEFAULT_RUNTIME_CONFIG.proactive.delegation.maxDepth, { min: 1, max: 4 }),
506
- maxPerTurn: normalizeInteger(rawDelegation.maxPerTurn, DEFAULT_RUNTIME_CONFIG.proactive.delegation.maxPerTurn, { min: 1, max: 8 }),
1439
+ enabled: normalizeBoolean(
1440
+ rawDelegation.enabled,
1441
+ DEFAULT_RUNTIME_CONFIG.proactive.delegation.enabled,
1442
+ ),
1443
+ maxConcurrent: normalizeInteger(
1444
+ rawDelegation.maxConcurrent,
1445
+ DEFAULT_RUNTIME_CONFIG.proactive.delegation.maxConcurrent,
1446
+ { min: 1, max: 8 },
1447
+ ),
1448
+ maxDepth: normalizeInteger(
1449
+ rawDelegation.maxDepth,
1450
+ DEFAULT_RUNTIME_CONFIG.proactive.delegation.maxDepth,
1451
+ { min: 1, max: 4 },
1452
+ ),
1453
+ maxPerTurn: normalizeInteger(
1454
+ rawDelegation.maxPerTurn,
1455
+ DEFAULT_RUNTIME_CONFIG.proactive.delegation.maxPerTurn,
1456
+ { min: 1, max: 8 },
1457
+ ),
507
1458
  },
508
1459
  autoRetry: {
509
- enabled: normalizeBoolean(rawAutoRetry.enabled, DEFAULT_RUNTIME_CONFIG.proactive.autoRetry.enabled),
510
- maxAttempts: normalizeInteger(rawAutoRetry.maxAttempts, DEFAULT_RUNTIME_CONFIG.proactive.autoRetry.maxAttempts, { min: 1, max: 8 }),
511
- baseDelayMs: normalizeInteger(rawAutoRetry.baseDelayMs, DEFAULT_RUNTIME_CONFIG.proactive.autoRetry.baseDelayMs, { min: 100, max: 120_000 }),
512
- maxDelayMs: normalizeInteger(rawAutoRetry.maxDelayMs, DEFAULT_RUNTIME_CONFIG.proactive.autoRetry.maxDelayMs, { min: 100, max: 600_000 }),
1460
+ enabled: normalizeBoolean(
1461
+ rawAutoRetry.enabled,
1462
+ DEFAULT_RUNTIME_CONFIG.proactive.autoRetry.enabled,
1463
+ ),
1464
+ maxAttempts: normalizeInteger(
1465
+ rawAutoRetry.maxAttempts,
1466
+ DEFAULT_RUNTIME_CONFIG.proactive.autoRetry.maxAttempts,
1467
+ { min: 1, max: 8 },
1468
+ ),
1469
+ baseDelayMs: normalizeInteger(
1470
+ rawAutoRetry.baseDelayMs,
1471
+ DEFAULT_RUNTIME_CONFIG.proactive.autoRetry.baseDelayMs,
1472
+ { min: 100, max: 120_000 },
1473
+ ),
1474
+ maxDelayMs: normalizeInteger(
1475
+ rawAutoRetry.maxDelayMs,
1476
+ DEFAULT_RUNTIME_CONFIG.proactive.autoRetry.maxDelayMs,
1477
+ { min: 100, max: 600_000 },
1478
+ ),
513
1479
  },
514
1480
  ralph: {
515
- maxIterations: normalizeInteger(rawRalph.maxIterations, DEFAULT_RUNTIME_CONFIG.proactive.ralph.maxIterations, { min: -1, max: 64 }),
1481
+ maxIterations: normalizeInteger(
1482
+ rawRalph.maxIterations,
1483
+ DEFAULT_RUNTIME_CONFIG.proactive.ralph.maxIterations,
1484
+ { min: -1, max: 64 },
1485
+ ),
516
1486
  },
517
1487
  },
1488
+ scheduler: {
1489
+ jobs: normalizeSchedulerJobList(
1490
+ rawScheduler.jobs,
1491
+ DEFAULT_RUNTIME_CONFIG.scheduler.jobs,
1492
+ ),
1493
+ },
518
1494
  };
519
1495
  }
520
1496
 
@@ -544,7 +1520,9 @@ function applyConfig(next: RuntimeConfig): void {
544
1520
  try {
545
1521
  listener(cloneConfig(currentConfig), cloneConfig(prev));
546
1522
  } catch (err) {
547
- console.warn(`[runtime-config] listener failed: ${err instanceof Error ? err.message : String(err)}`);
1523
+ console.warn(
1524
+ `[runtime-config] listener failed: ${err instanceof Error ? err.message : String(err)}`,
1525
+ );
548
1526
  }
549
1527
  }
550
1528
  }
@@ -559,7 +1537,9 @@ function reloadFromDisk(trigger: string): void {
559
1537
  const next = loadRuntimeConfigFromSources();
560
1538
  applyConfig(next);
561
1539
  } catch (err) {
562
- console.warn(`[runtime-config] reload failed (${trigger}): ${err instanceof Error ? err.message : String(err)}`);
1540
+ console.warn(
1541
+ `[runtime-config] reload failed (${trigger}): ${err instanceof Error ? err.message : String(err)}`,
1542
+ );
563
1543
  }
564
1544
  }
565
1545
 
@@ -574,13 +1554,15 @@ function scheduleReload(trigger: string): void {
574
1554
  function scheduleWatcherRestart(reason: string): void {
575
1555
  if (watcherRestartTimer) return;
576
1556
  if (watcherRetryAttempt >= WATCHER_RETRY_MAX_ATTEMPTS) {
577
- console.warn(`[runtime-config] watcher disabled after ${WATCHER_RETRY_MAX_ATTEMPTS} retries (${reason})`);
1557
+ console.warn(
1558
+ `[runtime-config] watcher disabled after ${WATCHER_RETRY_MAX_ATTEMPTS} retries (${reason})`,
1559
+ );
578
1560
  return;
579
1561
  }
580
1562
 
581
1563
  watcherRetryAttempt += 1;
582
1564
  const delay = Math.min(
583
- WATCHER_RETRY_BASE_DELAY_MS * (2 ** (watcherRetryAttempt - 1)),
1565
+ WATCHER_RETRY_BASE_DELAY_MS * 2 ** (watcherRetryAttempt - 1),
584
1566
  WATCHER_RETRY_MAX_DELAY_MS,
585
1567
  );
586
1568
  console.warn(
@@ -596,14 +1578,18 @@ function startWatcher(): void {
596
1578
  if (configWatcher) return;
597
1579
 
598
1580
  try {
599
- configWatcher = fs.watch(path.dirname(CONFIG_PATH), { persistent: false }, (_event, filename) => {
600
- if (!filename) {
601
- scheduleReload('unknown');
602
- return;
603
- }
604
- if (filename.toString() !== path.basename(CONFIG_PATH)) return;
605
- scheduleReload(`watch:${filename.toString()}`);
606
- });
1581
+ configWatcher = fs.watch(
1582
+ path.dirname(CONFIG_PATH),
1583
+ { persistent: false },
1584
+ (_event, filename) => {
1585
+ if (!filename) {
1586
+ scheduleReload('unknown');
1587
+ return;
1588
+ }
1589
+ if (filename.toString() !== path.basename(CONFIG_PATH)) return;
1590
+ scheduleReload(`watch:${filename.toString()}`);
1591
+ },
1592
+ );
607
1593
  watcherRetryAttempt = 0;
608
1594
  if (watcherRestartTimer) {
609
1595
  clearTimeout(watcherRestartTimer);
@@ -639,21 +1625,28 @@ function migrateConfigSchemaOnStartup(): void {
639
1625
  raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
640
1626
  parsed = JSON.parse(raw) as unknown;
641
1627
  } catch (err) {
642
- console.warn(`[runtime-config] schema migration skipped (invalid JSON): ${err instanceof Error ? err.message : String(err)}`);
1628
+ console.warn(
1629
+ `[runtime-config] schema migration skipped (invalid JSON): ${err instanceof Error ? err.message : String(err)}`,
1630
+ );
643
1631
  return;
644
1632
  }
645
1633
 
646
1634
  if (!isRecord(parsed)) {
647
- console.warn('[runtime-config] schema migration skipped: config.json is not an object');
1635
+ console.warn(
1636
+ '[runtime-config] schema migration skipped: config.json is not an object',
1637
+ );
648
1638
  return;
649
1639
  }
650
1640
 
651
- const previousVersion = typeof parsed.version === 'number' ? parsed.version : null;
1641
+ const previousVersion =
1642
+ typeof parsed.version === 'number' ? parsed.version : null;
652
1643
  let migrated: RuntimeConfig;
653
1644
  try {
654
1645
  migrated = normalizeRuntimeConfig(parseConfigPatch(parsed));
655
1646
  } catch (err) {
656
- console.warn(`[runtime-config] schema migration skipped: ${err instanceof Error ? err.message : String(err)}`);
1647
+ console.warn(
1648
+ `[runtime-config] schema migration skipped: ${err instanceof Error ? err.message : String(err)}`,
1649
+ );
657
1650
  return;
658
1651
  }
659
1652
 
@@ -664,12 +1657,18 @@ function migrateConfigSchemaOnStartup(): void {
664
1657
  writeConfigFile(migrated);
665
1658
  const from = previousVersion == null ? 'unknown' : String(previousVersion);
666
1659
  if (previousVersion !== CONFIG_VERSION) {
667
- console.info(`[runtime-config] migrated config schema from v${from} to v${CONFIG_VERSION}`);
1660
+ console.info(
1661
+ `[runtime-config] migrated config schema from v${from} to v${CONFIG_VERSION}`,
1662
+ );
668
1663
  } else {
669
- console.info(`[runtime-config] normalized config schema v${CONFIG_VERSION} (filled defaults/canonicalized values)`);
1664
+ console.info(
1665
+ `[runtime-config] normalized config schema v${CONFIG_VERSION} (filled defaults/canonicalized values)`,
1666
+ );
670
1667
  }
671
1668
  } catch (err) {
672
- console.warn(`[runtime-config] schema migration failed: ${err instanceof Error ? err.message : String(err)}`);
1669
+ console.warn(
1670
+ `[runtime-config] schema migration failed: ${err instanceof Error ? err.message : String(err)}`,
1671
+ );
673
1672
  }
674
1673
  }
675
1674
 
@@ -697,7 +1696,9 @@ export function getRuntimeConfig(): RuntimeConfig {
697
1696
  return cloneConfig(currentConfig);
698
1697
  }
699
1698
 
700
- export function onRuntimeConfigChange(listener: RuntimeConfigChangeListener): () => void {
1699
+ export function onRuntimeConfigChange(
1700
+ listener: RuntimeConfigChangeListener,
1701
+ ): () => void {
701
1702
  listeners.add(listener);
702
1703
  return () => listeners.delete(listener);
703
1704
  }
@@ -709,17 +1710,21 @@ export function saveRuntimeConfig(next: RuntimeConfig): RuntimeConfig {
709
1710
  return cloneConfig(normalized);
710
1711
  }
711
1712
 
712
- export function updateRuntimeConfig(mutator: (draft: RuntimeConfig) => void): RuntimeConfig {
1713
+ export function updateRuntimeConfig(
1714
+ mutator: (draft: RuntimeConfig) => void,
1715
+ ): RuntimeConfig {
713
1716
  const draft = cloneConfig(currentConfig);
714
1717
  mutator(draft);
715
1718
  return saveRuntimeConfig(draft);
716
1719
  }
717
1720
 
718
- export function isSecurityTrustAccepted(config: RuntimeConfig = currentConfig): boolean {
1721
+ export function isSecurityTrustAccepted(
1722
+ config: RuntimeConfig = currentConfig,
1723
+ ): boolean {
719
1724
  return Boolean(
720
- config.security.trustModelAccepted
721
- && config.security.trustModelAcceptedAt
722
- && config.security.trustModelVersion === SECURITY_POLICY_VERSION,
1725
+ config.security.trustModelAccepted &&
1726
+ config.security.trustModelAcceptedAt &&
1727
+ config.security.trustModelVersion === SECURITY_POLICY_VERSION,
723
1728
  );
724
1729
  }
725
1730
 
@@ -728,9 +1733,19 @@ export function acceptSecurityTrustModel(params?: {
728
1733
  acceptedBy?: string | null;
729
1734
  policyVersion?: string;
730
1735
  }): RuntimeConfig {
731
- const acceptedAt = normalizeString(params?.acceptedAt, new Date().toISOString(), { allowEmpty: false });
732
- const acceptedBy = normalizeString(params?.acceptedBy ?? '', '', { allowEmpty: true });
733
- const policyVersion = normalizeString(params?.policyVersion, SECURITY_POLICY_VERSION, { allowEmpty: false });
1736
+ const acceptedAt = normalizeString(
1737
+ params?.acceptedAt,
1738
+ new Date().toISOString(),
1739
+ { allowEmpty: false },
1740
+ );
1741
+ const acceptedBy = normalizeString(params?.acceptedBy ?? '', '', {
1742
+ allowEmpty: true,
1743
+ });
1744
+ const policyVersion = normalizeString(
1745
+ params?.policyVersion,
1746
+ SECURITY_POLICY_VERSION,
1747
+ { allowEmpty: false },
1748
+ );
734
1749
 
735
1750
  return updateRuntimeConfig((draft) => {
736
1751
  draft.security.trustModelAccepted = true;