@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
package/src/config.ts CHANGED
@@ -1,7 +1,8 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import { randomBytes } from 'crypto';
1
+ import { randomBytes } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
 
6
7
  import { loadEnvFile } from './env.js';
7
8
  import {
@@ -27,13 +28,9 @@ function required(name: string): string {
27
28
  return val;
28
29
  }
29
30
 
30
- function resolveAppVersion(): string {
31
- const envVersion = process.env.npm_package_version;
32
- if (envVersion) return envVersion;
33
-
34
- const packagePath = path.join(process.cwd(), 'package.json');
31
+ function readVersionFromPackageJson(packageJsonPath: string): string | null {
35
32
  try {
36
- const raw = fs.readFileSync(packagePath, 'utf-8');
33
+ const raw = fs.readFileSync(packageJsonPath, 'utf-8');
37
34
  const parsed = JSON.parse(raw) as { version?: unknown };
38
35
  if (typeof parsed.version === 'string' && parsed.version.trim()) {
39
36
  return parsed.version.trim();
@@ -41,6 +38,31 @@ function resolveAppVersion(): string {
41
38
  } catch {
42
39
  // fall through
43
40
  }
41
+ return null;
42
+ }
43
+
44
+ function resolveAppVersion(): string {
45
+ const envVersion = process.env.npm_package_version;
46
+ if (envVersion?.trim()) return envVersion.trim();
47
+
48
+ const modulePath = fileURLToPath(import.meta.url);
49
+ const moduleVersion = readVersionFromPackageJson(
50
+ path.join(path.dirname(modulePath), '..', 'package.json'),
51
+ );
52
+ if (moduleVersion) return moduleVersion;
53
+
54
+ const entryPath = process.argv[1];
55
+ if (entryPath) {
56
+ const entryVersion = readVersionFromPackageJson(
57
+ path.join(path.dirname(path.resolve(entryPath)), '..', 'package.json'),
58
+ );
59
+ if (entryVersion) return entryVersion;
60
+ }
61
+
62
+ const cwdVersion = readVersionFromPackageJson(
63
+ path.join(process.cwd(), 'package.json'),
64
+ );
65
+ if (cwdVersion) return cwdVersion;
44
66
 
45
67
  return '0.0.0';
46
68
  }
@@ -61,14 +83,63 @@ export let DISCORD_GUILD_MEMBERS_INTENT = false;
61
83
  export let DISCORD_PRESENCE_INTENT = false;
62
84
  export let DISCORD_RESPOND_TO_ALL_MESSAGES = false;
63
85
  export let DISCORD_COMMANDS_ONLY = false;
86
+ export let DISCORD_COMMAND_MODE: RuntimeConfig['discord']['commandMode'] =
87
+ 'public';
88
+ export let DISCORD_COMMAND_ALLOWED_USER_IDS: string[] = [];
64
89
  export let DISCORD_COMMAND_USER_ID = '';
65
- export let DISCORD_GROUP_POLICY: RuntimeConfig['discord']['groupPolicy'] = 'open';
90
+ export let DISCORD_GROUP_POLICY: RuntimeConfig['discord']['groupPolicy'] =
91
+ 'open';
66
92
  export let DISCORD_FREE_RESPONSE_CHANNELS: string[] = [];
93
+ export let DISCORD_TEXT_CHUNK_LIMIT = 2_000;
94
+ export let DISCORD_MAX_LINES_PER_MESSAGE = 17;
95
+ export let DISCORD_HUMAN_DELAY: RuntimeConfig['discord']['humanDelay'] = {
96
+ mode: 'natural',
97
+ minMs: 800,
98
+ maxMs: 2_500,
99
+ };
100
+ export let DISCORD_TYPING_MODE: RuntimeConfig['discord']['typingMode'] =
101
+ 'thinking';
102
+ export let DISCORD_SELF_PRESENCE: RuntimeConfig['discord']['presence'] = {
103
+ enabled: true,
104
+ intervalMs: 30_000,
105
+ healthyText: 'Watching the channels',
106
+ degradedText: 'Thinking slowly...',
107
+ exhaustedText: 'Taking a break',
108
+ activityType: 'watching',
109
+ };
110
+ export let DISCORD_LIFECYCLE_REACTIONS: RuntimeConfig['discord']['lifecycleReactions'] =
111
+ {
112
+ enabled: true,
113
+ removeOnComplete: true,
114
+ phases: {
115
+ queued: '⏳',
116
+ thinking: '🤔',
117
+ toolUse: '⚙️',
118
+ streaming: '✍️',
119
+ done: '✅',
120
+ error: '❌',
121
+ },
122
+ };
123
+ export let DISCORD_ACK_REACTION = '👀';
124
+ export let DISCORD_ACK_REACTION_SCOPE: RuntimeConfig['discord']['ackReactionScope'] =
125
+ 'group-mentions';
126
+ export let DISCORD_REMOVE_ACK_AFTER_REPLY = true;
127
+ export let DISCORD_DEBOUNCE_MS = 2_500;
128
+ export let DISCORD_RATE_LIMIT_PER_USER = 0;
129
+ export let DISCORD_RATE_LIMIT_EXEMPT_ROLES: string[] = [];
130
+ export let DISCORD_SUPPRESS_PATTERNS: string[] = [
131
+ '/stop',
132
+ '/pause',
133
+ 'brb',
134
+ 'afk',
135
+ ];
136
+ export let DISCORD_MAX_CONCURRENT_PER_CHANNEL = 2;
67
137
  export let DISCORD_GUILDS: RuntimeConfig['discord']['guilds'] = {};
68
138
 
69
139
  export let HYBRIDAI_BASE_URL = 'https://hybridai.one';
70
140
  export let HYBRIDAI_MODEL = 'gpt-5-nano';
71
141
  export let HYBRIDAI_CHATBOT_ID = '';
142
+ export let HYBRIDAI_MAX_TOKENS = 4_096;
72
143
  export let HYBRIDAI_ENABLE_RAG = true;
73
144
  export let HYBRIDAI_MODELS: string[] = ['gpt-5-nano', 'gpt-5-mini', 'gpt-5'];
74
145
 
@@ -78,7 +149,10 @@ export let CONTAINER_CPUS = '1';
78
149
  export let CONTAINER_TIMEOUT = 300_000;
79
150
 
80
151
  export const MOUNT_ALLOWLIST_PATH = path.join(
81
- os.homedir(), '.config', 'hybridclaw', 'mount-allowlist.json',
152
+ os.homedir(),
153
+ '.config',
154
+ 'hybridclaw',
155
+ 'mount-allowlist.json',
82
156
  );
83
157
  export let ADDITIONAL_MOUNTS = '';
84
158
 
@@ -88,6 +162,8 @@ export let MAX_CONCURRENT_CONTAINERS = 5;
88
162
  export let HEARTBEAT_ENABLED = true;
89
163
  export let HEARTBEAT_INTERVAL = 1_800_000;
90
164
  export let HEARTBEAT_CHANNEL = '';
165
+ export let MEMORY_DECAY_RATE = 0.1;
166
+ export let MEMORY_CONSOLIDATION_INTERVAL_HOURS = 24;
91
167
 
92
168
  export let HEALTH_HOST = '127.0.0.1';
93
169
  export let HEALTH_PORT = 9090;
@@ -100,7 +176,8 @@ export let DATA_DIR = path.dirname(DB_PATH);
100
176
 
101
177
  export let OBSERVABILITY_ENABLED = true;
102
178
  export let OBSERVABILITY_BASE_URL = 'https://hybridai.one';
103
- export let OBSERVABILITY_INGEST_PATH = '/api/v1/agent-observability/events:batch';
179
+ export let OBSERVABILITY_INGEST_PATH =
180
+ '/api/v1/agent-observability/events:batch';
104
181
  export let OBSERVABILITY_STATUS_PATH = '/api/v1/agent-observability/status';
105
182
  export let OBSERVABILITY_BOT_ID = '';
106
183
  export let OBSERVABILITY_AGENT_ID = 'agent_main';
@@ -110,7 +187,9 @@ export let OBSERVABILITY_FLUSH_INTERVAL_MS = 10_000;
110
187
  export let OBSERVABILITY_BATCH_MAX_EVENTS = 500;
111
188
 
112
189
  export let SESSION_COMPACTION_ENABLED = true;
113
- export let SESSION_COMPACTION_THRESHOLD = 120;
190
+ export let SESSION_COMPACTION_TOKEN_BUDGET = 100_000;
191
+ export let SESSION_COMPACTION_BUDGET_RATIO = 0.7;
192
+ export let SESSION_COMPACTION_THRESHOLD = 200;
114
193
  export let SESSION_COMPACTION_KEEP_RECENT = 40;
115
194
  export let SESSION_COMPACTION_SUMMARY_MAX_CHARS = 8_000;
116
195
  export let PRE_COMPACTION_MEMORY_FLUSH_ENABLED = true;
@@ -140,14 +219,51 @@ function applyRuntimeConfig(config: RuntimeConfig): void {
140
219
  DISCORD_PRESENCE_INTENT = config.discord.presenceIntent;
141
220
  DISCORD_RESPOND_TO_ALL_MESSAGES = config.discord.respondToAllMessages;
142
221
  DISCORD_COMMANDS_ONLY = config.discord.commandsOnly;
222
+ DISCORD_COMMAND_MODE = config.discord.commandMode;
223
+ DISCORD_COMMAND_ALLOWED_USER_IDS = [...config.discord.commandAllowedUserIds];
143
224
  DISCORD_COMMAND_USER_ID = config.discord.commandUserId;
144
225
  DISCORD_GROUP_POLICY = config.discord.groupPolicy;
145
226
  DISCORD_FREE_RESPONSE_CHANNELS = [...config.discord.freeResponseChannels];
146
- DISCORD_GUILDS = JSON.parse(JSON.stringify(config.discord.guilds)) as RuntimeConfig['discord']['guilds'];
227
+ DISCORD_TEXT_CHUNK_LIMIT = Math.max(
228
+ 200,
229
+ Math.min(2_000, config.discord.textChunkLimit),
230
+ );
231
+ DISCORD_MAX_LINES_PER_MESSAGE = Math.max(
232
+ 4,
233
+ Math.min(200, config.discord.maxLinesPerMessage),
234
+ );
235
+ DISCORD_HUMAN_DELAY = JSON.parse(
236
+ JSON.stringify(config.discord.humanDelay),
237
+ ) as RuntimeConfig['discord']['humanDelay'];
238
+ DISCORD_TYPING_MODE = config.discord.typingMode;
239
+ DISCORD_SELF_PRESENCE = JSON.parse(
240
+ JSON.stringify(config.discord.presence),
241
+ ) as RuntimeConfig['discord']['presence'];
242
+ DISCORD_LIFECYCLE_REACTIONS = JSON.parse(
243
+ JSON.stringify(config.discord.lifecycleReactions),
244
+ ) as RuntimeConfig['discord']['lifecycleReactions'];
245
+ DISCORD_ACK_REACTION = config.discord.ackReaction;
246
+ DISCORD_ACK_REACTION_SCOPE = config.discord.ackReactionScope;
247
+ DISCORD_REMOVE_ACK_AFTER_REPLY = config.discord.removeAckAfterReply;
248
+ DISCORD_DEBOUNCE_MS = Math.max(0, config.discord.debounceMs);
249
+ DISCORD_RATE_LIMIT_PER_USER = Math.max(0, config.discord.rateLimitPerUser);
250
+ DISCORD_RATE_LIMIT_EXEMPT_ROLES = [...config.discord.rateLimitExemptRoles];
251
+ DISCORD_SUPPRESS_PATTERNS = [...config.discord.suppressPatterns];
252
+ DISCORD_MAX_CONCURRENT_PER_CHANNEL = Math.max(
253
+ 1,
254
+ config.discord.maxConcurrentPerChannel,
255
+ );
256
+ DISCORD_GUILDS = JSON.parse(
257
+ JSON.stringify(config.discord.guilds),
258
+ ) as RuntimeConfig['discord']['guilds'];
147
259
 
148
260
  HYBRIDAI_BASE_URL = config.hybridai.baseUrl;
149
261
  HYBRIDAI_MODEL = config.hybridai.defaultModel;
150
262
  HYBRIDAI_CHATBOT_ID = config.hybridai.defaultChatbotId;
263
+ HYBRIDAI_MAX_TOKENS = Math.max(
264
+ 256,
265
+ Math.min(32_768, config.hybridai.maxTokens),
266
+ );
151
267
  HYBRIDAI_ENABLE_RAG = config.hybridai.enableRag;
152
268
  HYBRIDAI_MODELS = [...config.hybridai.models];
153
269
 
@@ -162,16 +278,19 @@ function applyRuntimeConfig(config: RuntimeConfig): void {
162
278
  HEARTBEAT_ENABLED = config.heartbeat.enabled;
163
279
  HEARTBEAT_INTERVAL = config.heartbeat.intervalMs;
164
280
  HEARTBEAT_CHANNEL = config.heartbeat.channel;
281
+ MEMORY_DECAY_RATE = config.memory.decayRate;
282
+ MEMORY_CONSOLIDATION_INTERVAL_HOURS =
283
+ config.memory.consolidationIntervalHours;
165
284
 
166
285
  HEALTH_HOST = config.ops.healthHost;
167
286
  HEALTH_PORT = config.ops.healthPort;
168
287
  WEB_API_TOKEN = process.env.WEB_API_TOKEN || config.ops.webApiToken;
169
288
  GATEWAY_BASE_URL = config.ops.gatewayBaseUrl;
170
289
  GATEWAY_API_TOKEN =
171
- process.env.GATEWAY_API_TOKEN
172
- || config.ops.gatewayApiToken
173
- || WEB_API_TOKEN
174
- || INTERNAL_GATEWAY_API_TOKEN;
290
+ process.env.GATEWAY_API_TOKEN ||
291
+ config.ops.gatewayApiToken ||
292
+ WEB_API_TOKEN ||
293
+ INTERNAL_GATEWAY_API_TOKEN;
175
294
  DB_PATH = config.ops.dbPath;
176
295
  DATA_DIR = path.dirname(DB_PATH);
177
296
 
@@ -183,41 +302,94 @@ function applyRuntimeConfig(config: RuntimeConfig): void {
183
302
  OBSERVABILITY_AGENT_ID = config.observability.agentId;
184
303
  OBSERVABILITY_LABEL = config.observability.label;
185
304
  OBSERVABILITY_ENVIRONMENT = config.observability.environment;
186
- OBSERVABILITY_FLUSH_INTERVAL_MS = Math.max(1_000, config.observability.flushIntervalMs);
187
- OBSERVABILITY_BATCH_MAX_EVENTS = Math.max(1, Math.min(1_000, config.observability.batchMaxEvents));
305
+ OBSERVABILITY_FLUSH_INTERVAL_MS = Math.max(
306
+ 1_000,
307
+ config.observability.flushIntervalMs,
308
+ );
309
+ OBSERVABILITY_BATCH_MAX_EVENTS = Math.max(
310
+ 1,
311
+ Math.min(1_000, config.observability.batchMaxEvents),
312
+ );
188
313
 
189
314
  SESSION_COMPACTION_ENABLED = config.sessionCompaction.enabled;
190
- SESSION_COMPACTION_THRESHOLD = Math.max(20, config.sessionCompaction.threshold);
315
+ SESSION_COMPACTION_TOKEN_BUDGET = Math.max(
316
+ 1_000,
317
+ config.sessionCompaction.tokenBudget,
318
+ );
319
+ SESSION_COMPACTION_BUDGET_RATIO = Math.max(
320
+ 0.05,
321
+ Math.min(1, config.sessionCompaction.budgetRatio),
322
+ );
323
+ SESSION_COMPACTION_THRESHOLD = Math.max(
324
+ 20,
325
+ config.sessionCompaction.threshold,
326
+ );
191
327
  SESSION_COMPACTION_KEEP_RECENT = Math.max(
192
328
  1,
193
- Math.min(config.sessionCompaction.keepRecent, SESSION_COMPACTION_THRESHOLD - 1),
329
+ Math.min(
330
+ config.sessionCompaction.keepRecent,
331
+ SESSION_COMPACTION_THRESHOLD - 1,
332
+ ),
333
+ );
334
+ SESSION_COMPACTION_SUMMARY_MAX_CHARS = Math.max(
335
+ 1_000,
336
+ config.sessionCompaction.summaryMaxChars,
337
+ );
338
+ PRE_COMPACTION_MEMORY_FLUSH_ENABLED =
339
+ config.sessionCompaction.preCompactionMemoryFlush.enabled;
340
+ PRE_COMPACTION_MEMORY_FLUSH_MAX_MESSAGES = Math.max(
341
+ 8,
342
+ config.sessionCompaction.preCompactionMemoryFlush.maxMessages,
343
+ );
344
+ PRE_COMPACTION_MEMORY_FLUSH_MAX_CHARS = Math.max(
345
+ 4_000,
346
+ config.sessionCompaction.preCompactionMemoryFlush.maxChars,
194
347
  );
195
- SESSION_COMPACTION_SUMMARY_MAX_CHARS = Math.max(1_000, config.sessionCompaction.summaryMaxChars);
196
- PRE_COMPACTION_MEMORY_FLUSH_ENABLED = config.sessionCompaction.preCompactionMemoryFlush.enabled;
197
- PRE_COMPACTION_MEMORY_FLUSH_MAX_MESSAGES = Math.max(8, config.sessionCompaction.preCompactionMemoryFlush.maxMessages);
198
- PRE_COMPACTION_MEMORY_FLUSH_MAX_CHARS = Math.max(4_000, config.sessionCompaction.preCompactionMemoryFlush.maxChars);
199
348
 
200
349
  PROACTIVE_ACTIVE_HOURS_ENABLED = config.proactive.activeHours.enabled;
201
350
  PROACTIVE_ACTIVE_HOURS_TIMEZONE = config.proactive.activeHours.timezone;
202
- PROACTIVE_ACTIVE_HOURS_START = Math.max(0, Math.min(23, config.proactive.activeHours.startHour));
203
- PROACTIVE_ACTIVE_HOURS_END = Math.max(0, Math.min(23, config.proactive.activeHours.endHour));
204
- PROACTIVE_QUEUE_OUTSIDE_HOURS = config.proactive.activeHours.queueOutsideHours;
351
+ PROACTIVE_ACTIVE_HOURS_START = Math.max(
352
+ 0,
353
+ Math.min(23, config.proactive.activeHours.startHour),
354
+ );
355
+ PROACTIVE_ACTIVE_HOURS_END = Math.max(
356
+ 0,
357
+ Math.min(23, config.proactive.activeHours.endHour),
358
+ );
359
+ PROACTIVE_QUEUE_OUTSIDE_HOURS =
360
+ config.proactive.activeHours.queueOutsideHours;
205
361
 
206
362
  PROACTIVE_DELEGATION_ENABLED = config.proactive.delegation.enabled;
207
- PROACTIVE_DELEGATION_MAX_CONCURRENT = Math.max(1, config.proactive.delegation.maxConcurrent);
208
- PROACTIVE_DELEGATION_MAX_DEPTH = Math.max(1, config.proactive.delegation.maxDepth);
209
- PROACTIVE_DELEGATION_MAX_PER_TURN = Math.max(1, config.proactive.delegation.maxPerTurn);
363
+ PROACTIVE_DELEGATION_MAX_CONCURRENT = Math.max(
364
+ 1,
365
+ config.proactive.delegation.maxConcurrent,
366
+ );
367
+ PROACTIVE_DELEGATION_MAX_DEPTH = Math.max(
368
+ 1,
369
+ config.proactive.delegation.maxDepth,
370
+ );
371
+ PROACTIVE_DELEGATION_MAX_PER_TURN = Math.max(
372
+ 1,
373
+ config.proactive.delegation.maxPerTurn,
374
+ );
210
375
 
211
376
  PROACTIVE_AUTO_RETRY_ENABLED = config.proactive.autoRetry.enabled;
212
- PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS = Math.max(1, config.proactive.autoRetry.maxAttempts);
213
- PROACTIVE_AUTO_RETRY_BASE_DELAY_MS = Math.max(100, config.proactive.autoRetry.baseDelayMs);
377
+ PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS = Math.max(
378
+ 1,
379
+ config.proactive.autoRetry.maxAttempts,
380
+ );
381
+ PROACTIVE_AUTO_RETRY_BASE_DELAY_MS = Math.max(
382
+ 100,
383
+ config.proactive.autoRetry.baseDelayMs,
384
+ );
214
385
  PROACTIVE_AUTO_RETRY_MAX_DELAY_MS = Math.max(
215
386
  PROACTIVE_AUTO_RETRY_BASE_DELAY_MS,
216
387
  config.proactive.autoRetry.maxDelayMs,
217
388
  );
218
389
 
219
390
  const rawRalphMax = Math.trunc(config.proactive.ralph.maxIterations);
220
- PROACTIVE_RALPH_MAX_ITERATIONS = rawRalphMax === -1 ? -1 : Math.max(0, rawRalphMax);
391
+ PROACTIVE_RALPH_MAX_ITERATIONS =
392
+ rawRalphMax === -1 ? -1 : Math.max(0, rawRalphMax);
221
393
  }
222
394
 
223
395
  applyRuntimeConfig(getRuntimeConfig());
@@ -2,7 +2,7 @@
2
2
  * Container Runner — manages a pool of persistent containers.
3
3
  * Containers stay alive between requests and exit after an idle timeout.
4
4
  */
5
- import { ChildProcess, spawn } from 'child_process';
5
+ import { type ChildProcess, spawn } from 'child_process';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
 
@@ -15,7 +15,9 @@ import {
15
15
  DATA_DIR,
16
16
  GATEWAY_API_TOKEN,
17
17
  GATEWAY_BASE_URL,
18
+ getHybridAIApiKey,
18
19
  HYBRIDAI_BASE_URL,
20
+ HYBRIDAI_MAX_TOKENS,
19
21
  HYBRIDAI_MODEL,
20
22
  MAX_CONCURRENT_CONTAINERS,
21
23
  PROACTIVE_AUTO_RETRY_BASE_DELAY_MS,
@@ -23,9 +25,15 @@ import {
23
25
  PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS,
24
26
  PROACTIVE_AUTO_RETRY_MAX_DELAY_MS,
25
27
  PROACTIVE_RALPH_MAX_ITERATIONS,
26
- getHybridAIApiKey,
27
28
  } from './config.js';
28
- import { cleanupIpc, ensureAgentDirs, ensureSessionDirs, getSessionPaths, readOutput, writeInput } from './ipc.js';
29
+ import {
30
+ cleanupIpc,
31
+ ensureAgentDirs,
32
+ ensureSessionDirs,
33
+ getSessionPaths,
34
+ readOutput,
35
+ writeInput,
36
+ } from './ipc.js';
29
37
  import { logger } from './logger.js';
30
38
  import { validateAdditionalMounts } from './mount-security.js';
31
39
  import type {
@@ -52,7 +60,8 @@ interface PoolEntry {
52
60
  }
53
61
 
54
62
  const pool = new Map<string, PoolEntry>();
55
- const TOOL_RESULT_RE = /^\[tool\]\s+([a-zA-Z0-9_.-]+)\s+result\s+\((\d+)ms\):\s*(.*)$/;
63
+ const TOOL_RESULT_RE =
64
+ /^\[tool\]\s+([a-zA-Z0-9_.-]+)\s+result\s+\((\d+)ms\):\s*(.*)$/;
56
65
  const TOOL_START_RE = /^\[tool\]\s+([a-zA-Z0-9_.-]+):\s*(.*)$/;
57
66
  const STREAM_DELTA_RE = /^\[stream\]\s+([A-Za-z0-9+/=]+)$/;
58
67
  const CONTAINER_WORKSPACE_ROOT = '/workspace';
@@ -73,7 +82,10 @@ function emitTextDelta(entry: PoolEntry, line: string): void {
73
82
  if (!delta) return;
74
83
  callback(delta);
75
84
  } catch (err) {
76
- logger.debug({ sessionId: entry.sessionId, err }, 'Text delta callback failed');
85
+ logger.debug(
86
+ { sessionId: entry.sessionId, err },
87
+ 'Text delta callback failed',
88
+ );
77
89
  }
78
90
  }
79
91
 
@@ -92,7 +104,10 @@ function emitToolProgress(entry: PoolEntry, line: string): void {
92
104
  preview: resultMatch[3],
93
105
  });
94
106
  } catch (err) {
95
- logger.debug({ sessionId: entry.sessionId, err }, 'Tool progress callback failed');
107
+ logger.debug(
108
+ { sessionId: entry.sessionId, err },
109
+ 'Tool progress callback failed',
110
+ );
96
111
  }
97
112
  return;
98
113
  }
@@ -107,7 +122,10 @@ function emitToolProgress(entry: PoolEntry, line: string): void {
107
122
  preview: startMatch[2],
108
123
  });
109
124
  } catch (err) {
110
- logger.debug({ sessionId: entry.sessionId, err }, 'Tool progress callback failed');
125
+ logger.debug(
126
+ { sessionId: entry.sessionId, err },
127
+ 'Tool progress callback failed',
128
+ );
111
129
  }
112
130
  }
113
131
  }
