@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/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
+ }
@@ -0,0 +1,606 @@
1
+ import {
2
+ addKnowledgeEntity as dbAddKnowledgeEntity,
3
+ addKnowledgeRelation as dbAddKnowledgeRelation,
4
+ appendCanonicalMessages as dbAppendCanonicalMessages,
5
+ clearSessionHistory as dbClearSessionHistory,
6
+ deleteMemoryValue as dbDeleteMemoryValue,
7
+ deleteMessagesBeforeId as dbDeleteMessagesBeforeId,
8
+ forgetSemanticMemory as dbForgetSemanticMemory,
9
+ getCanonicalContext as dbGetCanonicalContext,
10
+ getCompactionCandidateMessages as dbGetCompactionCandidateMessages,
11
+ getConversationHistory as dbGetConversationHistory,
12
+ getMemoryValue as dbGetMemoryValue,
13
+ getOrCreateSession as dbGetOrCreateSession,
14
+ getRecentMessages as dbGetRecentMessages,
15
+ getSessionById as dbGetSessionById,
16
+ listMemoryValues as dbListMemoryValues,
17
+ markSessionMemoryFlush as dbMarkSessionMemoryFlush,
18
+ queryKnowledgeGraph as dbQueryKnowledgeGraph,
19
+ recallSemanticMemories as dbRecallSemanticMemories,
20
+ setMemoryValue as dbSetMemoryValue,
21
+ storeMessage as dbStoreMessage,
22
+ storeSemanticMemory as dbStoreSemanticMemory,
23
+ updateSessionSummary as dbUpdateSessionSummary,
24
+ decaySemanticMemories,
25
+ type SemanticRecallFilter,
26
+ } from './db.js';
27
+ import {
28
+ type MemoryConsolidationConfig,
29
+ MemoryConsolidationEngine,
30
+ type MemoryConsolidationReport,
31
+ } from './memory-consolidation.js';
32
+ import type {
33
+ CanonicalSession,
34
+ CanonicalSessionContext,
35
+ KnowledgeEntityTypeValue,
36
+ KnowledgeGraphMatch,
37
+ KnowledgeGraphPattern,
38
+ KnowledgeRelationTypeValue,
39
+ SemanticMemoryEntry,
40
+ Session,
41
+ StoredMessage,
42
+ StructuredMemoryEntry,
43
+ } from './types.js';
44
+
45
+ export interface CompactionCandidate {
46
+ cutoffId: number;
47
+ olderMessages: StoredMessage[];
48
+ }
49
+
50
+ export interface MemoryBackend {
51
+ getOrCreateSession: (
52
+ sessionId: string,
53
+ guildId: string | null,
54
+ channelId: string,
55
+ ) => Session;
56
+ getSessionById: (sessionId: string) => Session | undefined;
57
+ getConversationHistory: (
58
+ sessionId: string,
59
+ limit?: number,
60
+ ) => StoredMessage[];
61
+ getRecentMessages: (sessionId: string, limit?: number) => StoredMessage[];
62
+ get: (sessionId: string, key: string) => unknown | null;
63
+ set: (sessionId: string, key: string, value: unknown) => void;
64
+ delete: (sessionId: string, key: string) => boolean;
65
+ list: (sessionId: string, prefix?: string) => StructuredMemoryEntry[];
66
+ appendCanonicalMessages: (params: {
67
+ agentId: string;
68
+ userId: string;
69
+ newMessages: Array<{
70
+ role: string;
71
+ content: string;
72
+ sessionId: string;
73
+ channelId?: string | null;
74
+ createdAt?: string | null;
75
+ }>;
76
+ windowSize?: number;
77
+ compactionThreshold?: number;
78
+ }) => CanonicalSession;
79
+ getCanonicalContext: (params: {
80
+ agentId: string;
81
+ userId: string;
82
+ windowSize?: number;
83
+ excludeSessionId?: string | null;
84
+ }) => CanonicalSessionContext;
85
+ addKnowledgeEntity: (params: {
86
+ id?: string | null;
87
+ name: string;
88
+ entityType: KnowledgeEntityTypeValue | string;
89
+ properties?: Record<string, unknown> | null;
90
+ }) => string;
91
+ addKnowledgeRelation: (params: {
92
+ source: string;
93
+ relation: KnowledgeRelationTypeValue | string;
94
+ target: string;
95
+ properties?: Record<string, unknown> | null;
96
+ confidence?: number;
97
+ }) => string;
98
+ queryKnowledgeGraph: (
99
+ pattern?: KnowledgeGraphPattern,
100
+ ) => KnowledgeGraphMatch[];
101
+ getCompactionCandidateMessages: (
102
+ sessionId: string,
103
+ keepRecent: number,
104
+ ) => CompactionCandidate | null;
105
+ storeMessage: (
106
+ sessionId: string,
107
+ userId: string,
108
+ username: string | null,
109
+ role: string,
110
+ content: string,
111
+ ) => number;
112
+ storeSemanticMemory: (params: {
113
+ sessionId: string;
114
+ role: string;
115
+ source?: string | null;
116
+ scope?: string | null;
117
+ metadata?: Record<string, unknown> | string | null;
118
+ content: string;
119
+ confidence?: number;
120
+ embedding?: number[] | null;
121
+ sourceMessageId?: number | null;
122
+ }) => number;
123
+ recallSemanticMemories: (params: {
124
+ sessionId: string;
125
+ query: string;
126
+ limit?: number;
127
+ minConfidence?: number;
128
+ queryEmbedding?: number[] | null;
129
+ filter?: SemanticRecallFilter;
130
+ }) => SemanticMemoryEntry[];
131
+ forgetSemanticMemory: (id: number) => boolean;
132
+ decaySemanticMemories: (params?: {
133
+ decayRate?: number;
134
+ staleAfterDays?: number;
135
+ minConfidence?: number;
136
+ }) => number;
137
+ clearSessionHistory: (sessionId: string) => number;
138
+ deleteMessagesBeforeId: (sessionId: string, cutoffId: number) => number;
139
+ updateSessionSummary: (sessionId: string, summary: string) => void;
140
+ markSessionMemoryFlush: (sessionId: string) => void;
141
+ }
142
+
143
+ export interface MemoryServiceConfig {
144
+ semanticRecallLimit: number;
145
+ semanticMinConfidence: number;
146
+ semanticMaxContentChars: number;
147
+ semanticDecayRate: number;
148
+ semanticDecayStaleAfterDays: number;
149
+ semanticDecayMinConfidence: number;
150
+ summaryDecayRate: number;
151
+ summaryMinConfidence: number;
152
+ summaryDiscardThreshold: number;
153
+ embeddingDimensions: number;
154
+ }
155
+
156
+ export interface EmbeddingProvider {
157
+ embed(text: string): number[] | null;
158
+ }
159
+
160
+ export interface StoreTurnParams {
161
+ sessionId: string;
162
+ user: {
163
+ userId: string;
164
+ username: string | null;
165
+ content: string;
166
+ };
167
+ assistant: {
168
+ userId?: string;
169
+ username?: string | null;
170
+ content: string;
171
+ };
172
+ }
173
+
174
+ export interface BuildMemoryPromptParams {
175
+ session: Session;
176
+ query: string;
177
+ semanticLimit?: number;
178
+ }
179
+
180
+ export interface BuildMemoryPromptResult {
181
+ promptSummary: string | null;
182
+ summaryConfidence: number | null;
183
+ semanticMemories: SemanticMemoryEntry[];
184
+ }
185
+
186
+ export interface RecallSemanticMemoriesParams {
187
+ sessionId: string;
188
+ query: string;
189
+ limit?: number;
190
+ minConfidence?: number;
191
+ filter?: SemanticRecallFilter;
192
+ }
193
+
194
+ const DEFAULT_CONFIG: MemoryServiceConfig = {
195
+ semanticRecallLimit: 5,
196
+ semanticMinConfidence: 0.2,
197
+ semanticMaxContentChars: 1_200,
198
+ semanticDecayRate: 0.1,
199
+ semanticDecayStaleAfterDays: 7,
200
+ semanticDecayMinConfidence: 0.1,
201
+ summaryDecayRate: 0.04,
202
+ summaryMinConfidence: 0.1,
203
+ summaryDiscardThreshold: 0.22,
204
+ embeddingDimensions: 128,
205
+ };
206
+
207
+ const DEFAULT_BACKEND: MemoryBackend = {
208
+ getOrCreateSession: dbGetOrCreateSession,
209
+ getSessionById: dbGetSessionById,
210
+ getConversationHistory: dbGetConversationHistory,
211
+ getRecentMessages: dbGetRecentMessages,
212
+ get: dbGetMemoryValue,
213
+ set: dbSetMemoryValue,
214
+ delete: dbDeleteMemoryValue,
215
+ list: dbListMemoryValues,
216
+ appendCanonicalMessages: dbAppendCanonicalMessages,
217
+ getCanonicalContext: dbGetCanonicalContext,
218
+ addKnowledgeEntity: dbAddKnowledgeEntity,
219
+ addKnowledgeRelation: dbAddKnowledgeRelation,
220
+ queryKnowledgeGraph: dbQueryKnowledgeGraph,
221
+ getCompactionCandidateMessages: dbGetCompactionCandidateMessages,
222
+ storeMessage: dbStoreMessage,
223
+ storeSemanticMemory: dbStoreSemanticMemory,
224
+ recallSemanticMemories: dbRecallSemanticMemories,
225
+ forgetSemanticMemory: dbForgetSemanticMemory,
226
+ decaySemanticMemories,
227
+ clearSessionHistory: dbClearSessionHistory,
228
+ deleteMessagesBeforeId: dbDeleteMessagesBeforeId,
229
+ updateSessionSummary: dbUpdateSessionSummary,
230
+ markSessionMemoryFlush: dbMarkSessionMemoryFlush,
231
+ };
232
+
233
+ function parseTimestamp(raw: string | null | undefined): number | null {
234
+ const value = (raw || '').trim();
235
+ if (!value) return null;
236
+ if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value)) {
237
+ const parsed = Date.parse(`${value.replace(' ', 'T')}Z`);
238
+ return Number.isNaN(parsed) ? null : parsed;
239
+ }
240
+ const parsed = Date.parse(value);
241
+ return Number.isNaN(parsed) ? null : parsed;
242
+ }
243
+
244
+ export function computeDecayedConfidence(params: {
245
+ updatedAt: string | null | undefined;
246
+ decayRate: number;
247
+ minConfidence: number;
248
+ nowMs?: number;
249
+ }): number {
250
+ const updatedMs = parseTimestamp(params.updatedAt);
251
+ if (updatedMs == null) return 1;
252
+
253
+ const nowMs =
254
+ typeof params.nowMs === 'number' && Number.isFinite(params.nowMs)
255
+ ? params.nowMs
256
+ : Date.now();
257
+ const ageDays = Math.max(0, (nowMs - updatedMs) / 86_400_000);
258
+ const decayRate = Math.max(0, Math.min(0.95, params.decayRate));
259
+ const minConfidence = Math.max(0, Math.min(0.95, params.minConfidence));
260
+ const decayed = (1 - decayRate) ** ageDays;
261
+ return Math.max(minConfidence, Math.min(1, decayed));
262
+ }
263
+
264
+ function truncateInline(content: string, maxChars: number): string {
265
+ const compact = content.replace(/\s+/g, ' ').trim();
266
+ if (compact.length <= maxChars) return compact;
267
+ return `${compact.slice(0, maxChars)}...`;
268
+ }
269
+
270
+ class HashedTokenEmbeddingProvider implements EmbeddingProvider {
271
+ private readonly dimensions: number;
272
+
273
+ constructor(dimensions: number) {
274
+ this.dimensions = Math.max(16, Math.min(1024, Math.floor(dimensions)));
275
+ }
276
+
277
+ embed(text: string): number[] | null {
278
+ const normalized = text
279
+ .toLowerCase()
280
+ .replace(/[^a-z0-9_\s-]/g, ' ')
281
+ .replace(/\s+/g, ' ')
282
+ .trim();
283
+ if (!normalized) return null;
284
+
285
+ const tokens = normalized
286
+ .split(' ')
287
+ .map((token) => token.trim())
288
+ .filter((token) => token.length > 1)
289
+ .slice(0, 256);
290
+ if (tokens.length === 0) return null;
291
+
292
+ const vector = new Float32Array(this.dimensions);
293
+ for (const token of tokens) {
294
+ const hash = this.hashToken(token);
295
+ const index = hash % this.dimensions;
296
+ const sign = (hash & 1) === 0 ? 1 : -1;
297
+ vector[index] += sign * Math.min(4, token.length);
298
+ }
299
+
300
+ let norm = 0;
301
+ for (let i = 0; i < vector.length; i += 1) {
302
+ norm += vector[i] * vector[i];
303
+ }
304
+ if (norm <= Number.EPSILON) return null;
305
+ const scale = 1 / Math.sqrt(norm);
306
+ return Array.from(vector, (value) => value * scale);
307
+ }
308
+
309
+ private hashToken(token: string): number {
310
+ let hash = 2166136261;
311
+ for (let i = 0; i < token.length; i += 1) {
312
+ hash ^= token.charCodeAt(i);
313
+ hash = Math.imul(hash, 16777619);
314
+ }
315
+ return hash >>> 0;
316
+ }
317
+ }
318
+
319
+ export class MemoryService {
320
+ private readonly backend: MemoryBackend;
321
+ private readonly config: MemoryServiceConfig;
322
+ private readonly embeddingProvider: EmbeddingProvider;
323
+ private readonly consolidationEngine: MemoryConsolidationEngine;
324
+
325
+ constructor(
326
+ backend: MemoryBackend = DEFAULT_BACKEND,
327
+ config?: Partial<MemoryServiceConfig>,
328
+ embeddingProvider?: EmbeddingProvider,
329
+ ) {
330
+ this.backend = backend;
331
+ this.config = { ...DEFAULT_CONFIG, ...(config || {}) };
332
+ this.embeddingProvider =
333
+ embeddingProvider ||
334
+ new HashedTokenEmbeddingProvider(this.config.embeddingDimensions);
335
+ this.consolidationEngine = new MemoryConsolidationEngine(this.backend, {
336
+ decayRate: this.config.semanticDecayRate,
337
+ staleAfterDays: this.config.semanticDecayStaleAfterDays,
338
+ minConfidence: this.config.semanticDecayMinConfidence,
339
+ });
340
+ }
341
+
342
+ getOrCreateSession(
343
+ sessionId: string,
344
+ guildId: string | null,
345
+ channelId: string,
346
+ ): Session {
347
+ return this.backend.getOrCreateSession(sessionId, guildId, channelId);
348
+ }
349
+
350
+ getSessionById(sessionId: string): Session | undefined {
351
+ return this.backend.getSessionById(sessionId);
352
+ }
353
+
354
+ getConversationHistory(sessionId: string, limit = 50): StoredMessage[] {
355
+ return this.backend.getConversationHistory(sessionId, limit);
356
+ }
357
+
358
+ getRecentMessages(sessionId: string, limit?: number): StoredMessage[] {
359
+ return this.backend.getRecentMessages(sessionId, limit);
360
+ }
361
+
362
+ get(sessionId: string, key: string): unknown | null {
363
+ return this.backend.get(sessionId, key);
364
+ }
365
+
366
+ set(sessionId: string, key: string, value: unknown): void {
367
+ this.backend.set(sessionId, key, value);
368
+ }
369
+
370
+ delete(sessionId: string, key: string): boolean {
371
+ return this.backend.delete(sessionId, key);
372
+ }
373
+
374
+ list(sessionId: string, prefix?: string): StructuredMemoryEntry[] {
375
+ return this.backend.list(sessionId, prefix);
376
+ }
377
+
378
+ appendCanonicalMessages(params: {
379
+ agentId: string;
380
+ userId: string;
381
+ newMessages: Array<{
382
+ role: string;
383
+ content: string;
384
+ sessionId: string;
385
+ channelId?: string | null;
386
+ createdAt?: string | null;
387
+ }>;
388
+ windowSize?: number;
389
+ compactionThreshold?: number;
390
+ }): CanonicalSession {
391
+ return this.backend.appendCanonicalMessages(params);
392
+ }
393
+
394
+ getCanonicalContext(params: {
395
+ agentId: string;
396
+ userId: string;
397
+ windowSize?: number;
398
+ excludeSessionId?: string | null;
399
+ }): CanonicalSessionContext {
400
+ return this.backend.getCanonicalContext(params);
401
+ }
402
+
403
+ addKnowledgeEntity(params: {
404
+ id?: string | null;
405
+ name: string;
406
+ entityType: KnowledgeEntityTypeValue | string;
407
+ properties?: Record<string, unknown> | null;
408
+ }): string {
409
+ return this.backend.addKnowledgeEntity(params);
410
+ }
411
+
412
+ addKnowledgeRelation(params: {
413
+ source: string;
414
+ relation: KnowledgeRelationTypeValue | string;
415
+ target: string;
416
+ properties?: Record<string, unknown> | null;
417
+ confidence?: number;
418
+ }): string {
419
+ return this.backend.addKnowledgeRelation(params);
420
+ }
421
+
422
+ queryKnowledgeGraph(pattern?: KnowledgeGraphPattern): KnowledgeGraphMatch[] {
423
+ return this.backend.queryKnowledgeGraph(pattern);
424
+ }
425
+
426
+ consolidateMemories(
427
+ overrides?: Partial<MemoryConsolidationConfig>,
428
+ ): MemoryConsolidationReport {
429
+ return this.consolidationEngine.consolidate(overrides);
430
+ }
431
+
432
+ recallSemanticMemories(
433
+ params: RecallSemanticMemoriesParams,
434
+ ): SemanticMemoryEntry[] {
435
+ const limit = Math.max(
436
+ 1,
437
+ Math.min(Math.floor(params.limit || this.config.semanticRecallLimit), 50),
438
+ );
439
+ const rawMinConfidence =
440
+ typeof params.minConfidence === 'number' &&
441
+ Number.isFinite(params.minConfidence)
442
+ ? params.minConfidence
443
+ : this.config.semanticMinConfidence;
444
+ const minConfidence = Math.max(0, Math.min(1, rawMinConfidence));
445
+ const query = params.query.trim();
446
+
447
+ return this.backend.recallSemanticMemories({
448
+ sessionId: params.sessionId,
449
+ query,
450
+ limit,
451
+ minConfidence,
452
+ queryEmbedding: this.embeddingProvider.embed(query),
453
+ filter: params.filter,
454
+ });
455
+ }
456
+
457
+ getCompactionCandidateMessages(
458
+ sessionId: string,
459
+ keepRecent: number,
460
+ ): CompactionCandidate | null {
461
+ return this.backend.getCompactionCandidateMessages(sessionId, keepRecent);
462
+ }
463
+
464
+ clearSessionHistory(sessionId: string): number {
465
+ return this.backend.clearSessionHistory(sessionId);
466
+ }
467
+
468
+ deleteMessagesBeforeId(sessionId: string, cutoffId: number): number {
469
+ return this.backend.deleteMessagesBeforeId(sessionId, cutoffId);
470
+ }
471
+
472
+ updateSessionSummary(sessionId: string, summary: string): void {
473
+ this.backend.updateSessionSummary(sessionId, summary);
474
+ }
475
+
476
+ markSessionMemoryFlush(sessionId: string): void {
477
+ this.backend.markSessionMemoryFlush(sessionId);
478
+ }
479
+
480
+ forgetSemanticMemory(id: number): boolean {
481
+ return this.backend.forgetSemanticMemory(id);
482
+ }
483
+
484
+ storeMessage(params: {
485
+ sessionId: string;
486
+ userId: string;
487
+ username: string | null;
488
+ role: string;
489
+ content: string;
490
+ }): number {
491
+ return this.backend.storeMessage(
492
+ params.sessionId,
493
+ params.userId,
494
+ params.username,
495
+ params.role,
496
+ params.content,
497
+ );
498
+ }
499
+
500
+ storeTurn(params: StoreTurnParams): void {
501
+ this.storeMessage({
502
+ sessionId: params.sessionId,
503
+ userId: params.user.userId,
504
+ username: params.user.username,
505
+ role: 'user',
506
+ content: params.user.content,
507
+ });
508
+ const assistantMessageId = this.storeMessage({
509
+ sessionId: params.sessionId,
510
+ userId: params.assistant.userId || 'assistant',
511
+ username: params.assistant.username || null,
512
+ role: 'assistant',
513
+ content: params.assistant.content,
514
+ });
515
+
516
+ const interactionText = this.normalizeSemanticContent(
517
+ `User asked: ${params.user.content.trim()}\nI responded: ${params.assistant.content.trim()}`,
518
+ );
519
+ if (!interactionText) return;
520
+
521
+ this.backend.storeSemanticMemory({
522
+ sessionId: params.sessionId,
523
+ role: 'assistant',
524
+ source: 'conversation',
525
+ scope: 'episodic',
526
+ metadata: {},
527
+ content: interactionText,
528
+ confidence: 1,
529
+ embedding: this.embeddingProvider.embed(interactionText),
530
+ sourceMessageId: assistantMessageId,
531
+ });
532
+ }
533
+
534
+ buildPromptMemoryContext(
535
+ params: BuildMemoryPromptParams,
536
+ ): BuildMemoryPromptResult {
537
+ const summaryText = (params.session.session_summary || '').trim();
538
+ const summaryConfidence = summaryText
539
+ ? computeDecayedConfidence({
540
+ updatedAt: params.session.summary_updated_at,
541
+ decayRate: this.config.summaryDecayRate,
542
+ minConfidence: this.config.summaryMinConfidence,
543
+ })
544
+ : null;
545
+
546
+ const includeSummary =
547
+ summaryText &&
548
+ (summaryConfidence == null ||
549
+ summaryConfidence >= this.config.summaryDiscardThreshold);
550
+
551
+ const semanticLimit = Math.max(
552
+ 1,
553
+ Math.min(
554
+ Math.floor(params.semanticLimit || this.config.semanticRecallLimit),
555
+ 12,
556
+ ),
557
+ );
558
+ const semanticMemories = this.recallSemanticMemories({
559
+ sessionId: params.session.id,
560
+ query: params.query,
561
+ limit: semanticLimit,
562
+ minConfidence: this.config.semanticMinConfidence,
563
+ });
564
+
565
+ const sections: string[] = [];
566
+ if (includeSummary) {
567
+ if (summaryConfidence != null && summaryConfidence < 0.999) {
568
+ sections.push(
569
+ `Summary confidence: ${Math.round(summaryConfidence * 100)}% (decayed by age).`,
570
+ );
571
+ }
572
+ sections.push(summaryText);
573
+ }
574
+
575
+ if (semanticMemories.length > 0) {
576
+ const lines = semanticMemories.map((memory) => {
577
+ const confidence = Math.round(
578
+ Math.max(0, Math.min(1, memory.confidence)) * 100,
579
+ );
580
+ return `- (${confidence}%) ${truncateInline(memory.content, 220)}`;
581
+ });
582
+ sections.push(
583
+ [
584
+ '### Relevant Memory Recall',
585
+ 'Topic-matched context from older turns (vector cosine search):',
586
+ ...lines,
587
+ ].join('\n'),
588
+ );
589
+ }
590
+
591
+ const promptSummary = sections.join('\n\n').trim();
592
+ return {
593
+ promptSummary: promptSummary || null,
594
+ summaryConfidence,
595
+ semanticMemories,
596
+ };
597
+ }
598
+
599
+ private normalizeSemanticContent(content: string): string {
600
+ const compact = content.replace(/\s+/g, ' ').trim();
601
+ if (compact.length <= this.config.semanticMaxContentChars) return compact;
602
+ return compact.slice(0, this.config.semanticMaxContentChars);
603
+ }
604
+ }
605
+
606
+ export const memoryService = new MemoryService();