@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
package/src/health.ts CHANGED
@@ -1,21 +1,25 @@
1
1
  import fs from 'fs';
2
2
  import http, { type IncomingMessage, type ServerResponse } from 'http';
3
3
  import path from 'path';
4
-
5
- import { GATEWAY_API_TOKEN, HEALTH_HOST, HEALTH_PORT, WEB_API_TOKEN } from './config.js';
4
+ import { runDiscordToolAction } from './channels/discord/runtime.js';
5
+ import type { DiscordToolActionRequest } from './channels/discord/tool-actions.js';
6
+ import {
7
+ GATEWAY_API_TOKEN,
8
+ HEALTH_HOST,
9
+ HEALTH_PORT,
10
+ WEB_API_TOKEN,
11
+ } from './config.js';
6
12
  import {
13
+ type GatewayChatRequest,
14
+ type GatewayCommandRequest,
7
15
  getGatewayHistory,
8
16
  getGatewayStatus,
9
17
  handleGatewayCommand,
10
18
  handleGatewayMessage,
11
- type GatewayCommandRequest,
12
- type GatewayChatRequest,
13
19
  } from './gateway-service.js';
14
- import { type GatewayChatRequestBody } from './gateway-types.js';
15
- import { type DiscordToolActionRequest } from './channels/discord/tool-actions.js';
16
- import { runDiscordToolAction } from './channels/discord/runtime.js';
17
- import { type ToolProgressEvent } from './types.js';
20
+ import type { GatewayChatRequestBody } from './gateway-types.js';
18
21
  import { logger } from './logger.js';
22
+ import type { ToolProgressEvent } from './types.js';
19
23
 
20
24
  const SITE_DIR = path.resolve(process.cwd(), 'docs');
21
25
  const MAX_REQUEST_BYTES = 1_000_000; // 1MB
