@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
@@ -73,7 +73,11 @@ export function loadMountAllowlist(): MountAllowlist | null {
73
73
 
74
74
  cachedAllowlist = allowlist;
75
75
  logger.info(
76
- { path: MOUNT_ALLOWLIST_PATH, allowedRoots: allowlist.allowedRoots.length, blockedPatterns: allowlist.blockedPatterns.length },
76
+ {
77
+ path: MOUNT_ALLOWLIST_PATH,
78
+ allowedRoots: allowlist.allowedRoots.length,
79
+ blockedPatterns: allowlist.blockedPatterns.length,
80
+ },
77
81
  'Mount allowlist loaded',
78
82
  );
79
83
  return cachedAllowlist;
@@ -102,7 +106,10 @@ function getRealPath(p: string): string | null {
102
106
  }
103
107
  }
104
108
 
105
- function matchesBlockedPattern(realPath: string, blockedPatterns: string[]): string | null {
109
+ function matchesBlockedPattern(
110
+ realPath: string,
111
+ blockedPatterns: string[],
112
+ ): string | null {
106
113
  const pathParts = realPath.split(path.sep);
107
114
  for (const pattern of blockedPatterns) {
108
115
  for (const part of pathParts) {
@@ -113,7 +120,10 @@ function matchesBlockedPattern(realPath: string, blockedPatterns: string[]): str
113
120
  return null;
114
121
  }
115
122
 
116
- function findAllowedRoot(realPath: string, allowedRoots: AllowedRoot[]): AllowedRoot | null {
123
+ function findAllowedRoot(
124
+ realPath: string,
125
+ allowedRoots: AllowedRoot[],
126
+ ): AllowedRoot | null {
117
127
  for (const root of allowedRoots) {
118
128
  const realRoot = getRealPath(expandPath(root.path));
119
129
  if (realRoot === null) continue;
@@ -144,23 +154,38 @@ export interface MountValidationResult {
144
154
  export function validateMount(mount: AdditionalMount): MountValidationResult {
145
155
  const allowlist = loadMountAllowlist();
146
156
  if (allowlist === null) {
147
- return { allowed: false, reason: `No mount allowlist configured at ${MOUNT_ALLOWLIST_PATH}` };
157
+ return {
158
+ allowed: false,
159
+ reason: `No mount allowlist configured at ${MOUNT_ALLOWLIST_PATH}`,
160
+ };
148
161
  }
149
162
 
150
163
  const containerPath = mount.containerPath || path.basename(mount.hostPath);
151
164
  if (!isValidContainerPath(containerPath)) {
152
- return { allowed: false, reason: `Invalid container path: "${containerPath}" — must be relative, non-empty, and not contain ".."` };
165
+ return {
166
+ allowed: false,
167
+ reason: `Invalid container path: "${containerPath}" — must be relative, non-empty, and not contain ".."`,
168
+ };
153
169
  }
154
170
 
155
171
  const expandedPath = expandPath(mount.hostPath);
156
172
  const realPath = getRealPath(expandedPath);
157
173
  if (realPath === null) {
158
- return { allowed: false, reason: `Host path does not exist: "${mount.hostPath}" (expanded: "${expandedPath}")` };
174
+ return {
175
+ allowed: false,
176
+ reason: `Host path does not exist: "${mount.hostPath}" (expanded: "${expandedPath}")`,
177
+ };
159
178
  }
160
179
 
161
- const blockedMatch = matchesBlockedPattern(realPath, allowlist.blockedPatterns);
180
+ const blockedMatch = matchesBlockedPattern(
181
+ realPath,
182
+ allowlist.blockedPatterns,
183
+ );
162
184
  if (blockedMatch !== null) {
163
- return { allowed: false, reason: `Path matches blocked pattern "${blockedMatch}": "${realPath}"` };
185
+ return {
186
+ allowed: false,
187
+ reason: `Path matches blocked pattern "${blockedMatch}": "${realPath}"`,
188
+ };
164
189
  }
165
190
 
166
191
  const allowedRoot = findAllowedRoot(realPath, allowlist.allowedRoots);
@@ -176,7 +201,10 @@ export function validateMount(mount: AdditionalMount): MountValidationResult {
176
201
  if (mount.readonly === false && allowedRoot.allowReadWrite) {
177
202
  effectiveReadonly = false;
178
203
  } else if (mount.readonly === false && !allowedRoot.allowReadWrite) {
179
- logger.info({ mount: mount.hostPath, root: allowedRoot.path }, 'Mount forced to read-only — root does not allow read-write');
204
+ logger.info(
205
+ { mount: mount.hostPath, root: allowedRoot.path },
206
+ 'Mount forced to read-only — root does not allow read-write',
207
+ );
180
208
  }
181
209
 
182
210
  return {
@@ -195,7 +223,11 @@ export function validateMount(mount: AdditionalMount): MountValidationResult {
195
223
  export function validateAdditionalMounts(
196
224
  mounts: AdditionalMount[],
197
225
  ): Array<{ hostPath: string; containerPath: string; readonly: boolean }> {
198
- const validated: Array<{ hostPath: string; containerPath: string; readonly: boolean }> = [];
226
+ const validated: Array<{
227
+ hostPath: string;
228
+ containerPath: string;
229
+ readonly: boolean;
230
+ }> = [];
199
231
 
200
232
  for (const mount of mounts) {
201
233
  const result = validateMount(mount);
@@ -205,12 +237,25 @@ export function validateAdditionalMounts(
205
237
  containerPath: `/workspace/extra/${result.resolvedContainerPath}`,
206
238
  readonly: result.effectiveReadonly!,
207
239
  });
208
- logger.debug({ hostPath: result.realHostPath, containerPath: result.resolvedContainerPath, readonly: result.effectiveReadonly }, 'Mount validated');
240
+ logger.debug(
241
+ {
242
+ hostPath: result.realHostPath,
243
+ containerPath: result.resolvedContainerPath,
244
+ readonly: result.effectiveReadonly,
245
+ },
246
+ 'Mount validated',
247
+ );
209
248
  } else {
210
- logger.warn({ requestedPath: mount.hostPath, containerPath: mount.containerPath, reason: result.reason }, 'Additional mount REJECTED');
249
+ logger.warn(
250
+ {
251
+ requestedPath: mount.hostPath,
252
+ containerPath: mount.containerPath,
253
+ reason: result.reason,
254
+ },
255
+ 'Additional mount REJECTED',
256
+ );
211
257
  }
212
258
  }
213
259
 
214
260
  return validated;
215
261
  }
216
-
@@ -133,7 +133,9 @@ function normalizeIngestUrl(baseUrl: string, ingestPath: string): string {
133
133
  const trimmedPath = ingestPath.trim();
134
134
  if (/^https?:\/\//i.test(trimmedPath)) return trimmedPath;
135
135
  const normalizedBase = baseUrl.replace(/\/+$/, '');
136
- const normalizedPath = trimmedPath.startsWith('/') ? trimmedPath : `/${trimmedPath}`;
136
+ const normalizedPath = trimmedPath.startsWith('/')
137
+ ? trimmedPath
138
+ : `/${trimmedPath}`;
137
139
  return `${normalizedBase}${normalizedPath}`;
138
140
  }
139
141
 
@@ -141,12 +143,23 @@ function resolveConfig(): ResolvedIngestConfig {
141
143
  const botId = OBSERVABILITY_BOT_ID.trim() || HYBRIDAI_CHATBOT_ID.trim();
142
144
  const label = OBSERVABILITY_LABEL.trim() || os.hostname();
143
145
  const environment = OBSERVABILITY_ENVIRONMENT.trim() || 'prod';
144
- const batchMaxEvents = clampInteger(OBSERVABILITY_BATCH_MAX_EVENTS, 1, PLATFORM_MAX_EVENTS);
145
- const flushIntervalMs = clampInteger(OBSERVABILITY_FLUSH_INTERVAL_MS, 1_000, 3_600_000);
146
+ const batchMaxEvents = clampInteger(
147
+ OBSERVABILITY_BATCH_MAX_EVENTS,
148
+ 1,
149
+ PLATFORM_MAX_EVENTS,
150
+ );
151
+ const flushIntervalMs = clampInteger(
152
+ OBSERVABILITY_FLUSH_INTERVAL_MS,
153
+ 1_000,
154
+ 3_600_000,
155
+ );
146
156
 
147
157
  return {
148
158
  enabled: OBSERVABILITY_ENABLED,
149
- ingestUrl: normalizeIngestUrl(OBSERVABILITY_BASE_URL, OBSERVABILITY_INGEST_PATH),
159
+ ingestUrl: normalizeIngestUrl(
160
+ OBSERVABILITY_BASE_URL,
161
+ OBSERVABILITY_INGEST_PATH,
162
+ ),
150
163
  tokenAdminUrl: normalizeIngestUrl(OBSERVABILITY_BASE_URL, TOKEN_ADMIN_PATH),
151
164
  apiKey: HYBRIDAI_API_KEY.trim(),
152
165
  botId,
@@ -159,12 +172,24 @@ function resolveConfig(): ResolvedIngestConfig {
159
172
  };
160
173
  }
161
174
 
162
- function validateConfig(config: ResolvedIngestConfig): { ok: boolean; reason: string | null } {
175
+ function validateConfig(config: ResolvedIngestConfig): {
176
+ ok: boolean;
177
+ reason: string | null;
178
+ } {
163
179
  if (!config.enabled) return { ok: false, reason: 'disabled' };
164
- if (!config.botId) return { ok: false, reason: 'missing observability.botId (or hybridai.defaultChatbotId)' };
165
- if (!config.agentId) return { ok: false, reason: 'missing observability.agentId' };
180
+ if (!config.botId)
181
+ return {
182
+ ok: false,
183
+ reason: 'missing observability.botId (or hybridai.defaultChatbotId)',
184
+ };
185
+ if (!config.agentId)
186
+ return { ok: false, reason: 'missing observability.agentId' };
166
187
  if (!config.apiKey) {
167
- return { ok: false, reason: 'missing HYBRIDAI_API_KEY (needed to auto-fetch observability ingest token)' };
188
+ return {
189
+ ok: false,
190
+ reason:
191
+ 'missing HYBRIDAI_API_KEY (needed to auto-fetch observability ingest token)',
192
+ };
168
193
  }
169
194
  return { ok: true, reason: null };
170
195
  }
@@ -205,12 +230,18 @@ function parsePayload(raw: string): Record<string, unknown> {
205
230
  return {};
206
231
  }
207
232
 
208
- function readNullableString(payload: Record<string, unknown>, key: string): string | null {
233
+ function readNullableString(
234
+ payload: Record<string, unknown>,
235
+ key: string,
236
+ ): string | null {
209
237
  const value = payload[key];
210
238
  return typeof value === 'string' && value.trim() ? value : null;
211
239
  }
212
240
 
213
- function readNullableInteger(payload: Record<string, unknown>, key: string): number | null {
241
+ function readNullableInteger(
242
+ payload: Record<string, unknown>,
243
+ key: string,
244
+ ): number | null {
214
245
  const value = payload[key];
215
246
  if (typeof value === 'number' && Number.isFinite(value)) {
216
247
  return Math.floor(value);
@@ -222,7 +253,10 @@ function readNullableInteger(payload: Record<string, unknown>, key: string): num
222
253
  return null;
223
254
  }
224
255
 
225
- function readNullableBoolean(payload: Record<string, unknown>, key: string): boolean | null {
256
+ function readNullableBoolean(
257
+ payload: Record<string, unknown>,
258
+ key: string,
259
+ ): boolean | null {
226
260
  const value = payload[key];
227
261
  if (typeof value === 'boolean') return value;
228
262
  if (typeof value === 'string') {
@@ -240,7 +274,10 @@ function inferDenied(payload: Record<string, unknown>): boolean {
240
274
  return false;
241
275
  }
242
276
 
243
- function buildEventUid(config: ResolvedIngestConfig, row: StructuredAuditEntry): string {
277
+ function buildEventUid(
278
+ config: ResolvedIngestConfig,
279
+ row: StructuredAuditEntry,
280
+ ): string {
244
281
  const raw = [
245
282
  config.botId,
246
283
  config.agentId,
@@ -253,7 +290,10 @@ function buildEventUid(config: ResolvedIngestConfig, row: StructuredAuditEntry):
253
290
  return createHash('sha256').update(raw).digest('hex');
254
291
  }
255
292
 
256
- function mapAuditRowToEvent(config: ResolvedIngestConfig, row: StructuredAuditEntry): Record<string, unknown> {
293
+ function mapAuditRowToEvent(
294
+ config: ResolvedIngestConfig,
295
+ row: StructuredAuditEntry,
296
+ ): Record<string, unknown> {
257
297
  const payload = parsePayload(row.payload);
258
298
  return {
259
299
  session_id: row.session_id,
@@ -274,9 +314,18 @@ function mapAuditRowToEvent(config: ResolvedIngestConfig, row: StructuredAuditEn
274
314
  prompt_tokens: readNullableInteger(payload, 'promptTokens'),
275
315
  completion_tokens: readNullableInteger(payload, 'completionTokens'),
276
316
  total_tokens: readNullableInteger(payload, 'totalTokens'),
277
- estimated_prompt_tokens: readNullableInteger(payload, 'estimatedPromptTokens'),
278
- estimated_completion_tokens: readNullableInteger(payload, 'estimatedCompletionTokens'),
279
- estimated_total_tokens: readNullableInteger(payload, 'estimatedTotalTokens'),
317
+ estimated_prompt_tokens: readNullableInteger(
318
+ payload,
319
+ 'estimatedPromptTokens',
320
+ ),
321
+ estimated_completion_tokens: readNullableInteger(
322
+ payload,
323
+ 'estimatedCompletionTokens',
324
+ ),
325
+ estimated_total_tokens: readNullableInteger(
326
+ payload,
327
+ 'estimatedTotalTokens',
328
+ ),
280
329
  api_usage_available: readNullableBoolean(payload, 'apiUsageAvailable'),
281
330
  api_prompt_tokens: readNullableInteger(payload, 'apiPromptTokens'),
282
331
  api_completion_tokens: readNullableInteger(payload, 'apiCompletionTokens'),
@@ -302,7 +351,11 @@ function buildBatchPayloadText(
302
351
  });
303
352
  }
304
353
 
305
- function prepareBatch(config: ResolvedIngestConfig, rows: StructuredAuditEntry[], currentCursor: number): PreparedBatch {
354
+ function prepareBatch(
355
+ config: ResolvedIngestConfig,
356
+ rows: StructuredAuditEntry[],
357
+ currentCursor: number,
358
+ ): PreparedBatch {
306
359
  const selectedEvents: Record<string, unknown>[] = [];
307
360
  const droppedEventIds: number[] = [];
308
361
  let lastEventId = currentCursor;
@@ -366,7 +419,10 @@ function formatGrantError(prefix: string, grant: TokenGrantResult): string {
366
419
  return `${prefix}: HTTP ${grant.statusCode}`;
367
420
  }
368
421
 
369
- async function requestIngestToken(config: ResolvedIngestConfig, rotate = false): Promise<TokenGrantResult> {
422
+ async function requestIngestToken(
423
+ config: ResolvedIngestConfig,
424
+ rotate = false,
425
+ ): Promise<TokenGrantResult> {
370
426
  let response: Response;
371
427
  try {
372
428
  response = await fetch(config.tokenAdminUrl, {
@@ -411,7 +467,10 @@ async function requestIngestToken(config: ResolvedIngestConfig, rotate = false):
411
467
  rotated: rotate,
412
468
  token: null,
413
469
  message: parseMessage(payload.message),
414
- errorText: parseMessage(payload.message) || rawText || `${response.status} ${response.statusText}`,
470
+ errorText:
471
+ parseMessage(payload.message) ||
472
+ rawText ||
473
+ `${response.status} ${response.statusText}`,
415
474
  };
416
475
  }
417
476
 
@@ -419,7 +478,7 @@ async function requestIngestToken(config: ResolvedIngestConfig, rotate = false):
419
478
  const successFlagPresent = payload.success != null;
420
479
  const success = successFlagPresent
421
480
  ? parseBoolean(payload.success)
422
- : (statusText === null || statusText === 'ok' || statusText === 'accepted');
481
+ : statusText === null || statusText === 'ok' || statusText === 'accepted';
423
482
  const token = parseMessage(payload.token);
424
483
  const created = parseBoolean(payload.created);
425
484
  const rotated = parseBoolean(payload.rotated) || rotate;
@@ -444,7 +503,7 @@ async function requestIngestToken(config: ResolvedIngestConfig, rotate = false):
444
503
  rotated,
445
504
  token,
446
505
  message,
447
- errorText: token ? '' : (rawText || ''),
506
+ errorText: token ? '' : rawText || '',
448
507
  };
449
508
  }
450
509
 
@@ -457,14 +516,16 @@ async function resolveIngestToken(
457
516
  ok: false,
458
517
  token: null,
459
518
  source: null,
460
- reason: 'missing HYBRIDAI_API_KEY (needed to auto-fetch observability ingest token)',
519
+ reason:
520
+ 'missing HYBRIDAI_API_KEY (needed to auto-fetch observability ingest token)',
461
521
  };
462
522
  }
463
523
 
464
524
  const tokenKey = buildTokenCacheKey(config);
465
525
  if (!forceRefresh) {
466
526
  const cached = getObservabilityIngestToken(tokenKey);
467
- if (cached) return { ok: true, token: cached, source: 'cache', reason: null };
527
+ if (cached)
528
+ return { ok: true, token: cached, source: 'cache', reason: null };
468
529
  } else {
469
530
  deleteObservabilityIngestToken(tokenKey);
470
531
  const rotated = await requestIngestToken(config, true);
@@ -473,11 +534,17 @@ async function resolveIngestToken(
473
534
  ok: false,
474
535
  token: null,
475
536
  source: null,
476
- reason: formatGrantError('failed to rotate observability ingest token', rotated),
537
+ reason: formatGrantError(
538
+ 'failed to rotate observability ingest token',
539
+ rotated,
540
+ ),
477
541
  };
478
542
  }
479
543
  if (!rotated.token) {
480
- const message = rotated.message || rotated.errorText || 'token rotate endpoint returned no token';
544
+ const message =
545
+ rotated.message ||
546
+ rotated.errorText ||
547
+ 'token rotate endpoint returned no token';
481
548
  return {
482
549
  ok: false,
483
550
  token: null,
@@ -500,7 +567,10 @@ async function resolveIngestToken(
500
567
  ok: false,
501
568
  token: null,
502
569
  source: null,
503
- reason: formatGrantError('failed to ensure observability ingest token', granted),
570
+ reason: formatGrantError(
571
+ 'failed to ensure observability ingest token',
572
+ granted,
573
+ ),
504
574
  };
505
575
  }
506
576
  if (!granted.token) {
@@ -510,11 +580,19 @@ async function resolveIngestToken(
510
580
  ok: false,
511
581
  token: null,
512
582
  source: null,
513
- reason: formatGrantError('failed to rotate observability ingest token', rotated),
583
+ reason: formatGrantError(
584
+ 'failed to rotate observability ingest token',
585
+ rotated,
586
+ ),
514
587
  };
515
588
  }
516
589
  if (!rotated.token) {
517
- const message = rotated.message || rotated.errorText || granted.message || granted.errorText || 'token rotate endpoint returned no token';
590
+ const message =
591
+ rotated.message ||
592
+ rotated.errorText ||
593
+ granted.message ||
594
+ granted.errorText ||
595
+ 'token rotate endpoint returned no token';
518
596
  return {
519
597
  ok: false,
520
598
  token: null,
@@ -540,7 +618,11 @@ async function resolveIngestToken(
540
618
  };
541
619
  }
542
620
 
543
- async function postBatch(config: ResolvedIngestConfig, token: string, payloadText: string): Promise<IngestResult> {
621
+ async function postBatch(
622
+ config: ResolvedIngestConfig,
623
+ token: string,
624
+ payloadText: string,
625
+ ): Promise<IngestResult> {
544
626
  let response: Response;
545
627
  try {
546
628
  response = await fetch(config.ingestUrl, {
@@ -595,7 +677,12 @@ async function postBatch(config: ResolvedIngestConfig, token: string, payloadTex
595
677
  }
596
678
 
597
679
  function isPauseStatus(statusCode: number): boolean {
598
- return statusCode === 400 || statusCode === 401 || statusCode === 403 || statusCode === 413;
680
+ return (
681
+ statusCode === 400 ||
682
+ statusCode === 401 ||
683
+ statusCode === 403 ||
684
+ statusCode === 413
685
+ );
599
686
  }
600
687
 
601
688
  async function flushObservability(reason: string): Promise<void> {
@@ -618,7 +705,8 @@ async function flushObservability(reason: string): Promise<void> {
618
705
 
619
706
  const initialToken = await resolveIngestToken(config);
620
707
  if (!initialToken.ok || !initialToken.token) {
621
- ingestState.reason = initialToken.reason || 'failed to resolve observability ingest token';
708
+ ingestState.reason =
709
+ initialToken.reason || 'failed to resolve observability ingest token';
622
710
  ingestState.lastFailureAt = new Date().toISOString();
623
711
  ingestState.lastError = ingestState.reason;
624
712
  logger.warn(
@@ -636,7 +724,11 @@ async function flushObservability(reason: string): Promise<void> {
636
724
  ingestState.streamKey = streamKey;
637
725
  let cursor = getObservabilityOffset(streamKey);
638
726
  ingestState.lastCursor = cursor;
639
- const fetchLimit = clampInteger(config.batchMaxEvents * FETCH_LIMIT_FACTOR, config.batchMaxEvents, 5_000);
727
+ const fetchLimit = clampInteger(
728
+ config.batchMaxEvents * FETCH_LIMIT_FACTOR,
729
+ config.batchMaxEvents,
730
+ 5_000,
731
+ );
640
732
 
641
733
  while (true) {
642
734
  const rows = getStructuredAuditAfterId(cursor, fetchLimit);
@@ -663,7 +755,10 @@ async function flushObservability(reason: string): Promise<void> {
663
755
  }
664
756
 
665
757
  let result = await postBatch(config, activeToken, batch.payloadText);
666
- if (!result.ok && (result.statusCode === 401 || result.statusCode === 403)) {
758
+ if (
759
+ !result.ok &&
760
+ (result.statusCode === 401 || result.statusCode === 403)
761
+ ) {
667
762
  const refreshed = await resolveIngestToken(config, true);
668
763
  if (refreshed.ok && refreshed.token) {
669
764
  activeToken = refreshed.token;
@@ -677,7 +772,8 @@ async function flushObservability(reason: string): Promise<void> {
677
772
  );
678
773
  result = await postBatch(config, activeToken, batch.payloadText);
679
774
  } else {
680
- const refreshReason = refreshed.reason || 'unknown token refresh failure';
775
+ const refreshReason =
776
+ refreshed.reason || 'unknown token refresh failure';
681
777
  result = {
682
778
  ...result,
683
779
  errorText: `${result.errorText} | token refresh failed: ${refreshReason}`,
@@ -754,7 +850,10 @@ export function startObservabilityIngest(): void {
754
850
  if (!validation.ok) {
755
851
  ingestState.reason = validation.reason;
756
852
  if (config.enabled) {
757
- logger.warn({ reason: validation.reason }, 'Observability ingest not started');
853
+ logger.warn(
854
+ { reason: validation.reason },
855
+ 'Observability ingest not started',
856
+ );
758
857
  }
759
858
  return;
760
859
  }