@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
@@ -0,0 +1,196 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { agentWorkspaceDir, ensureAgentDirs } from './ipc.js';
5
+ import { logger } from './logger.js';
6
+ import type { StoredMessage } from './types.js';
7
+
8
+ const SESSION_EXPORTS_DIR_NAME = '.session-exports';
9
+
10
+ interface ExportedMessageRow {
11
+ type: 'message';
12
+ section: 'full' | 'compacted' | 'retained';
13
+ sessionId: string;
14
+ channelId: string | null;
15
+ id: number;
16
+ role: string;
17
+ userId: string;
18
+ username: string | null;
19
+ content: string;
20
+ createdAt: string;
21
+ }
22
+
23
+ function safeFilePart(raw: string): string {
24
+ const normalized = raw.trim().replace(/[^a-zA-Z0-9_-]/g, '_');
25
+ return normalized || 'session';
26
+ }
27
+
28
+ function compactText(text: string, maxChars = 4_000): string {
29
+ const compact = text.replace(/\s+/g, ' ').trim();
30
+ if (compact.length <= maxChars) return compact;
31
+ return `${compact.slice(0, maxChars)}...`;
32
+ }
33
+
34
+ function exportBaseDir(agentId: string, sessionId: string): string {
35
+ ensureAgentDirs(agentId);
36
+ return path.join(
37
+ agentWorkspaceDir(agentId),
38
+ SESSION_EXPORTS_DIR_NAME,
39
+ safeFilePart(sessionId),
40
+ );
41
+ }
42
+
43
+ function exportFilePath(baseDir: string, reason: string): string {
44
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
45
+ const filename = `${stamp}-${safeFilePart(reason)}.jsonl`;
46
+ return path.join(baseDir, filename);
47
+ }
48
+
49
+ function writeJsonlFile(filePath: string, rows: unknown[]): boolean {
50
+ try {
51
+ const lines = rows.map((row) => JSON.stringify(row)).join('\n');
52
+ fs.writeFileSync(filePath, `${lines}\n`, 'utf8');
53
+ return true;
54
+ } catch (err) {
55
+ logger.warn({ filePath, err }, 'Failed to write session export JSONL');
56
+ return false;
57
+ }
58
+ }
59
+
60
+ export function exportSessionSnapshotJsonl(params: {
61
+ agentId: string;
62
+ sessionId: string;
63
+ channelId?: string | null;
64
+ summary?: string | null;
65
+ messages: StoredMessage[];
66
+ reason: string;
67
+ }): { path: string; lineCount: number } | null {
68
+ const agentId = params.agentId.trim();
69
+ const sessionId = params.sessionId.trim();
70
+ if (!agentId || !sessionId) return null;
71
+ try {
72
+ const baseDir = exportBaseDir(agentId, sessionId);
73
+ fs.mkdirSync(baseDir, { recursive: true });
74
+ const filePath = exportFilePath(baseDir, params.reason);
75
+ const now = new Date().toISOString();
76
+ const rows: unknown[] = [
77
+ {
78
+ type: 'meta',
79
+ reason: params.reason,
80
+ exportedAt: now,
81
+ agentId,
82
+ sessionId,
83
+ channelId: params.channelId || null,
84
+ messageCount: params.messages.length,
85
+ hasSummary: Boolean((params.summary || '').trim()),
86
+ },
87
+ ];
88
+ const summary = (params.summary || '').trim();
89
+ if (summary) {
90
+ rows.push({
91
+ type: 'summary',
92
+ sessionId,
93
+ content: compactText(summary, 12_000),
94
+ });
95
+ }
96
+ rows.push(
97
+ ...params.messages.map<ExportedMessageRow>((message) => ({
98
+ type: 'message',
99
+ section: 'full',
100
+ sessionId,
101
+ channelId: params.channelId || null,
102
+ id: message.id,
103
+ role: message.role,
104
+ userId: message.user_id,
105
+ username: message.username,
106
+ content: message.content,
107
+ createdAt: message.created_at,
108
+ })),
109
+ );
110
+ if (!writeJsonlFile(filePath, rows)) return null;
111
+ return { path: filePath, lineCount: rows.length };
112
+ } catch (err) {
113
+ logger.warn(
114
+ { agentId, sessionId, err },
115
+ 'Failed to export session snapshot',
116
+ );
117
+ return null;
118
+ }
119
+ }
120
+
121
+ export function exportCompactedSessionJsonl(params: {
122
+ agentId: string;
123
+ sessionId: string;
124
+ channelId: string;
125
+ summary: string;
126
+ compactedMessages: StoredMessage[];
127
+ retainedMessages: StoredMessage[];
128
+ deletedCount: number;
129
+ cutoffId: number;
130
+ }): { path: string; lineCount: number } | null {
131
+ const agentId = params.agentId.trim();
132
+ const sessionId = params.sessionId.trim();
133
+ if (!agentId || !sessionId) return null;
134
+ try {
135
+ const baseDir = exportBaseDir(agentId, sessionId);
136
+ fs.mkdirSync(baseDir, { recursive: true });
137
+ const filePath = exportFilePath(baseDir, 'compaction');
138
+ const exportedAt = new Date().toISOString();
139
+ const rows: unknown[] = [
140
+ {
141
+ type: 'meta',
142
+ reason: 'compaction',
143
+ exportedAt,
144
+ agentId,
145
+ sessionId,
146
+ channelId: params.channelId,
147
+ cutoffId: params.cutoffId,
148
+ deletedCount: params.deletedCount,
149
+ compactedMessageCount: params.compactedMessages.length,
150
+ retainedMessageCount: params.retainedMessages.length,
151
+ },
152
+ {
153
+ type: 'summary',
154
+ sessionId,
155
+ content: compactText(params.summary, 12_000),
156
+ },
157
+ ];
158
+
159
+ rows.push(
160
+ ...params.compactedMessages.map<ExportedMessageRow>((message) => ({
161
+ type: 'message',
162
+ section: 'compacted',
163
+ sessionId,
164
+ channelId: params.channelId,
165
+ id: message.id,
166
+ role: message.role,
167
+ userId: message.user_id,
168
+ username: message.username,
169
+ content: message.content,
170
+ createdAt: message.created_at,
171
+ })),
172
+ );
173
+ rows.push(
174
+ ...params.retainedMessages.map<ExportedMessageRow>((message) => ({
175
+ type: 'message',
176
+ section: 'retained',
177
+ sessionId,
178
+ channelId: params.channelId,
179
+ id: message.id,
180
+ role: message.role,
181
+ userId: message.user_id,
182
+ username: message.username,
183
+ content: message.content,
184
+ createdAt: message.created_at,
185
+ })),
186
+ );
187
+ if (!writeJsonlFile(filePath, rows)) return null;
188
+ return { path: filePath, lineCount: rows.length };
189
+ } catch (err) {
190
+ logger.warn(
191
+ { agentId, sessionId, err },
192
+ 'Failed to export compacted session JSONL',
193
+ );
194
+ return null;
195
+ }
196
+ }
@@ -1,28 +1,41 @@
1
+ import { runAgent } from './agent.js';
1
2
  import {
2
3
  PRE_COMPACTION_MEMORY_FLUSH_ENABLED,
3
4
  PRE_COMPACTION_MEMORY_FLUSH_MAX_CHARS,
4
5
  PRE_COMPACTION_MEMORY_FLUSH_MAX_MESSAGES,
6
+ SESSION_COMPACTION_BUDGET_RATIO,
5
7
  SESSION_COMPACTION_ENABLED,
6
8
  SESSION_COMPACTION_KEEP_RECENT,
7
9
  SESSION_COMPACTION_SUMMARY_MAX_CHARS,
8
10
  SESSION_COMPACTION_THRESHOLD,
11
+ SESSION_COMPACTION_TOKEN_BUDGET,
9
12
  } from './config.js';