@@ -40,7 +44,8 @@ function isLoopbackAddress(address: string | undefined): boolean {
40
44
 
41
45
  function hasApiAuth(req: IncomingMessage): boolean {
42
46
  const authHeader = req.headers.authorization || '';
43
- const gatewayTokenMatch = Boolean(GATEWAY_API_TOKEN) && authHeader === `Bearer ${GATEWAY_API_TOKEN}`;
47
+ const gatewayTokenMatch =
48
+ Boolean(GATEWAY_API_TOKEN) && authHeader === `Bearer ${GATEWAY_API_TOKEN}`;
44
49
 
45
50
  if (!WEB_API_TOKEN) {
46
51
  return gatewayTokenMatch || isLoopbackAddress(req.socket.remoteAddress);
@@ -49,8 +54,14 @@ function hasApiAuth(req: IncomingMessage): boolean {
49
54
  return gatewayTokenMatch;
50
55
  }
51
56
 
52
- function sendJson(res: ServerResponse, statusCode: number, payload: unknown): void {
53
- res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
57
+ function sendJson(
58
+ res: ServerResponse,
59
+ statusCode: number,
60
+ payload: unknown,
61
+ ): void {
62
+ res.writeHead(statusCode, {
63
+ 'Content-Type': 'application/json; charset=utf-8',
64
+ });
54
65
  res.end(JSON.stringify(payload, null, 2));
55
66
  }
56
67
 
@@ -81,12 +92,15 @@ function resolveSiteFile(pathname: string): string | null {
81
92
  const normalized = path.normalize(cleanPath).replace(/^(\.\.(\/|\\|$))+/, '');
82
93
  const candidate = path.resolve(SITE_DIR, `.${normalized}`);
83
94
  if (!candidate.startsWith(SITE_DIR)) return null;
84
- if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) return null;
95
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile())
96
+ return null;
85
97
  return candidate;
86
98
  }
87
99
 
88
100
  function serveStatic(pathname: string, res: ServerResponse): boolean {
89
- const filePath = resolveSiteFile(pathname === '/chat' ? '/chat.html' : pathname);
101
+ const filePath = resolveSiteFile(
102
+ pathname === '/chat' ? '/chat.html' : pathname,
103
+ );
90
104
  if (!filePath) return false;
91
105
  const ext = path.extname(filePath).toLowerCase();
92
106
  const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
@@ -95,8 +109,11 @@ function serveStatic(pathname: string, res: ServerResponse): boolean {
95
109
  return true;
96
110
  }
97
111
 
98
- async function handleApiChat(req: IncomingMessage, res: ServerResponse): Promise<void> {
99
- const body = await readJsonBody(req) as Partial<ApiChatRequestBody>;
112
+ async function handleApiChat(
113
+ req: IncomingMessage,
114
+ res: ServerResponse,
115
+ ): Promise<void> {
116
+ const body = (await readJsonBody(req)) as Partial<ApiChatRequestBody>;
100
117
  const wantsStream = body.stream === true;
101
118
 
102
119
  const content = body.content?.trim();
@@ -177,7 +194,10 @@ async function handleApiChatStream(
177
194
  error: errorMessage,
178
195
  },
179
196
  });
180
- logger.error({ error, reqUrl: '/api/chat' }, 'Gateway streaming chat failed');
197
+ logger.error(
198
+ { error, reqUrl: '/api/chat' },
199
+ 'Gateway streaming chat failed',
200
+ );
181
201
  } finally {
182
202
  if (!res.writableEnded) {
183
203
  res.end();
@@ -191,11 +211,18 @@ async function handleApiChatStream(
191
211
  });
192
212
  }
193
213
 
194
- async function handleApiCommand(req: IncomingMessage, res: ServerResponse): Promise<void> {
195
- const body = await readJsonBody(req) as Partial<GatewayCommandRequest>;
196
- const args = Array.isArray(body.args) ? body.args.map((value) => String(value)) : [];
214
+ async function handleApiCommand(
215
+ req: IncomingMessage,
216
+ res: ServerResponse,
217
+ ): Promise<void> {
218
+ const body = (await readJsonBody(req)) as Partial<GatewayCommandRequest>;
219
+ const args = Array.isArray(body.args)
220
+ ? body.args.map((value) => String(value))
221
+ : [];
197
222
  if (args.length === 0) {
198
- sendJson(res, 400, { error: 'Missing command. Provide non-empty `args` array.' });
223
+ sendJson(res, 400, {
224
+ error: 'Missing command. Provide non-empty `args` array.',
225
+ });
199
226
  return;
200
227
  }
201
228
 
@@ -209,12 +236,20 @@ async function handleApiCommand(req: IncomingMessage, res: ServerResponse): Prom
209
236
  sendJson(res, result.kind === 'error' ? 400 : 200, result);
210
237
  }
211
238
 
212
- async function handleApiDiscordAction(req: IncomingMessage, res: ServerResponse): Promise<void> {
213
- const body = await readJsonBody(req) as ApiDiscordActionRequestBody;
239
+ async function handleApiDiscordAction(
240
+ req: IncomingMessage,
241
+ res: ServerResponse,
242
+ ): Promise<void> {
243
+ const body = (await readJsonBody(req)) as ApiDiscordActionRequestBody;
214
244
  const action = typeof body.action === 'string' ? body.action.trim() : '';
215
- if (action !== 'read' && action !== 'member-info' && action !== 'channel-info') {
245
+ if (
246
+ action !== 'read' &&
247
+ action !== 'member-info' &&
248
+ action !== 'channel-info'
249
+ ) {
216
250
  sendJson(res, 400, {
217
- error: 'Invalid `action`. Allowed: "read", "member-info", "channel-info".',
251
+ error:
252
+ 'Invalid `action`. Allowed: "read", "member-info", "channel-info".',
218
253
  });
219
254
  return;
220
255
  }
@@ -268,7 +303,9 @@ export function startHealthServer(): void {
268
303
 
269
304
  if (pathname.startsWith('/api/')) {
270
305
  if (!hasApiAuth(req)) {
271
- sendJson(res, 401, { error: 'Unauthorized. Set `Authorization: Bearer <WEB_API_TOKEN>`.' });
306
+ sendJson(res, 401, {
307
+ error: 'Unauthorized. Set `Authorization: Bearer <WEB_API_TOKEN>`.',
308
+ });
272
309
  return;
273
310
  }
274
311
 
@@ -312,6 +349,9 @@ export function startHealthServer(): void {
312
349
  });
313
350
 
314
351
  server.listen(HEALTH_PORT, HEALTH_HOST, () => {
315
- logger.info({ host: HEALTH_HOST, port: HEALTH_PORT }, 'Gateway HTTP server started');
352
+ logger.info(
353
+ { host: HEALTH_HOST, port: HEALTH_PORT },
354
+ 'Gateway HTTP server started',
355
+ );
316
356
  });
317
357
  }
package/src/heartbeat.ts CHANGED
@@ -2,17 +2,35 @@
2
2
  * Heartbeat — periodic poll so the agent can proactively check tasks,
3
3
  * maintain memory, and reach out when needed.
4
4
  */
5
- import { HEARTBEAT_CHANNEL, HEARTBEAT_ENABLED, HYBRIDAI_CHATBOT_ID, HYBRIDAI_ENABLE_RAG, HYBRIDAI_MODEL } from './config.js';
5
+
6
6
  import { runAgent } from './agent.js';
7
- import { getConversationHistory, getOrCreateSession, getTasksForSession, storeMessage } from './db.js';
7
+ import {
8
+ emitToolExecutionAuditEvents,
9
+ makeAuditRunId,
10
+ recordAuditEvent,
11
+ } from './audit-events.js';
12
+ import {
13
+ HEARTBEAT_CHANNEL,
14
+ HEARTBEAT_ENABLED,
15
+ HYBRIDAI_CHATBOT_ID,
16
+ HYBRIDAI_ENABLE_RAG,
17
+ HYBRIDAI_MODEL,
18
+ } from './config.js';
19
+ import { buildConversationContext } from './conversation.js';
20
+ import { getTasksForSession } from './db.js';
8
21
  import { logger } from './logger.js';
9
- import { processSideEffects } from './side-effects.js';
22
+ import { memoryService } from './memory-service.js';
23
+ import {
24
+ isWithinActiveHours,
25
+ proactiveWindowLabel,
26
+ } from './proactive-policy.js';
10
27
  import { maybeCompactSession } from './session-maintenance.js';
11
28
  import { appendSessionTranscript } from './session-transcripts.js';
12
- import { buildConversationContext } from './conversation.js';
13
- import { isWithinActiveHours, proactiveWindowLabel } from './proactive-policy.js';
14
- import { emitToolExecutionAuditEvents, makeAuditRunId, recordAuditEvent } from './audit-events.js';
15
- import { estimateTokenCountFromMessages, estimateTokenCountFromText } from './token-efficiency.js';
29
+ import { processSideEffects } from './side-effects.js';
30
+ import {
31
+ estimateTokenCountFromMessages,
32
+ estimateTokenCountFromText,
33
+ } from './token-efficiency.js';
16
34
 
17
35
  const HEARTBEAT_PROMPT =
18
36
  '[Heartbeat poll] Check HEARTBEAT.md for periodic tasks. If nothing needs attention, reply HEARTBEAT_OK.';
@@ -53,7 +71,10 @@ let timer: ReturnType<typeof setInterval> | null = null;
53
71
  let running = false;
54
72
 
55
73
  function isHeartbeatOk(text: string): boolean {
56
- const normalized = text.trim().replace(/[^a-z_]/gi, '').toUpperCase();
74
+ const normalized = text
75
+ .trim()
76
+ .replace(/[^a-z_]/gi, '')
77
+ .toUpperCase();
57
78
  return normalized === 'HEARTBEATOK' || normalized.startsWith('HEARTBEATOK');
58
79
  }
59
80
 
@@ -75,7 +96,10 @@ export function startHeartbeat(
75
96
  return;
76
97
  }
77
98
  if (!isWithinActiveHours()) {
78
- logger.debug({ activeHours: proactiveWindowLabel() }, 'Heartbeat skipped — outside active hours window');
99
+ logger.debug(
100
+ { activeHours: proactiveWindowLabel() },
101
+ 'Heartbeat skipped — outside active hours window',
102
+ );
79
103
  return;
80
104
  }
81
105
  running = true;
@@ -87,13 +111,24 @@ export function startHeartbeat(
87
111
  let turnIndex = 1;
88
112
 
89
113
  try {
90
- const session = getOrCreateSession(sessionId, null, channelId);
114
+ const session = memoryService.getOrCreateSession(
115
+ sessionId,
116
+ null,
117
+ channelId,
118
+ );
91
119
  turnIndex = session.message_count + 1;
92
120
 
93
- const history = getConversationHistory(sessionId, MAX_HEARTBEAT_HISTORY);
121
+ const history = memoryService.getConversationHistory(
122
+ sessionId,
123
+ MAX_HEARTBEAT_HISTORY,
124
+ );
125
+ const memoryContext = memoryService.buildPromptMemoryContext({
126
+ session,
127
+ query: HEARTBEAT_PROMPT,
128
+ });
94
129
  const { messages } = buildConversationContext({
95
130
  agentId,
96
- sessionSummary: session.session_summary,
131
+ sessionSummary: memoryContext.promptSummary,
97
132
  history,
98
133
  });
99
134
  messages.push({ role: 'user', content: HEARTBEAT_PROMPT });
@@ -141,13 +176,20 @@ export function startHeartbeat(
141
176
  toolExecutions: output.toolExecutions || [],
142
177
  });
143
178
  const tokenUsage = output.tokenUsage;
144
- const estimatedPromptTokens = tokenUsage?.estimatedPromptTokens || estimateTokenCountFromMessages(messages);
145
- const estimatedCompletionTokens = tokenUsage?.estimatedCompletionTokens || estimateTokenCountFromText(output.result || '');
146
- const estimatedTotalTokens = tokenUsage?.estimatedTotalTokens || (estimatedPromptTokens + estimatedCompletionTokens);
179
+ const estimatedPromptTokens =
180
+ tokenUsage?.estimatedPromptTokens ||
181
+ estimateTokenCountFromMessages(messages);
182
+ const estimatedCompletionTokens =
183
+ tokenUsage?.estimatedCompletionTokens ||
184
+ estimateTokenCountFromText(output.result || '');
185
+ const estimatedTotalTokens =
186
+ tokenUsage?.estimatedTotalTokens ||
187
+ estimatedPromptTokens + estimatedCompletionTokens;
147
188
  const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
148
189
  const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
149
190
  const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
150
- const apiTotalTokens = tokenUsage?.apiTotalTokens || (apiPromptTokens + apiCompletionTokens);
191
+ const apiTotalTokens =
192
+ tokenUsage?.apiTotalTokens || apiPromptTokens + apiCompletionTokens;
151
193
  recordAuditEvent({
152
194
  sessionId,
153
195
  runId,
@@ -158,9 +200,15 @@ export function startHeartbeat(
158
200
  durationMs: Date.now() - startedAt,
159
201
  toolCallCount: (output.toolExecutions || []).length,
160
202
  modelCalls: tokenUsage ? Math.max(1, tokenUsage.modelCalls) : 0,
161
- promptTokens: apiUsageAvailable ? apiPromptTokens : estimatedPromptTokens,
162
- completionTokens: apiUsageAvailable ? apiCompletionTokens : estimatedCompletionTokens,
163
- totalTokens: apiUsageAvailable ? apiTotalTokens : estimatedTotalTokens,
203
+ promptTokens: apiUsageAvailable
204
+ ? apiPromptTokens
205
+ : estimatedPromptTokens,
206
+ completionTokens: apiUsageAvailable
207
+ ? apiCompletionTokens
208
+ : estimatedCompletionTokens,
209
+ totalTokens: apiUsageAvailable
210
+ ? apiTotalTokens
211
+ : estimatedTotalTokens,
164
212
  estimatedPromptTokens,
165
213
  estimatedCompletionTokens,
166
214
  estimatedTotalTokens,
@@ -241,8 +289,19 @@ export function startHeartbeat(
241
289
  }
242
290
 
243
291
  // Real content — persist and deliver
244
- storeMessage(sessionId, 'heartbeat', 'heartbeat', 'user', HEARTBEAT_PROMPT);
245
- storeMessage(sessionId, 'assistant', null, 'assistant', result);
292
+ memoryService.storeTurn({
293
+ sessionId,
294
+ user: {
295
+ userId: 'heartbeat',
296
+ username: 'heartbeat',
297
+ content: HEARTBEAT_PROMPT,
298
+ },
299
+ assistant: {
300
+ userId: 'assistant',
301
+ username: null,
302
+ content: result,
303
+ },
304
+ });
246
305
  appendSessionTranscript(agentId, {
247
306
  sessionId,
248
307
  channelId: heartbeatChannelId,
@@ -267,7 +326,10 @@ export function startHeartbeat(
267
326
  model: HYBRIDAI_MODEL,
268
327
  channelId: heartbeatChannelId,
269
328
  });
270
- logger.info({ length: result.length }, 'Heartbeat: agent has something to say');
329
+ logger.info(
330
+ { length: result.length },
331
+ 'Heartbeat: agent has something to say',
332
+ );
271
333
  recordAuditEvent({
272
334
  sessionId,
273
335
  runId,
@@ -1,4 +1,4 @@
1
- import { HYBRIDAI_BASE_URL, getHybridAIApiKey } from './config.js';
1
+ import { getHybridAIApiKey, HYBRIDAI_BASE_URL } from './config.js';
2
2
  import type { HybridAIBot } from './types.js';
3
3
 
4
4
  interface BotCacheEntry {
@@ -10,18 +10,27 @@ let botCache: BotCacheEntry | null = null;
10
10
 
11
11
  function normalizeBots(payload: unknown): HybridAIBot[] {
12
12
  const data = payload as
13
- | { data?: Record<string, unknown>[]; bots?: Record<string, unknown>[]; items?: Record<string, unknown>[] }
13
+ | {
14
+ data?: Record<string, unknown>[];
15
+ bots?: Record<string, unknown>[];
16
+ items?: Record<string, unknown>[];
17
+ }
14
18
  | Record<string, unknown>[];
15
- const raw = Array.isArray(data) ? data : (data.data || data.bots || data.items || []);
19
+ const raw = Array.isArray(data)
20
+ ? data
21
+ : data.data || data.bots || data.items || [];
16
22
 
17
23
  return raw.map((item) => ({
18
24
  id: String(item.id ?? item._id ?? item.chatbot_id ?? item.bot_id ?? ''),
19
25
  name: String(item.bot_name ?? item.name ?? 'Unnamed'),
20
- description: item.description != null ? String(item.description) : undefined,
26
+ description:
27
+ item.description != null ? String(item.description) : undefined,
21
28
  }));
22
29
  }
23
30
 
24
- export async function fetchHybridAIBots(options?: { cacheTtlMs?: number }): Promise<HybridAIBot[]> {
31
+ export async function fetchHybridAIBots(options?: {
32
+ cacheTtlMs?: number;
33
+ }): Promise<HybridAIBot[]> {
25
34
  const cacheTtlMs = Math.max(0, options?.cacheTtlMs ?? 0);
26
35
  const now = Date.now();
27
36
 
@@ -22,7 +22,10 @@ function ensureAuditReady(): boolean {
22
22
  initDatabase({ quiet: true });
23
23
  auditReady = true;
24
24
  } catch (err) {
25
- logger.warn({ err }, 'Failed to initialize DB for instruction approval audit');
25
+ logger.warn(
26
+ { err },
27
+ 'Failed to initialize DB for instruction approval audit',
28
+ );
26
29
  auditReady = false;
27
30
  }
28
31
  return auditReady;
@@ -2,9 +2,18 @@ import { createHash } from 'crypto';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
 
5
- export const INSTRUCTION_FILES = ['AGENTS.md', 'SECURITY.md', 'TRUST_MODEL.md'] as const;
5
+ export const INSTRUCTION_FILES = [
6
+ 'AGENTS.md',
7
+ 'SECURITY.md',
8
+ 'TRUST_MODEL.md',
9
+ ] as const;
6
10
  export const INSTRUCTION_BASELINE_VERSION = 1;
7
- export const INSTRUCTION_BASELINE_PATH = path.join(process.cwd(), 'data', 'audit', 'instruction-hashes.json');
11
+ export const INSTRUCTION_BASELINE_PATH = path.join(
12
+ process.cwd(),
13
+ 'data',
14
+ 'audit',
15
+ 'instruction-hashes.json',
16
+ );
8
17
 
9
18
  export interface InstructionHashBaseline {
10
19
  version: number;
@@ -29,7 +38,9 @@ export interface InstructionIntegrityResult {
29
38
  files: InstructionFileResult[];
30
39
  }
31
40
 
32
- export function summarizeInstructionIntegrity(result: InstructionIntegrityResult): string {
41
+ export function summarizeInstructionIntegrity(
42
+ result: InstructionIntegrityResult,
43
+ ): string {
33
44
  if (result.baselineError) return `baseline.invalid (${result.baselineError})`;
34
45
  if (!result.baseline) return 'baseline.missing';
35
46
 
@@ -62,19 +73,24 @@ export function loadInstructionBaseline(): InstructionHashBaseline | null {
62
73
 
63
74
  const raw = fs.readFileSync(INSTRUCTION_BASELINE_PATH, 'utf-8');
64
75
  const parsed = JSON.parse(raw) as unknown;
65
- if (!isRecord(parsed)) throw new Error('Instruction baseline is not a JSON object.');
76
+ if (!isRecord(parsed))
77
+ throw new Error('Instruction baseline is not a JSON object.');
66
78
 
67
79
  const version = parsed.version;
68
80
  const approvedAt = parsed.approvedAt;
69
81
  const files = parsed.files;
70
- if (typeof version !== 'number') throw new Error('Instruction baseline is missing numeric `version`.');
82
+ if (typeof version !== 'number')
83
+ throw new Error('Instruction baseline is missing numeric `version`.');
71
84
  if (version !== INSTRUCTION_BASELINE_VERSION) {
72
- throw new Error(`Instruction baseline version ${String(version)} is unsupported.`);
85
+ throw new Error(
86
+ `Instruction baseline version ${String(version)} is unsupported.`,
87
+ );
73
88
  }
74
89
  if (typeof approvedAt !== 'string' || !approvedAt.trim()) {
75
90
  throw new Error('Instruction baseline is missing `approvedAt`.');
76
91
  }
77
- if (!isRecord(files)) throw new Error('Instruction baseline is missing `files` object.');
92
+ if (!isRecord(files))
93
+ throw new Error('Instruction baseline is missing `files` object.');
78
94
 
79
95
  const normalizedFiles: Record<string, string> = {};
80
96
  for (const relPath of INSTRUCTION_FILES) {
@@ -95,7 +111,9 @@ export function approveInstructionBaseline(): InstructionHashBaseline {
95
111
  const hashes = computeCurrentHashes();
96
112
  const missing = INSTRUCTION_FILES.filter((relPath) => !hashes[relPath]);
97
113
  if (missing.length > 0) {
98
- throw new Error(`Approval failed: missing instruction files (${missing.join(', ')}).`);
114
+ throw new Error(
115
+ `Approval failed: missing instruction files (${missing.join(', ')}).`,
116
+ );
99
117
  }
100
118
 
101
119
  const baseline: InstructionHashBaseline = {
@@ -165,7 +183,10 @@ export function verifyInstructionBaseline(): InstructionIntegrityResult {
165
183
  };
166
184
  });
167
185
 
168
- const ok = baselineError === null && baseline !== null && files.every((file) => file.status === 'ok');
186
+ const ok =
187
+ baselineError === null &&
188
+ baseline !== null &&
189
+ files.every((file) => file.status === 'ok');
169
190
  return {
170
191
  ok,
171
192
  baselinePath: INSTRUCTION_BASELINE_PATH,
package/src/ipc.ts CHANGED
@@ -46,7 +46,11 @@ export function ensureAgentDirs(agentId: string): void {
46
46
  * When omitApiKey is set, the apiKey field is excluded from the file on disk
47
47
  * (the container already has the key in memory from the initial stdin payload).
48
48
  */
49
- export function writeInput(sessionId: string, input: ContainerInput, opts?: { omitApiKey?: boolean }): string {
49
+ export function writeInput(
50
+ sessionId: string,
51
+ input: ContainerInput,
52
+ opts?: { omitApiKey?: boolean },
53
+ ): string {
50
54
  const dir = ipcDir(sessionId);
51
55
  const inputPath = path.join(dir, 'input.json');
52
56
  const toWrite = opts?.omitApiKey ? { ...input, apiKey: '' } : input;
@@ -67,7 +71,10 @@ function interruptedOutput(): ContainerOutput {
67
71
  };
68
72
  }
69
73
 
70
- async function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<boolean> {
74
+ async function sleepWithAbort(
75
+ ms: number,
76
+ signal?: AbortSignal,
77
+ ): Promise<boolean> {
71
78
  if (!signal) {
72
79
  await new Promise((resolve) => setTimeout(resolve, ms));
73
80
  return false;
@@ -109,8 +116,16 @@ export async function readOutput(
109
116
  const stat = fs.statSync(outputPath);
110
117
  if (stat.size > CONTAINER_MAX_OUTPUT_SIZE) {
111
118
  fs.unlinkSync(outputPath);
112
- logger.warn({ sessionId, size: stat.size, limit: CONTAINER_MAX_OUTPUT_SIZE }, 'Container output exceeded size limit');
113
- return { status: 'error', result: null, toolsUsed: [], error: `Output too large (${stat.size} bytes, limit ${CONTAINER_MAX_OUTPUT_SIZE})` };
119
+ logger.warn(
120
+ { sessionId, size: stat.size, limit: CONTAINER_MAX_OUTPUT_SIZE },
121
+ 'Container output exceeded size limit',
122
+ );
123
+ return {
124
+ status: 'error',
125
+ result: null,
126
+ toolsUsed: [],
127
+ error: `Output too large (${stat.size} bytes, limit ${CONTAINER_MAX_OUTPUT_SIZE})`,
128
+ };
114
129
  }
115
130
  try {
116
131
  const raw = fs.readFileSync(outputPath, 'utf-8');
@@ -152,7 +167,10 @@ export function cleanupIpc(sessionId: string): void {
152
167
  /**
153
168
  * Get host paths for container mounting.
154
169
  */
155
- export function getSessionPaths(sessionId: string, agentId: string): {
170
+ export function getSessionPaths(
171
+ sessionId: string,
172
+ agentId: string,
173
+ ): {
156
174
  ipcPath: string;
157
175
  workspacePath: string;
158
176
  } {
package/src/logger.ts CHANGED
@@ -12,7 +12,10 @@ export const logger = pino({
12
12
  onRuntimeConfigChange((next, prev) => {
13
13
  if (next.ops.logLevel !== prev.ops.logLevel) {
14
14
  logger.level = next.ops.logLevel;
15
- logger.info({ level: next.ops.logLevel }, 'Logger level updated from config.json');
15
+ logger.info(
16
+ { level: next.ops.logLevel },
17
+ 'Logger level updated from config.json',
18
+ );
16
19
  }
17
20
  });
18
21
 
@@ -0,0 +1,41 @@
1
+ import type { MemoryBackend } from './memory-service.js';
2
+
3
+ export interface MemoryConsolidationConfig {
4
+ decayRate: number;
5
+ staleAfterDays: number;
6
+ minConfidence: number;
7
+ }
8
+
9
+ export interface MemoryConsolidationReport {
10
+ memoriesDecayed: number;
11
+ durationMs: number;
12
+ }
13
+
14
+ export class MemoryConsolidationEngine {
15
+ private readonly backend: MemoryBackend;
16
+ private readonly config: MemoryConsolidationConfig;
17
+
18
+ constructor(backend: MemoryBackend, config: MemoryConsolidationConfig) {
19
+ this.backend = backend;
20
+ this.config = config;
21
+ }
22
+
23
+ consolidate(
24
+ overrides?: Partial<MemoryConsolidationConfig>,
25
+ ): MemoryConsolidationReport {
26
+ const start = Date.now();
27
+ const config = {
28
+ ...this.config,
29
+ ...(overrides || {}),
30
+ };
31
+ const memoriesDecayed = this.backend.decaySemanticMemories({
32
+ decayRate: config.decayRate,
33
+ staleAfterDays: config.staleAfterDays,
34
+ minConfidence: config.minConfidence,
35
+ });
36
+ return {
37
+ memoriesDecayed,
38
+ durationMs: Math.max(0, Date.now() - start),
39
+ };
40
+ }
41
+ }