@@ -119,7 +137,10 @@ export function getActiveContainerCount(): number {
119
137
  export function stopSessionContainer(sessionId: string): boolean {
120
138
  const entry = pool.get(sessionId);
121
139
  if (!entry) return false;
122
- logger.info({ sessionId, containerName: entry.containerName }, 'Stopping session container');
140
+ logger.info(
141
+ { sessionId, containerName: entry.containerName },
142
+ 'Stopping session container',
143
+ );
123
144
  stopContainer(entry.containerName);
124
145
  pool.delete(sessionId);
125
146
  return true;
@@ -132,7 +153,10 @@ function stopContainer(containerName: string): void {
132
153
  });
133
154
  }
134
155
 
135
- function resolveArtifactHostPath(rawPath: string, workspacePath: string): string | null {
156
+ function resolveArtifactHostPath(
157
+ rawPath: string,
158
+ workspacePath: string,
159
+ ): string | null {
136
160
  const input = String(rawPath || '').trim();
137
161
  if (!input) return null;
138
162
  const normalized = input.replace(/\\/g, '/');
@@ -140,12 +164,20 @@ function resolveArtifactHostPath(rawPath: string, workspacePath: string): string
140
164
 
141
165
  if (path.posix.isAbsolute(normalized)) {
142
166
  const cleanAbs = path.posix.normalize(normalized);
143
- if (cleanAbs !== CONTAINER_WORKSPACE_ROOT && !cleanAbs.startsWith(`${CONTAINER_WORKSPACE_ROOT}/`)) {
167
+ if (
168
+ cleanAbs !== CONTAINER_WORKSPACE_ROOT &&
169
+ !cleanAbs.startsWith(`${CONTAINER_WORKSPACE_ROOT}/`)
170
+ ) {
144
171
  return null;
145
172
  }
146
- const rel = cleanAbs.slice(CONTAINER_WORKSPACE_ROOT.length).replace(/^\/+/, '');
173
+ const rel = cleanAbs
174
+ .slice(CONTAINER_WORKSPACE_ROOT.length)
175
+ .replace(/^\/+/, '');
147
176
  const resolved = path.resolve(workspaceRoot, rel);
148
- if (resolved === workspaceRoot || resolved.startsWith(`${workspaceRoot}${path.sep}`)) {
177
+ if (
178
+ resolved === workspaceRoot ||
179
+ resolved.startsWith(`${workspaceRoot}${path.sep}`)
180
+ ) {
149
181
  return resolved;
150
182
  }
151
183
  return null;
@@ -154,21 +186,32 @@ function resolveArtifactHostPath(rawPath: string, workspacePath: string): string
154
186
  const cleanRel = path.posix.normalize(normalized);
155
187
  if (cleanRel === '..' || cleanRel.startsWith('../')) return null;
156
188
  const resolved = path.resolve(workspaceRoot, cleanRel);
157
- if (resolved === workspaceRoot || resolved.startsWith(`${workspaceRoot}${path.sep}`)) {
189
+ if (
190
+ resolved === workspaceRoot ||
191
+ resolved.startsWith(`${workspaceRoot}${path.sep}`)
192
+ ) {
158
193
  return resolved;
159
194
  }
160
195
  return null;
161
196
  }
162
197
 
163
- function remapOutputArtifacts(output: ContainerOutput, workspacePath: string): void {
198
+ function remapOutputArtifacts(
199
+ output: ContainerOutput,
200
+ workspacePath: string,
201
+ ): void {
164
202
  if (!Array.isArray(output.artifacts) || output.artifacts.length === 0) return;
165
203
  const mapped: ArtifactMetadata[] = [];
166
204
  for (const artifact of output.artifacts) {
167
205
  const raw = artifact as Partial<ArtifactMetadata>;
168
- const hostPath = resolveArtifactHostPath(String(raw.path || ''), workspacePath);
206
+ const hostPath = resolveArtifactHostPath(
207
+ String(raw.path || ''),
208
+ workspacePath,
209
+ );
169
210
  if (!hostPath) continue;
170
- const filename = String(raw.filename || '').trim() || path.basename(hostPath);
171
- const mimeType = String(raw.mimeType || '').trim() || 'application/octet-stream';
211
+ const filename =
212
+ String(raw.filename || '').trim() || path.basename(hostPath);
213
+ const mimeType =
214
+ String(raw.mimeType || '').trim() || 'application/octet-stream';
172
215
  mapped.push({ path: hostPath, filename, mimeType });
173
216
  }
174
217
  if (mapped.length === 0) {
@@ -179,7 +222,10 @@ function remapOutputArtifacts(output: ContainerOutput, workspacePath: string): v
179
222
  }
180
223
 
181
224
  function remapHostBaseUrlForContainer(baseUrl: string): string {
182
- return baseUrl.replace(/\/\/(localhost|127\.0\.0\.1)([:\/])/, '//host.docker.internal$2');
225
+ return baseUrl.replace(
226
+ /\/\/(localhost|127\.0\.0\.1)([:/])/,
227
+ '//host.docker.internal$2',
228
+ );
183
229
  }
184
230
 
185
231
  /**
@@ -187,8 +233,15 @@ function remapHostBaseUrlForContainer(baseUrl: string): string {
187
233
  */
188
234
  function getOrSpawnContainer(sessionId: string, agentId: string): PoolEntry {
189
235
  const existing = pool.get(sessionId);
190
- if (existing && !existing.process.killed && existing.process.exitCode === null) {
191
- logger.debug({ sessionId, containerName: existing.containerName }, 'Reusing container');
236
+ if (
237
+ existing &&
238
+ !existing.process.killed &&
239
+ existing.process.exitCode === null
240
+ ) {
241
+ logger.debug(
242
+ { sessionId, containerName: existing.containerName },
243
+ 'Reusing container',
244
+ );
192
245
  return existing;
193
246
  }
194
247
 
@@ -208,23 +261,38 @@ function getOrSpawnContainer(sessionId: string, agentId: string): PoolEntry {
208
261
  'run',
209
262
  '--rm',
210
263
  '-i',
211
- '--name', containerName,
212
- '--memory', CONTAINER_MEMORY,
264
+ '--name',
265
+ containerName,
266
+ '--memory',
267
+ CONTAINER_MEMORY,
213
268
  `--cpus=${CONTAINER_CPUS}`,
214
269
  '--read-only',
215
- '--tmpfs', '/tmp',
216
- '-v', `${workspacePath}:/workspace:rw`,
217
- '-v', `${ipcPath}:/ipc:rw`,
218
- '-v', `${mediaCacheHostPath}:${CONTAINER_DISCORD_MEDIA_CACHE_ROOT}:ro`,
219
- '-e', `HYBRIDAI_BASE_URL=${HYBRIDAI_BASE_URL}`,
220
- '-e', `HYBRIDAI_MODEL=${HYBRIDAI_MODEL}`,
221
- '-e', `CONTAINER_IDLE_TIMEOUT=${IDLE_TIMEOUT_MS}`,
222
- '-e', `HYBRIDCLAW_RETRY_ENABLED=${PROACTIVE_AUTO_RETRY_ENABLED ? 'true' : 'false'}`,
223
- '-e', `HYBRIDCLAW_RETRY_MAX_ATTEMPTS=${PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS}`,
224
- '-e', `HYBRIDCLAW_RETRY_BASE_DELAY_MS=${PROACTIVE_AUTO_RETRY_BASE_DELAY_MS}`,
225
- '-e', `HYBRIDCLAW_RETRY_MAX_DELAY_MS=${PROACTIVE_AUTO_RETRY_MAX_DELAY_MS}`,
226
- '-e', `HYBRIDCLAW_RALPH_MAX_ITERATIONS=${PROACTIVE_RALPH_MAX_ITERATIONS}`,
227
- '-e', 'PLAYWRIGHT_BROWSERS_PATH=/ms-playwright',
270
+ '--tmpfs',
271
+ '/tmp',
272
+ '-v',
273
+ `${workspacePath}:/workspace:rw`,
274
+ '-v',
275
+ `${ipcPath}:/ipc:rw`,
276
+ '-v',
277
+ `${mediaCacheHostPath}:${CONTAINER_DISCORD_MEDIA_CACHE_ROOT}:ro`,
278
+ '-e',
279
+ `HYBRIDAI_BASE_URL=${HYBRIDAI_BASE_URL}`,
280
+ '-e',
281
+ `HYBRIDAI_MODEL=${HYBRIDAI_MODEL}`,
282
+ '-e',
283
+ `CONTAINER_IDLE_TIMEOUT=${IDLE_TIMEOUT_MS}`,
284
+ '-e',
285
+ `HYBRIDCLAW_RETRY_ENABLED=${PROACTIVE_AUTO_RETRY_ENABLED ? 'true' : 'false'}`,
286
+ '-e',
287
+ `HYBRIDCLAW_RETRY_MAX_ATTEMPTS=${PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS}`,
288
+ '-e',
289
+ `HYBRIDCLAW_RETRY_BASE_DELAY_MS=${PROACTIVE_AUTO_RETRY_BASE_DELAY_MS}`,
290
+ '-e',
291
+ `HYBRIDCLAW_RETRY_MAX_DELAY_MS=${PROACTIVE_AUTO_RETRY_MAX_DELAY_MS}`,
292
+ '-e',
293
+ `HYBRIDCLAW_RALPH_MAX_ITERATIONS=${PROACTIVE_RALPH_MAX_ITERATIONS}`,
294
+ '-e',
295
+ 'PLAYWRIGHT_BROWSERS_PATH=/ms-playwright',
228
296
  ];
229
297
 
230
298
  // Run as host user so bind-mount file ownership matches
@@ -241,10 +309,16 @@ function getOrSpawnContainer(sessionId: string, agentId: string): PoolEntry {
241
309
  const requested = JSON.parse(ADDITIONAL_MOUNTS) as AdditionalMount[];
242
310
  const validated = validateAdditionalMounts(requested);
243
311
  for (const m of validated) {
244
- args.push('-v', `${m.hostPath}:${m.containerPath}:${m.readonly ? 'ro' : 'rw'}`);
312
+ args.push(
313
+ '-v',
314
+ `${m.hostPath}:${m.containerPath}:${m.readonly ? 'ro' : 'rw'}`,
315
+ );
245
316
  }
246
317
  } catch (err) {
247
- logger.warn({ error: err instanceof Error ? err.message : String(err) }, 'Failed to parse ADDITIONAL_MOUNTS');
318
+ logger.warn(
319
+ { error: err instanceof Error ? err.message : String(err) },
320
+ 'Failed to parse ADDITIONAL_MOUNTS',
321
+ );
248
322
  }
249
323
  }
250
324
 
@@ -334,7 +408,10 @@ export async function runContainer(
334
408
  cleanupIpc(sessionId);
335
409
  ensureSessionDirs(sessionId);
336
410
 
337
- const isNewContainer = !pool.has(sessionId) || pool.get(sessionId)!.process.killed || pool.get(sessionId)!.process.exitCode !== null;
411
+ const isNewContainer =
412
+ !pool.has(sessionId) ||
413
+ pool.get(sessionId)!.process.killed ||
414
+ pool.get(sessionId)!.process.exitCode !== null;
338
415
 
339
416
  let entry: PoolEntry;
340
417
  try {
@@ -358,6 +435,7 @@ export async function runContainer(
358
435
  gatewayBaseUrl: remapHostBaseUrlForContainer(GATEWAY_BASE_URL),
359
436
  gatewayApiToken: GATEWAY_API_TOKEN || undefined,
360
437
  model,
438
+ maxTokens: HYBRIDAI_MAX_TOKENS,
361
439
  channelId,
362
440
  scheduledTasks: scheduledTasks?.map((t) => ({
363
441
  id: t.id,
@@ -377,7 +455,10 @@ export async function runContainer(
377
455
  entry.onTextDelta = onTextDelta;
378
456
  entry.onToolProgress = onToolProgress;
379
457
  const onAbort = () => {
380
- logger.info({ sessionId, containerName: entry.containerName }, 'Interrupt requested, stopping container');
458
+ logger.info(
459
+ { sessionId, containerName: entry.containerName },
460
+ 'Interrupt requested, stopping container',
461
+ );
381
462
  stopContainer(entry.containerName);
382
463
  };
383
464
  if (abortSignal) {
@@ -397,12 +478,20 @@ export async function runContainer(
397
478
  }
398
479
 
399
480
  // Wait for the container to produce output
400
- const output = await readOutput(sessionId, CONTAINER_TIMEOUT, { signal: abortSignal });
481
+ const output = await readOutput(sessionId, CONTAINER_TIMEOUT, {
482
+ signal: abortSignal,
483
+ });
401
484
  remapOutputArtifacts(output, workspacePath);
402
485
  const duration = Date.now() - startTime;
403
486
 
404
487
  logger.info(
405
- { sessionId, containerName: entry.containerName, duration, status: output.status, toolsUsed: output.toolsUsed },
488
+ {
489
+ sessionId,
490
+ containerName: entry.containerName,
491
+ duration,
492
+ status: output.status,
493
+ toolsUsed: output.toolsUsed,
494
+ },
406
495
  'Request completed',
407
496
  );
408
497
 
@@ -423,7 +512,10 @@ export async function runContainer(
423
512
  */
424
513
  export function stopAllContainers(): void {
425
514
  for (const [sessionId, entry] of pool) {
426
- logger.info({ sessionId, containerName: entry.containerName }, 'Stopping container (shutdown)');
515
+ logger.info(
516
+ { sessionId, containerName: entry.containerName },
517
+ 'Stopping container (shutdown)',
518
+ );
427
519
  stopContainer(entry.containerName);
428
520
  }
429
521
  pool.clear();