@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/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,24 @@ 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;
193
+ const apiCacheUsageAvailable =
194
+ tokenUsage?.apiCacheUsageAvailable === true;
195
+ const apiCacheReadTokens = tokenUsage?.apiCacheReadTokens || 0;
196
+ const apiCacheWriteTokens = tokenUsage?.apiCacheWriteTokens || 0;
151
197
  recordAuditEvent({
152
198
  sessionId,
153
199
  runId,
@@ -158,9 +204,15 @@ export function startHeartbeat(
158
204
  durationMs: Date.now() - startedAt,
159
205
  toolCallCount: (output.toolExecutions || []).length,
160
206
  modelCalls: tokenUsage ? Math.max(1, tokenUsage.modelCalls) : 0,
161
- promptTokens: apiUsageAvailable ? apiPromptTokens : estimatedPromptTokens,
162
- completionTokens: apiUsageAvailable ? apiCompletionTokens : estimatedCompletionTokens,
163
- totalTokens: apiUsageAvailable ? apiTotalTokens : estimatedTotalTokens,
207
+ promptTokens: apiUsageAvailable
208
+ ? apiPromptTokens
209
+ : estimatedPromptTokens,
210
+ completionTokens: apiUsageAvailable
211
+ ? apiCompletionTokens
212
+ : estimatedCompletionTokens,
213
+ totalTokens: apiUsageAvailable
214
+ ? apiTotalTokens
215
+ : estimatedTotalTokens,
164
216
  estimatedPromptTokens,
165
217
  estimatedCompletionTokens,
166
218
  estimatedTotalTokens,
@@ -168,6 +220,17 @@ export function startHeartbeat(
168
220
  apiPromptTokens,
169
221
  apiCompletionTokens,
170
222
  apiTotalTokens,
223
+ ...(apiCacheUsageAvailable
224
+ ? {
225
+ apiCacheUsageAvailable,
226
+ apiCacheReadTokens,
227
+ apiCacheWriteTokens,
228
+ cacheReadTokens: apiCacheReadTokens,
229
+ cacheReadInputTokens: apiCacheReadTokens,
230
+ cacheWriteTokens: apiCacheWriteTokens,
231
+ cacheWriteInputTokens: apiCacheWriteTokens,
232
+ }
233
+ : {}),
171
234
  },
172
235
  });
173
236
  processSideEffects(output, sessionId, heartbeatChannelId);
@@ -241,8 +304,19 @@ export function startHeartbeat(
241
304
  }
242
305
 
243
306
  // Real content — persist and deliver
244
- storeMessage(sessionId, 'heartbeat', 'heartbeat', 'user', HEARTBEAT_PROMPT);
245
- storeMessage(sessionId, 'assistant', null, 'assistant', result);
307
+ memoryService.storeTurn({
308
+ sessionId,
309
+ user: {
310
+ userId: 'heartbeat',
311
+ username: 'heartbeat',
312
+ content: HEARTBEAT_PROMPT,
313
+ },
314
+ assistant: {
315
+ userId: 'assistant',
316
+ username: null,
317
+ content: result,
318
+ },
319
+ });
246
320
  appendSessionTranscript(agentId, {
247
321
  sessionId,
248
322
  channelId: heartbeatChannelId,
@@ -267,7 +341,10 @@ export function startHeartbeat(
267
341
  model: HYBRIDAI_MODEL,
268
342
  channelId: heartbeatChannelId,
269
343
  });
270
- logger.info({ length: result.length }, 'Heartbeat: agent has something to say');
344
+ logger.info(
345
+ { length: result.length },
346
+ 'Heartbeat: agent has something to say',
347
+ );
271
348
  recordAuditEvent({
272
349
  sessionId,
273
350
  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
 
@@ -0,0 +1,158 @@
1
+ interface HybridAIModel {
2
+ id: string;
3
+ contextWindowTokens: number | null;
4
+ }
5
+
6
+ // Source: ../../examples/pi-mono/packages/ai/src/models.generated.ts
7
+ // Keep this list intentionally small and focused on the GPT-5 family we use.
8
+ const STATIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
9
+ // Claude 4.6
10
+ 'claude-opus-4-6': 200_000,
11
+ 'claude-opus-4.6': 200_000,
12
+ 'claude-sonnet-4-6': 200_000,
13
+ 'claude-sonnet-4.6': 200_000,
14
+
15
+ // Gemini 3 / 3.1
16
+ 'gemini-3': 1_048_576,
17
+ 'gemini-3-pro': 1_048_576,
18
+ 'gemini-3-flash': 1_048_576,
19
+ 'gemini-3.1': 1_048_576,
20
+ 'gemini-3.1-pro': 1_048_576,
21
+ 'gemini-3.1-pro-high': 1_048_576,
22
+ 'gemini-3.1-pro-low': 1_048_576,
23
+ 'gemini-3-pro-preview': 1_048_576,
24
+ 'gemini-3-flash-preview': 1_048_576,
25
+ 'gemini-3.1-pro-preview': 1_048_576,
26
+
27
+ // GPT-5 family
28
+ 'gpt-5': 400_000,
29
+ 'gpt-5-chat-latest': 128_000,
30
+ 'gpt-5-codex': 400_000,
31
+ 'gpt-5-mini': 400_000,
32
+ 'gpt-5-nano': 400_000,
33
+ 'gpt-5-pro': 400_000,
34
+ 'gpt-5.1': 400_000,
35
+ 'gpt-5.1-chat-latest': 128_000,
36
+ 'gpt-5.1-codex': 400_000,
37
+ 'gpt-5.1-codex-max': 400_000,
38
+ 'gpt-5.1-codex-mini': 400_000,
39
+ 'gpt-5.2': 400_000,
40
+ 'gpt-5.2-chat-latest': 128_000,
41
+ 'gpt-5.2-codex': 400_000,
42
+ 'gpt-5.2-pro': 400_000,
43
+ 'gpt-5.3-codex': 400_000,
44
+ 'gpt-5.3-codex-spark': 128_000,
45
+ };
46
+
47
+ export function resolveModelContextWindowFromList(
48
+ models: HybridAIModel[],
49
+ modelName: string,
50
+ ): number | null {
51
+ const normalizeModelIdTail = (modelId: string): string => {
52
+ const normalized = modelId.trim().toLowerCase();
53
+ return normalized.includes('/')
54
+ ? (normalized.split('/').at(-1) ?? normalized)
55
+ : normalized;
56
+ };
57
+ const matchesModelFamily = (
58
+ candidateId: string,
59
+ targetId: string,
60
+ ): boolean => {
61
+ if (!candidateId || !targetId) return false;
62
+ if (candidateId === targetId) return true;
63
+ const boundary = candidateId.at(targetId.length);
64
+ return (
65
+ candidateId.startsWith(targetId) &&
66
+ (boundary === '-' ||
67
+ boundary === '.' ||
68
+ boundary === ':' ||
69
+ boundary === '/')
70
+ );
71
+ };
72
+
73
+ const target = modelName.trim().toLowerCase();
74
+ if (!target) return null;
75
+
76
+ const direct = models.find(
77
+ (entry) =>
78
+ entry.contextWindowTokens != null &&
79
+ entry.id.trim().toLowerCase() === target,
80
+ );
81
+ if (direct?.contextWindowTokens != null) return direct.contextWindowTokens;
82
+
83
+ const targetTail = target.includes('/')
84
+ ? (target.split('/').at(-1) ?? '')
85
+ : target;
86
+ if (!targetTail) return null;
87
+
88
+ const tailMatch = models.find((entry) => {
89
+ if (entry.contextWindowTokens == null) return false;
90
+ const normalizedTail = normalizeModelIdTail(entry.id);
91
+ return normalizedTail === targetTail;
92
+ });
93
+ if (tailMatch?.contextWindowTokens != null)
94
+ return tailMatch.contextWindowTokens;
95
+
96
+ const familyMatch = models
97
+ .filter((entry) => entry.contextWindowTokens != null)
98
+ .map((entry) => ({
99
+ contextWindowTokens: entry.contextWindowTokens as number,
100
+ tail: normalizeModelIdTail(entry.id),
101
+ }))
102
+ .filter((entry) => matchesModelFamily(entry.tail, targetTail))
103
+ .sort((a, b) => b.tail.length - a.tail.length)
104
+ .at(0);
105
+ return familyMatch?.contextWindowTokens ?? null;
106
+ }
107
+
108
+ export function resolveModelContextWindowFallback(
109
+ modelName: string,
110
+ ): number | null {
111
+ const matchesModelFamily = (
112
+ candidateId: string,
113
+ targetId: string,
114
+ ): boolean => {
115
+ if (!candidateId || !targetId) return false;
116
+ if (candidateId === targetId) return true;
117
+ const boundary = candidateId.at(targetId.length);
118
+ return (
119
+ candidateId.startsWith(targetId) &&
120
+ (boundary === '-' ||
121
+ boundary === '.' ||
122
+ boundary === ':' ||
123
+ boundary === '/')
124
+ );
125
+ };
126
+
127
+ const normalized = modelName.trim().toLowerCase();
128
+ if (!normalized) return null;
129
+
130
+ const direct = STATIC_MODEL_CONTEXT_WINDOWS[normalized];
131
+ if (direct != null) return direct;
132
+
133
+ const slashTail = normalized.includes('/')
134
+ ? (normalized.split('/').at(-1) ?? '')
135
+ : normalized;
136
+ if (slashTail && STATIC_MODEL_CONTEXT_WINDOWS[slashTail] != null) {
137
+ return STATIC_MODEL_CONTEXT_WINDOWS[slashTail];
138
+ }
139
+
140
+ const colonTail = normalized.includes(':')
141
+ ? (normalized.split(':').at(-1) ?? '')
142
+ : normalized;
143
+ if (colonTail && STATIC_MODEL_CONTEXT_WINDOWS[colonTail] != null) {
144
+ return STATIC_MODEL_CONTEXT_WINDOWS[colonTail];
145
+ }
146
+
147
+ // Family fallback for versioned ids, e.g. "gpt-5.1-2025-11-13".
148
+ const familyCandidates = [slashTail, colonTail, normalized].filter(Boolean);
149
+ for (const candidate of familyCandidates) {
150
+ const bestMatch = Object.keys(STATIC_MODEL_CONTEXT_WINDOWS)
151
+ .filter((key) => matchesModelFamily(candidate, key))
152
+ .sort((a, b) => b.length - a.length)
153
+ .at(0);
154
+ if (bestMatch) return STATIC_MODEL_CONTEXT_WINDOWS[bestMatch] ?? null;
155
+ }
156
+
157
+ return null;
158
+ }
@@ -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,