10
- import {
11
- deleteMessagesBeforeId,
12
- getCompactionCandidateMessages,
13
- getSessionById,
14
- markSessionMemoryFlush,
15
- updateSessionSummary,
16
- } from './db.js';
17
13
  import { logger } from './logger.js';
14
+ import { memoryService } from './memory-service.js';
15
+ import { buildSystemPromptFromHooks } from './prompt-hooks.js';
16
+ import { exportCompactedSessionJsonl } from './session-export.js';
18
17
  import { loadSkills } from './skills.js';
18
+ import {
19
+ estimateTokenCountFromMessages,
20
+ estimateTokenCountFromText,
21
+ } from './token-efficiency.js';
19
22
  import type { ChatMessage, StoredMessage } from './types.js';
20
- import { runAgent } from './agent.js';
21
- import { buildSystemPromptFromHooks } from './prompt-hooks.js';
22
23
 
23
24
  const COMPACTION_SOURCE_MAX_MESSAGES = 240;
24
25
  const COMPACTION_SOURCE_MAX_CHARS = 80_000;
25
26
 
27
+ function normalizeStoredMessageRole(role: string): ChatMessage['role'] {
28
+ if (
29
+ role === 'system' ||
30
+ role === 'user' ||
31
+ role === 'assistant' ||
32
+ role === 'tool'
33
+ ) {
34
+ return role;
35
+ }
36
+ return 'user';
37
+ }
38
+
26
39
  function formatDateStampInLocalTimezone(now: Date): string {
27
40
  const parts = new Intl.DateTimeFormat('en-US', {
28
41
  year: 'numeric',
@@ -41,7 +54,10 @@ function formatDateStampInLocalTimezone(now: Date): string {
41
54
  function normalizeSummary(summary: string): string {
42
55
  let text = summary.trim();
43
56
  if (text.startsWith('```')) {
44
- text = text.replace(/^```[a-z0-9_-]*\s*/i, '').replace(/```$/i, '').trim();
57
+ text = text
58
+ .replace(/^```[a-z0-9_-]*\s*/i, '')
59
+ .replace(/```$/i, '')
60
+ .trim();
45
61
  }
46
62
  if (text.length > SESSION_COMPACTION_SUMMARY_MAX_CHARS) {
47
63
  text = `${text.slice(0, SESSION_COMPACTION_SUMMARY_MAX_CHARS)}\n\n...[truncated]`;
@@ -61,7 +77,10 @@ function formatMessagesForPrompt(
61
77
  for (const msg of selected) {
62
78
  const role = (msg.role || 'unknown').toUpperCase();
63
79
  const compact = msg.content.replace(/\r/g, '').trim();
64
- const bounded = compact.length > 1_200 ? `${compact.slice(0, 1_200)}\n...[truncated]` : compact;
80
+ const bounded =
81
+ compact.length > 1_200
82
+ ? `${compact.slice(0, 1_200)}\n...[truncated]`
83
+ : compact;
65
84
  const entry = `[${role}] ${bounded}`;
66
85
  const bytes = entry.length + 2;
67
86
  if (usedChars + bytes > maxChars) break;
@@ -72,7 +91,11 @@ function formatMessagesForPrompt(
72
91
  return lines.join('\n\n');
73
92
  }
74
93
 
75
- function buildSystemPrompt(agentId: string, sessionSummary?: string | null, extra?: string): string {
94
+ function buildSystemPrompt(
95
+ agentId: string,
96
+ sessionSummary?: string | null,
97
+ extra?: string,
98
+ ): string {
76
99
  return buildSystemPromptFromHooks({
77
100
  agentId,
78
101
  sessionSummary,
@@ -143,12 +166,18 @@ async function runPreCompactionMemoryFlush(params: {
143
166
  ['memory'],
144
167
  );
145
168
  if (output.status === 'error') {
146
- logger.warn({ sessionId: params.sessionId, error: output.error }, 'Pre-compaction memory flush failed');
169
+ logger.warn(
170
+ { sessionId: params.sessionId, error: output.error },
171
+ 'Pre-compaction memory flush failed',
172
+ );
147
173
  return;
148
174
  }
149
- markSessionMemoryFlush(params.sessionId);
175
+ memoryService.markSessionMemoryFlush(params.sessionId);
150
176
  } catch (err) {
151
- logger.warn({ sessionId: params.sessionId, err }, 'Pre-compaction memory flush crashed');
177
+ logger.warn(
178
+ { sessionId: params.sessionId, err },
179
+ 'Pre-compaction memory flush crashed',
180
+ );
152
181
  }
153
182
  }
154
183
 
@@ -204,7 +233,10 @@ async function generateCompactionSummary(params: {
204
233
  );
205
234
 
206
235
  if (output.status === 'error' || !output.result) {
207
- logger.warn({ sessionId: params.sessionId, error: output.error }, 'Session compaction summary failed');
236
+ logger.warn(
237
+ { sessionId: params.sessionId, error: output.error },
238
+ 'Session compaction summary failed',
239
+ );
208
240
  return null;
209
241
  }
210
242
 
@@ -223,15 +255,60 @@ export async function maybeCompactSession(params: {
223
255
  }): Promise<void> {
224
256
  if (!SESSION_COMPACTION_ENABLED) return;
225
257
 
226
- const session = getSessionById(params.sessionId);
258
+ const session = memoryService.getSessionById(params.sessionId);
227
259
  if (!session) return;
228
260
 
229
261
  const threshold = Math.max(SESSION_COMPACTION_THRESHOLD, 20);
230
- const keepRecent = Math.max(1, Math.min(SESSION_COMPACTION_KEEP_RECENT, threshold - 1));
262
+ const tokenBudget = Math.max(1_000, SESSION_COMPACTION_TOKEN_BUDGET);
263
+ const budgetRatio = Math.max(
264
+ 0.05,
265
+ Math.min(1, SESSION_COMPACTION_BUDGET_RATIO),
266
+ );
267
+ const budget = Math.max(1, Math.floor(tokenBudget * budgetRatio));
268
+ const allMessages = memoryService.getRecentMessages(params.sessionId);
269
+ const keepRecent = Math.max(
270
+ 1,
271
+ Math.min(
272
+ SESSION_COMPACTION_KEEP_RECENT,
273
+ Math.max(1, threshold - 1),
274
+ Math.max(1, allMessages.length - 1),
275
+ ),
276
+ );
277
+ const msgTokens = estimateTokenCountFromMessages(
278
+ allMessages.map((message) => ({
279
+ role: normalizeStoredMessageRole(message.role),
280
+ content: message.content,
281
+ })),
282
+ );
283
+ const summaryTokens = estimateTokenCountFromText(session.session_summary);
284
+ const totalTokens = msgTokens + summaryTokens;
285
+ const shouldCompactForTokens = totalTokens >= budget;
286
+ const shouldCompactForMessageCount = session.message_count >= threshold;
287
+
288
+ logger.debug(
289
+ {
290
+ sessionId: params.sessionId,
291
+ messageCount: session.message_count,
292
+ loadedMessages: allMessages.length,
293
+ msgTokens,
294
+ summaryTokens,
295
+ totalTokens,
296
+ tokenBudget,
297
+ budgetRatio,
298
+ triggerBudget: budget,
299
+ triggerThreshold: threshold,
300
+ shouldCompactForTokens,
301
+ shouldCompactForMessageCount,
302
+ },
303
+ 'Session compaction budget check',
304
+ );
231
305
 
232
- if (session.message_count < threshold) return;
306
+ if (!shouldCompactForTokens && !shouldCompactForMessageCount) return;
233
307
 
234
- const candidate = getCompactionCandidateMessages(params.sessionId, keepRecent);
308
+ const candidate = memoryService.getCompactionCandidateMessages(
309
+ params.sessionId,
310
+ keepRecent,
311
+ );
235
312
  if (!candidate || candidate.olderMessages.length === 0) return;
236
313
 
237
314
  await runPreCompactionMemoryFlush({
@@ -247,10 +324,27 @@ export async function maybeCompactSession(params: {
247
324
  });
248
325
  if (!summary) return;
249
326
 
250
- const deleted = deleteMessagesBeforeId(params.sessionId, candidate.cutoffId);
327
+ const deleted = memoryService.deleteMessagesBeforeId(
328
+ params.sessionId,
329
+ candidate.cutoffId,
330
+ );
251
331
  if (deleted <= 0) return;
252
332
 
253
- updateSessionSummary(params.sessionId, summary);
333
+ memoryService.updateSessionSummary(params.sessionId, summary);
334
+ const retainedMessages = memoryService.getRecentMessages(
335
+ params.sessionId,
336
+ keepRecent,
337
+ );
338
+ const exported = exportCompactedSessionJsonl({
339
+ agentId: params.agentId,
340
+ sessionId: params.sessionId,
341
+ channelId: params.channelId,
342
+ summary,
343
+ compactedMessages: candidate.olderMessages,
344
+ retainedMessages,
345
+ deletedCount: deleted,
346
+ cutoffId: candidate.cutoffId,
347
+ });
254
348
  logger.info(
255
349
  {
256
350
  sessionId: params.sessionId,
@@ -258,6 +352,15 @@ export async function maybeCompactSession(params: {
258
352
  cutoffId: candidate.cutoffId,
259
353
  threshold,
260
354
  keepRecent,
355
+ msgTokens,
356
+ summaryTokens,
357
+ totalTokens,
358
+ tokenBudget,
359
+ budgetRatio,
360
+ triggerBudget: budget,
361
+ shouldCompactForTokens,
362
+ shouldCompactForMessageCount,
363
+ exportPath: exported?.path || null,
261
364
  },
262
365
  'Session compacted',
263
366
  );
@@ -21,14 +21,20 @@ function safeSessionFilename(sessionId: string): string {
21
21
  return normalized || 'session';
22
22
  }
23
23
 
24
- export function appendSessionTranscript(agentId: string, entry: TranscriptEntry): void {
24
+ export function appendSessionTranscript(
25
+ agentId: string,
26
+ entry: TranscriptEntry,
27
+ ): void {
25
28
  try {
26
29
  ensureAgentDirs(agentId);
27
30
  const workspace = agentWorkspaceDir(agentId);
28
31
  const transcriptDir = path.join(workspace, TRANSCRIPTS_DIR_NAME);
29
32
  fs.mkdirSync(transcriptDir, { recursive: true });
30
33
 
31
- const filePath = path.join(transcriptDir, `${safeSessionFilename(entry.sessionId)}.jsonl`);
34
+ const filePath = path.join(
35
+ transcriptDir,
36
+ `${safeSessionFilename(entry.sessionId)}.jsonl`,
37
+ );
32
38
  const row = {
33
39
  sessionId: entry.sessionId,
34
40
  channelId: entry.channelId,
@@ -40,6 +46,9 @@ export function appendSessionTranscript(agentId: string, entry: TranscriptEntry)
40
46
  };
41
47
  fs.appendFileSync(filePath, JSON.stringify(row) + '\n', 'utf-8');
42
48
  } catch (err) {
43
- logger.debug({ agentId, sessionId: entry.sessionId, err }, 'Failed to append session transcript');
49
+ logger.debug(
50
+ { agentId, sessionId: entry.sessionId, err },
51
+ 'Failed to append session transcript',
52
+ );
44
53
  }
45
54
  }
@@ -15,7 +15,8 @@ export function processSideEffects(
15
15
  ): void {
16
16
  const schedules = output.sideEffects?.schedules;
17
17
  const delegations = output.sideEffects?.delegations || [];
18
- if ((!schedules || schedules.length === 0) && delegations.length === 0) return;
18
+ if ((!schedules || schedules.length === 0) && delegations.length === 0)
19
+ return;
19
20
 
20
21
  let changed = false;
21
22
 
@@ -31,11 +32,24 @@ export function processSideEffects(
31
32
  effect.runAt,
32
33
  effect.everyMs,
33
34
  );
34
- logger.info({ taskId, sessionId, channelId, cronExpr: effect.cronExpr, runAt: effect.runAt, everyMs: effect.everyMs }, 'Side-effect: created task');
35
+ logger.info(
36
+ {
37
+ taskId,
38
+ sessionId,
39
+ channelId,
40
+ cronExpr: effect.cronExpr,
41
+ runAt: effect.runAt,
42
+ everyMs: effect.everyMs,
43
+ },
44
+ 'Side-effect: created task',
45
+ );
35
46
  changed = true;
36
47
  } else if (effect.action === 'remove') {
37
48
  deleteTask(effect.taskId);
38
- logger.info({ taskId: effect.taskId, sessionId }, 'Side-effect: removed task');
49
+ logger.info(
50
+ { taskId: effect.taskId, sessionId },
51
+ 'Side-effect: removed task',
52
+ );
39
53
  changed = true;
40
54
  }
41
55
  } catch (err) {
@@ -54,7 +68,13 @@ export function processSideEffects(
54
68
  {
55
69
  sessionId,
56
70
  channelId,
57
- mode: effect.mode || (effect.chain?.length ? 'chain' : effect.tasks?.length ? 'parallel' : 'single'),
71
+ mode:
72
+ effect.mode ||
73
+ (effect.chain?.length
74
+ ? 'chain'
75
+ : effect.tasks?.length
76
+ ? 'parallel'
77
+ : 'single'),
58
78
  prompt: effect.prompt,
59
79
  label: effect.label,
60
80
  tasks: effect.tasks?.length,
@@ -64,7 +84,10 @@ export function processSideEffects(
64
84
  );
65
85
  }
66
86
  } catch (err) {
67
- logger.error({ effect, err }, 'Failed to process delegation side-effect');
87
+ logger.error(
88
+ { effect, err },
89
+ 'Failed to process delegation side-effect',
90
+ );
68
91
  }
69
92
  }
70
93
  }