@xandout/libra-harness 0.1.8

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 (289) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +486 -0
  3. package/dist/agent.d.ts +174 -0
  4. package/dist/agent.d.ts.map +1 -0
  5. package/dist/agent.js +575 -0
  6. package/dist/agent.js.map +1 -0
  7. package/dist/ai-sdk-model.d.ts +26 -0
  8. package/dist/ai-sdk-model.d.ts.map +1 -0
  9. package/dist/ai-sdk-model.js +260 -0
  10. package/dist/ai-sdk-model.js.map +1 -0
  11. package/dist/context.d.ts +75 -0
  12. package/dist/context.d.ts.map +1 -0
  13. package/dist/context.js +2 -0
  14. package/dist/context.js.map +1 -0
  15. package/dist/errors.d.ts +33 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +59 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/extension.d.ts +63 -0
  20. package/dist/extension.d.ts.map +1 -0
  21. package/dist/extension.js +2 -0
  22. package/dist/extension.js.map +1 -0
  23. package/dist/extras/extension-loader.d.ts +205 -0
  24. package/dist/extras/extension-loader.d.ts.map +1 -0
  25. package/dist/extras/extension-loader.js +315 -0
  26. package/dist/extras/extension-loader.js.map +1 -0
  27. package/dist/extras/extensions/auto-steer/extension.d.ts +46 -0
  28. package/dist/extras/extensions/auto-steer/extension.d.ts.map +1 -0
  29. package/dist/extras/extensions/auto-steer/extension.js +52 -0
  30. package/dist/extras/extensions/auto-steer/extension.js.map +1 -0
  31. package/dist/extras/extensions/auto-steer/extension.json +7 -0
  32. package/dist/extras/extensions/auto-steer/index.d.ts +3 -0
  33. package/dist/extras/extensions/auto-steer/index.d.ts.map +1 -0
  34. package/dist/extras/extensions/auto-steer/index.js +2 -0
  35. package/dist/extras/extensions/auto-steer/index.js.map +1 -0
  36. package/dist/extras/extensions/disk-session/extension.d.ts +167 -0
  37. package/dist/extras/extensions/disk-session/extension.d.ts.map +1 -0
  38. package/dist/extras/extensions/disk-session/extension.js +492 -0
  39. package/dist/extras/extensions/disk-session/extension.js.map +1 -0
  40. package/dist/extras/extensions/disk-session/extension.json +6 -0
  41. package/dist/extras/extensions/disk-session/extension.test.d.ts +2 -0
  42. package/dist/extras/extensions/disk-session/extension.test.d.ts.map +1 -0
  43. package/dist/extras/extensions/disk-session/extension.test.js +672 -0
  44. package/dist/extras/extensions/disk-session/extension.test.js.map +1 -0
  45. package/dist/extras/extensions/disk-session/index.d.ts +3 -0
  46. package/dist/extras/extensions/disk-session/index.d.ts.map +1 -0
  47. package/dist/extras/extensions/disk-session/index.js +2 -0
  48. package/dist/extras/extensions/disk-session/index.js.map +1 -0
  49. package/dist/extras/extensions/emoji/extension.d.ts +20 -0
  50. package/dist/extras/extensions/emoji/extension.d.ts.map +1 -0
  51. package/dist/extras/extensions/emoji/extension.js +22 -0
  52. package/dist/extras/extensions/emoji/extension.js.map +1 -0
  53. package/dist/extras/extensions/emoji/extension.json +7 -0
  54. package/dist/extras/extensions/emoji/index.d.ts +3 -0
  55. package/dist/extras/extensions/emoji/index.d.ts.map +1 -0
  56. package/dist/extras/extensions/emoji/index.js +2 -0
  57. package/dist/extras/extensions/emoji/index.js.map +1 -0
  58. package/dist/extras/extensions/filesystem/extension.d.ts +53 -0
  59. package/dist/extras/extensions/filesystem/extension.d.ts.map +1 -0
  60. package/dist/extras/extensions/filesystem/extension.js +350 -0
  61. package/dist/extras/extensions/filesystem/extension.js.map +1 -0
  62. package/dist/extras/extensions/filesystem/extension.json +6 -0
  63. package/dist/extras/extensions/filesystem/extension.test.d.ts +2 -0
  64. package/dist/extras/extensions/filesystem/extension.test.d.ts.map +1 -0
  65. package/dist/extras/extensions/filesystem/extension.test.js +387 -0
  66. package/dist/extras/extensions/filesystem/extension.test.js.map +1 -0
  67. package/dist/extras/extensions/filesystem/index.d.ts +2 -0
  68. package/dist/extras/extensions/filesystem/index.d.ts.map +1 -0
  69. package/dist/extras/extensions/filesystem/index.js +2 -0
  70. package/dist/extras/extensions/filesystem/index.js.map +1 -0
  71. package/dist/extras/extensions/keyword-extractor/extension.d.ts +53 -0
  72. package/dist/extras/extensions/keyword-extractor/extension.d.ts.map +1 -0
  73. package/dist/extras/extensions/keyword-extractor/extension.js +61 -0
  74. package/dist/extras/extensions/keyword-extractor/extension.js.map +1 -0
  75. package/dist/extras/extensions/keyword-extractor/extension.json +6 -0
  76. package/dist/extras/extensions/keyword-extractor/extension.test.d.ts +2 -0
  77. package/dist/extras/extensions/keyword-extractor/extension.test.d.ts.map +1 -0
  78. package/dist/extras/extensions/keyword-extractor/extension.test.js +105 -0
  79. package/dist/extras/extensions/keyword-extractor/extension.test.js.map +1 -0
  80. package/dist/extras/extensions/keyword-extractor/index.d.ts +5 -0
  81. package/dist/extras/extensions/keyword-extractor/index.d.ts.map +1 -0
  82. package/dist/extras/extensions/keyword-extractor/index.js +3 -0
  83. package/dist/extras/extensions/keyword-extractor/index.js.map +1 -0
  84. package/dist/extras/extensions/keyword-extractor/nlp.d.ts +68 -0
  85. package/dist/extras/extensions/keyword-extractor/nlp.d.ts.map +1 -0
  86. package/dist/extras/extensions/keyword-extractor/nlp.js +156 -0
  87. package/dist/extras/extensions/keyword-extractor/nlp.js.map +1 -0
  88. package/dist/extras/extensions/keyword-extractor/nlp.test.d.ts +2 -0
  89. package/dist/extras/extensions/keyword-extractor/nlp.test.d.ts.map +1 -0
  90. package/dist/extras/extensions/keyword-extractor/nlp.test.js +176 -0
  91. package/dist/extras/extensions/keyword-extractor/nlp.test.js.map +1 -0
  92. package/dist/extras/extensions/logger/extension.d.ts +52 -0
  93. package/dist/extras/extensions/logger/extension.d.ts.map +1 -0
  94. package/dist/extras/extensions/logger/extension.js +68 -0
  95. package/dist/extras/extensions/logger/extension.js.map +1 -0
  96. package/dist/extras/extensions/logger/extension.json +7 -0
  97. package/dist/extras/extensions/logger/index.d.ts +3 -0
  98. package/dist/extras/extensions/logger/index.d.ts.map +1 -0
  99. package/dist/extras/extensions/logger/index.js +2 -0
  100. package/dist/extras/extensions/logger/index.js.map +1 -0
  101. package/dist/extras/extensions/mcp/extension.d.ts +63 -0
  102. package/dist/extras/extensions/mcp/extension.d.ts.map +1 -0
  103. package/dist/extras/extensions/mcp/extension.js +437 -0
  104. package/dist/extras/extensions/mcp/extension.js.map +1 -0
  105. package/dist/extras/extensions/mcp/extension.json +7 -0
  106. package/dist/extras/extensions/mcp/extension.test.d.ts +2 -0
  107. package/dist/extras/extensions/mcp/extension.test.d.ts.map +1 -0
  108. package/dist/extras/extensions/mcp/extension.test.js +286 -0
  109. package/dist/extras/extensions/mcp/extension.test.js.map +1 -0
  110. package/dist/extras/extensions/mcp/index.d.ts +3 -0
  111. package/dist/extras/extensions/mcp/index.d.ts.map +1 -0
  112. package/dist/extras/extensions/mcp/index.js +2 -0
  113. package/dist/extras/extensions/mcp/index.js.map +1 -0
  114. package/dist/extras/extensions/mem-session/extension.d.ts +24 -0
  115. package/dist/extras/extensions/mem-session/extension.d.ts.map +1 -0
  116. package/dist/extras/extensions/mem-session/extension.js +49 -0
  117. package/dist/extras/extensions/mem-session/extension.js.map +1 -0
  118. package/dist/extras/extensions/mem-session/extension.json +6 -0
  119. package/dist/extras/extensions/mem-session/index.d.ts +2 -0
  120. package/dist/extras/extensions/mem-session/index.d.ts.map +1 -0
  121. package/dist/extras/extensions/mem-session/index.js +2 -0
  122. package/dist/extras/extensions/mem-session/index.js.map +1 -0
  123. package/dist/extras/extensions/memory/extension.d.ts +54 -0
  124. package/dist/extras/extensions/memory/extension.d.ts.map +1 -0
  125. package/dist/extras/extensions/memory/extension.js +143 -0
  126. package/dist/extras/extensions/memory/extension.js.map +1 -0
  127. package/dist/extras/extensions/memory/extension.json +7 -0
  128. package/dist/extras/extensions/memory/index.d.ts +5 -0
  129. package/dist/extras/extensions/memory/index.d.ts.map +1 -0
  130. package/dist/extras/extensions/memory/index.js +3 -0
  131. package/dist/extras/extensions/memory/index.js.map +1 -0
  132. package/dist/extras/extensions/memory/llm-extractor.d.ts +42 -0
  133. package/dist/extras/extensions/memory/llm-extractor.d.ts.map +1 -0
  134. package/dist/extras/extensions/memory/llm-extractor.js +89 -0
  135. package/dist/extras/extensions/memory/llm-extractor.js.map +1 -0
  136. package/dist/extras/extensions/memory/types.d.ts +142 -0
  137. package/dist/extras/extensions/memory/types.d.ts.map +1 -0
  138. package/dist/extras/extensions/memory/types.js +2 -0
  139. package/dist/extras/extensions/memory/types.js.map +1 -0
  140. package/dist/extras/extensions/otel/extension.d.ts +57 -0
  141. package/dist/extras/extensions/otel/extension.d.ts.map +1 -0
  142. package/dist/extras/extensions/otel/extension.js +253 -0
  143. package/dist/extras/extensions/otel/extension.js.map +1 -0
  144. package/dist/extras/extensions/otel/extension.json +7 -0
  145. package/dist/extras/extensions/otel/extension.test.d.ts +2 -0
  146. package/dist/extras/extensions/otel/extension.test.d.ts.map +1 -0
  147. package/dist/extras/extensions/otel/extension.test.js +399 -0
  148. package/dist/extras/extensions/otel/extension.test.js.map +1 -0
  149. package/dist/extras/extensions/otel/index.d.ts +4 -0
  150. package/dist/extras/extensions/otel/index.d.ts.map +1 -0
  151. package/dist/extras/extensions/otel/index.js +3 -0
  152. package/dist/extras/extensions/otel/index.js.map +1 -0
  153. package/dist/extras/extensions/otel/jsonl-span-exporter.d.ts +60 -0
  154. package/dist/extras/extensions/otel/jsonl-span-exporter.d.ts.map +1 -0
  155. package/dist/extras/extensions/otel/jsonl-span-exporter.js +134 -0
  156. package/dist/extras/extensions/otel/jsonl-span-exporter.js.map +1 -0
  157. package/dist/extras/extensions/scripts/extension.d.ts +180 -0
  158. package/dist/extras/extensions/scripts/extension.d.ts.map +1 -0
  159. package/dist/extras/extensions/scripts/extension.js +618 -0
  160. package/dist/extras/extensions/scripts/extension.js.map +1 -0
  161. package/dist/extras/extensions/scripts/extension.json +7 -0
  162. package/dist/extras/extensions/scripts/extension.test.d.ts +2 -0
  163. package/dist/extras/extensions/scripts/extension.test.d.ts.map +1 -0
  164. package/dist/extras/extensions/scripts/extension.test.js +552 -0
  165. package/dist/extras/extensions/scripts/extension.test.js.map +1 -0
  166. package/dist/extras/extensions/scripts/index.d.ts +3 -0
  167. package/dist/extras/extensions/scripts/index.d.ts.map +1 -0
  168. package/dist/extras/extensions/scripts/index.js +2 -0
  169. package/dist/extras/extensions/scripts/index.js.map +1 -0
  170. package/dist/extras/extensions/skills/extension.d.ts +105 -0
  171. package/dist/extras/extensions/skills/extension.d.ts.map +1 -0
  172. package/dist/extras/extensions/skills/extension.js +419 -0
  173. package/dist/extras/extensions/skills/extension.js.map +1 -0
  174. package/dist/extras/extensions/skills/extension.json +7 -0
  175. package/dist/extras/extensions/skills/extension.test.d.ts +2 -0
  176. package/dist/extras/extensions/skills/extension.test.d.ts.map +1 -0
  177. package/dist/extras/extensions/skills/extension.test.js +184 -0
  178. package/dist/extras/extensions/skills/extension.test.js.map +1 -0
  179. package/dist/extras/extensions/skills/index.d.ts +4 -0
  180. package/dist/extras/extensions/skills/index.d.ts.map +1 -0
  181. package/dist/extras/extensions/skills/index.js +3 -0
  182. package/dist/extras/extensions/skills/index.js.map +1 -0
  183. package/dist/extras/extensions/streaming/extension.d.ts +15 -0
  184. package/dist/extras/extensions/streaming/extension.d.ts.map +1 -0
  185. package/dist/extras/extensions/streaming/extension.js +41 -0
  186. package/dist/extras/extensions/streaming/extension.js.map +1 -0
  187. package/dist/extras/extensions/streaming/extension.json +6 -0
  188. package/dist/extras/extensions/streaming/index.d.ts +2 -0
  189. package/dist/extras/extensions/streaming/index.d.ts.map +1 -0
  190. package/dist/extras/extensions/streaming/index.js +2 -0
  191. package/dist/extras/extensions/streaming/index.js.map +1 -0
  192. package/dist/extras/extensions/structured-output/extension.d.ts +11 -0
  193. package/dist/extras/extensions/structured-output/extension.d.ts.map +1 -0
  194. package/dist/extras/extensions/structured-output/extension.js +102 -0
  195. package/dist/extras/extensions/structured-output/extension.js.map +1 -0
  196. package/dist/extras/extensions/structured-output/extension.json +7 -0
  197. package/dist/extras/extensions/structured-output/index.d.ts +3 -0
  198. package/dist/extras/extensions/structured-output/index.d.ts.map +1 -0
  199. package/dist/extras/extensions/structured-output/index.js +2 -0
  200. package/dist/extras/extensions/structured-output/index.js.map +1 -0
  201. package/dist/extras/extensions/timestamp/extension.d.ts +11 -0
  202. package/dist/extras/extensions/timestamp/extension.d.ts.map +1 -0
  203. package/dist/extras/extensions/timestamp/extension.js +21 -0
  204. package/dist/extras/extensions/timestamp/extension.js.map +1 -0
  205. package/dist/extras/extensions/timestamp/extension.json +6 -0
  206. package/dist/extras/extensions/timestamp/index.d.ts +2 -0
  207. package/dist/extras/extensions/timestamp/index.d.ts.map +1 -0
  208. package/dist/extras/extensions/timestamp/index.js +2 -0
  209. package/dist/extras/extensions/timestamp/index.js.map +1 -0
  210. package/dist/extras/extensions/token-stats/extension.d.ts +50 -0
  211. package/dist/extras/extensions/token-stats/extension.d.ts.map +1 -0
  212. package/dist/extras/extensions/token-stats/extension.js +99 -0
  213. package/dist/extras/extensions/token-stats/extension.js.map +1 -0
  214. package/dist/extras/extensions/token-stats/extension.json +7 -0
  215. package/dist/extras/extensions/token-stats/extension.test.d.ts +2 -0
  216. package/dist/extras/extensions/token-stats/extension.test.d.ts.map +1 -0
  217. package/dist/extras/extensions/token-stats/extension.test.js +197 -0
  218. package/dist/extras/extensions/token-stats/extension.test.js.map +1 -0
  219. package/dist/extras/extensions/token-stats/index.d.ts +3 -0
  220. package/dist/extras/extensions/token-stats/index.d.ts.map +1 -0
  221. package/dist/extras/extensions/token-stats/index.js +2 -0
  222. package/dist/extras/extensions/token-stats/index.js.map +1 -0
  223. package/dist/extras/extensions/tool-buffer/extension.d.ts +89 -0
  224. package/dist/extras/extensions/tool-buffer/extension.d.ts.map +1 -0
  225. package/dist/extras/extensions/tool-buffer/extension.js +182 -0
  226. package/dist/extras/extensions/tool-buffer/extension.js.map +1 -0
  227. package/dist/extras/extensions/tool-buffer/extension.json +7 -0
  228. package/dist/extras/extensions/tool-buffer/extension.test.d.ts +2 -0
  229. package/dist/extras/extensions/tool-buffer/extension.test.d.ts.map +1 -0
  230. package/dist/extras/extensions/tool-buffer/extension.test.js +549 -0
  231. package/dist/extras/extensions/tool-buffer/extension.test.js.map +1 -0
  232. package/dist/extras/extensions/tool-buffer/index.d.ts +3 -0
  233. package/dist/extras/extensions/tool-buffer/index.d.ts.map +1 -0
  234. package/dist/extras/extensions/tool-buffer/index.js +2 -0
  235. package/dist/extras/extensions/tool-buffer/index.js.map +1 -0
  236. package/dist/extras/extensions/weather-tool/extension.d.ts +22 -0
  237. package/dist/extras/extensions/weather-tool/extension.d.ts.map +1 -0
  238. package/dist/extras/extensions/weather-tool/extension.js +33 -0
  239. package/dist/extras/extensions/weather-tool/extension.js.map +1 -0
  240. package/dist/extras/extensions/weather-tool/extension.json +7 -0
  241. package/dist/extras/extensions/weather-tool/index.d.ts +3 -0
  242. package/dist/extras/extensions/weather-tool/index.d.ts.map +1 -0
  243. package/dist/extras/extensions/weather-tool/index.js +2 -0
  244. package/dist/extras/extensions/weather-tool/index.js.map +1 -0
  245. package/dist/extras/models/ai-sdk-resolver.d.ts +15 -0
  246. package/dist/extras/models/ai-sdk-resolver.d.ts.map +1 -0
  247. package/dist/extras/models/ai-sdk-resolver.js +57 -0
  248. package/dist/extras/models/ai-sdk-resolver.js.map +1 -0
  249. package/dist/extras/models/index.d.ts +5 -0
  250. package/dist/extras/models/index.d.ts.map +1 -0
  251. package/dist/extras/models/index.js +3 -0
  252. package/dist/extras/models/index.js.map +1 -0
  253. package/dist/extras/models/routing-model.d.ts +18 -0
  254. package/dist/extras/models/routing-model.d.ts.map +1 -0
  255. package/dist/extras/models/routing-model.js +24 -0
  256. package/dist/extras/models/routing-model.js.map +1 -0
  257. package/dist/extras/openai-provider/index.d.ts +3 -0
  258. package/dist/extras/openai-provider/index.d.ts.map +1 -0
  259. package/dist/extras/openai-provider/index.js +2 -0
  260. package/dist/extras/openai-provider/index.js.map +1 -0
  261. package/dist/extras/openai-provider/server.d.ts +34 -0
  262. package/dist/extras/openai-provider/server.d.ts.map +1 -0
  263. package/dist/extras/openai-provider/server.js +335 -0
  264. package/dist/extras/openai-provider/server.js.map +1 -0
  265. package/dist/handle.d.ts +56 -0
  266. package/dist/handle.d.ts.map +1 -0
  267. package/dist/handle.js +2 -0
  268. package/dist/handle.js.map +1 -0
  269. package/dist/hooks.d.ts +98 -0
  270. package/dist/hooks.d.ts.map +1 -0
  271. package/dist/hooks.js +53 -0
  272. package/dist/hooks.js.map +1 -0
  273. package/dist/index.d.ts +16 -0
  274. package/dist/index.d.ts.map +1 -0
  275. package/dist/index.js +9 -0
  276. package/dist/index.js.map +1 -0
  277. package/dist/model.d.ts +75 -0
  278. package/dist/model.d.ts.map +1 -0
  279. package/dist/model.js +2 -0
  280. package/dist/model.js.map +1 -0
  281. package/dist/tool.d.ts +78 -0
  282. package/dist/tool.d.ts.map +1 -0
  283. package/dist/tool.js +78 -0
  284. package/dist/tool.js.map +1 -0
  285. package/dist/types.d.ts +83 -0
  286. package/dist/types.d.ts.map +1 -0
  287. package/dist/types.js +21 -0
  288. package/dist/types.js.map +1 -0
  289. package/package.json +178 -0
@@ -0,0 +1,167 @@
1
+ import type { Extension } from '@xandout/libra-harness';
2
+ import type { MessageContent, ToolCall } from '@xandout/libra-harness';
3
+ /**
4
+ * A single record in a session's JSONL log.
5
+ *
6
+ * Maps directly to a Message plus correlation metadata. The system
7
+ * prompt that was active for a turn is captured on the user record's
8
+ * `systemPrompt` field. Background channel messages (observed but not
9
+ * directed at the agent) are stored as `system` records so the agent
10
+ * sees them as context, not as messages to respond to.
11
+ */
12
+ export interface SessionRecord {
13
+ role: 'user' | 'assistant' | 'tool' | 'control' | 'system';
14
+ content: MessageContent;
15
+ toolCalls?: ToolCall[];
16
+ toolCallId?: string;
17
+ name?: string;
18
+ /** Message correlation id (e.g. a Slack message timestamp, a Discord message id). */
19
+ ts: string;
20
+ /** Thread parent correlation id — undefined for top-level messages. */
21
+ threadTs?: string;
22
+ /** ISO timestamp for internal tracking. */
23
+ recordedAt: string;
24
+ /**
25
+ * The system prompt that was active for this turn. Persisted on the
26
+ * user record (the first record of each turn) so every turn's JSONL
27
+ * entry shows exactly what instructions the model was operating under.
28
+ * Absent on assistant, tool, and control records. Lets you audit
29
+ * "why did the model respond this way?" without replaying config +
30
+ * extension state.
31
+ */
32
+ systemPrompt?: string;
33
+ /** Token usage from the LLM provider (accumulated across iterations). */
34
+ usage?: {
35
+ promptTokens: number;
36
+ completionTokens: number;
37
+ iterations: number;
38
+ /** Prompt tokens served from provider cache (accumulated). */
39
+ cachedPromptTokens?: number;
40
+ /** Reasoning/thinking tokens (accumulated). */
41
+ reasoningTokens?: number;
42
+ };
43
+ /**
44
+ * Generic enrichment bag. Populated opaquely from
45
+ * `ctx.turn.metadata.sessionMeta` — any extension may write into
46
+ * that metadata key during `beforeTurn`/`afterLLM` and disk-session
47
+ * persists it here without inspecting the contents. Keys are owned
48
+ * by the extension that writes them (e.g. `keywords`, `sentiment`).
49
+ */
50
+ meta?: Record<string, unknown>;
51
+ }
52
+ /**
53
+ * Identifies which session a turn belongs to.
54
+ *
55
+ * The host (or a resolver) produces this from turn metadata — the
56
+ * disk-session extension never reads host-specific types directly.
57
+ */
58
+ export interface SessionIdentity {
59
+ /** Stable session key — used as the JSONL filename and in-memory cache key. e.g. 'slack_C1', 'dm_U1', 'issue_42'.
60
+ * Should be filesystem-safe (alphanumeric, underscore, hyphen). Characters outside [a-zA-Z0-9_-] are replaced with '_' for the filename, so keys with colons or dots won't round-trip identically through a restart. */
61
+ key: string;
62
+ /** Correlation id for the incoming message (persisted as `record.ts`). */
63
+ messageTs: string;
64
+ /** Parent correlation id — when set, this message is a reply in a thread/sub-conversation. Persisted as `record.threadTs` and used as the context fork point. */
65
+ threadTs?: string;
66
+ /** True for 1:1 conversations (DMs) — disables thread forking, uses simple last-N history. */
67
+ isDirect?: boolean;
68
+ }
69
+ /**
70
+ * Extracts a {@link SessionIdentity} from turn metadata.
71
+ *
72
+ * The host supplies a resolver so disk-session doesn't need to know
73
+ * about Slack channels, Discord channels, or any other host concept.
74
+ * Return `undefined` to skip session handling for a turn.
75
+ */
76
+ export interface SessionResolver {
77
+ resolve(metadata: Record<string, unknown>): SessionIdentity | undefined;
78
+ }
79
+ export interface DiskSessionConfig {
80
+ /** Directory for session JSONL files. Default: ./sessions */
81
+ sessionDir?: string;
82
+ /**
83
+ * Max records to keep in memory per session. Older records are
84
+ * evicted from the cache but remain in the JSONL file.
85
+ * Default: 1000.
86
+ */
87
+ maxRecords?: number;
88
+ /**
89
+ * Max messages to include in the agent's context per turn.
90
+ * Default: 50.
91
+ */
92
+ maxContextMessages?: number;
93
+ /**
94
+ * Number of top-level (non-thread) messages to include as channel
95
+ * context when forking a thread. These are messages that came BEFORE
96
+ * the thread parent — what the channel was discussing when the thread
97
+ * started. Default: 10.
98
+ */
99
+ channelContextMessages?: number;
100
+ /**
101
+ * Number of recent top-level messages to include after the last thread
102
+ * reply. This gives the agent awareness of what's happened in the
103
+ * channel since the thread was last active (e.g. if someone comes back
104
+ * to a thread a week later, the agent sees recent channel activity).
105
+ * Default: 5.
106
+ */
107
+ recentChannelMessages?: number;
108
+ /** Load existing JSONL files into memory on startup. Default: true. */
109
+ loadOnStartup?: boolean;
110
+ /**
111
+ * Resolve a {@link SessionIdentity} from turn metadata. Default:
112
+ * reads `metadata.session` as a `SessionIdentity`, falls back to
113
+ * `metadata.sessionId` as a plain key, then to `'default'`.
114
+ */
115
+ resolver?: SessionResolver;
116
+ }
117
+ /**
118
+ * Disk-backed session extension.
119
+ *
120
+ * ## Architecture
121
+ *
122
+ * **One JSONL file per session** (`<sessionKey>.jsonl`). Append-only —
123
+ * every message the agent sees (human or bot, top-level or threaded) is
124
+ * appended. This log is the source of truth.
125
+ *
126
+ * **Snapshot at beforeTurn**: when a turn starts, the extension takes a
127
+ * read-only snapshot of the session log at that moment. The turn runs
128
+ * against the snapshot. Concurrent turns each take their own snapshot
129
+ * and don't see each other's in-progress work.
130
+ *
131
+ * **Fork for threads**: when a thread reply comes in (identity has
132
+ * `threadTs`), the snapshot is filtered to include only:
133
+ * 1. Top-level messages before the thread parent (channel context)
134
+ * 2. All messages in that thread (parent + replies)
135
+ * 3. Recent top-level messages after the last thread reply
136
+ *
137
+ * **Append after turn**: when the turn finishes, new messages (user +
138
+ * assistant + tool calls) are appended to the session log. No data is
139
+ * ever rewritten or reordered.
140
+ *
141
+ * **Stable prefix for LLM caching**: the messages array is built as
142
+ * [system prompt] + [stable session history] + [new user message]
143
+ * The session history grows monotonically (append-only), so earlier
144
+ * tokens stay cached upstream. Per-turn metadata is injected at the end
145
+ * of the context (by a host-side `beforeContext` hook), not the
146
+ * beginning, to preserve the cache prefix.
147
+ *
148
+ * ## Host integration
149
+ *
150
+ * The host supplies a {@link SessionResolver} (or writes
151
+ * `metadata.session`) so the extension knows which session a turn
152
+ * belongs to. The extension itself is host-agnostic — it doesn't know
153
+ * about Slack, Discord, or any other platform.
154
+ */
155
+ export default function createDiskSessionExtension(config?: DiskSessionConfig): Extension & {
156
+ getRecords(sessionKey?: string): SessionRecord[];
157
+ getSessions(): string[];
158
+ clear(sessionKey?: string): void;
159
+ clearAll(): void;
160
+ appendControl(sessionKey: string, content: string, ts?: string, threadTs?: string): void;
161
+ appendMessage(sessionKey: string, content: string, opts?: {
162
+ ts?: string;
163
+ threadTs?: string;
164
+ meta?: Record<string, unknown>;
165
+ }): void;
166
+ };
167
+ //# sourceMappingURL=extension.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../../../../src/extras/extensions/disk-session/extension.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,492 @@
1
+ import { readFileSync, appendFileSync, mkdirSync, readdirSync, writeFileSync, existsSync, renameSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Default resolver: reads `metadata.session` as a `SessionIdentity`,
5
+ * falls back to `metadata.sessionId` as a plain key, then to `'default'`.
6
+ */
7
+ const defaultResolver = {
8
+ resolve(metadata) {
9
+ const session = metadata.session;
10
+ if (session && typeof session.key === 'string')
11
+ return session;
12
+ const sessionId = metadata.sessionId;
13
+ if (sessionId)
14
+ return { key: sessionId, messageTs: '' };
15
+ return { key: 'default', messageTs: '' };
16
+ },
17
+ };
18
+ /**
19
+ * Disk-backed session extension.
20
+ *
21
+ * ## Architecture
22
+ *
23
+ * **One JSONL file per session** (`<sessionKey>.jsonl`). Append-only —
24
+ * every message the agent sees (human or bot, top-level or threaded) is
25
+ * appended. This log is the source of truth.
26
+ *
27
+ * **Snapshot at beforeTurn**: when a turn starts, the extension takes a
28
+ * read-only snapshot of the session log at that moment. The turn runs
29
+ * against the snapshot. Concurrent turns each take their own snapshot
30
+ * and don't see each other's in-progress work.
31
+ *
32
+ * **Fork for threads**: when a thread reply comes in (identity has
33
+ * `threadTs`), the snapshot is filtered to include only:
34
+ * 1. Top-level messages before the thread parent (channel context)
35
+ * 2. All messages in that thread (parent + replies)
36
+ * 3. Recent top-level messages after the last thread reply
37
+ *
38
+ * **Append after turn**: when the turn finishes, new messages (user +
39
+ * assistant + tool calls) are appended to the session log. No data is
40
+ * ever rewritten or reordered.
41
+ *
42
+ * **Stable prefix for LLM caching**: the messages array is built as
43
+ * [system prompt] + [stable session history] + [new user message]
44
+ * The session history grows monotonically (append-only), so earlier
45
+ * tokens stay cached upstream. Per-turn metadata is injected at the end
46
+ * of the context (by a host-side `beforeContext` hook), not the
47
+ * beginning, to preserve the cache prefix.
48
+ *
49
+ * ## Host integration
50
+ *
51
+ * The host supplies a {@link SessionResolver} (or writes
52
+ * `metadata.session`) so the extension knows which session a turn
53
+ * belongs to. The extension itself is host-agnostic — it doesn't know
54
+ * about Slack, Discord, or any other platform.
55
+ */
56
+ export default function createDiskSessionExtension(config) {
57
+ const dir = config?.sessionDir ?? './sessions';
58
+ const maxRecords = config?.maxRecords ?? 1000;
59
+ const maxContextMessages = config?.maxContextMessages ?? 50;
60
+ const channelContextMessages = config?.channelContextMessages ?? 10;
61
+ const recentChannelMessages = config?.recentChannelMessages ?? 5;
62
+ const loadOnStartup = config?.loadOnStartup ?? true;
63
+ const resolver = config?.resolver ?? defaultResolver;
64
+ mkdirSync(dir, { recursive: true });
65
+ // In-memory cache: sessionKey → SessionRecord[]
66
+ // This is the live log. Reads take a snapshot (copy); writes append.
67
+ const store = new Map();
68
+ // ── Load existing sessions from disk ──────────────────────────
69
+ if (loadOnStartup) {
70
+ try {
71
+ const files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
72
+ let totalRecords = 0;
73
+ for (const file of files) {
74
+ try {
75
+ const raw = readFileSync(join(dir, file), 'utf-8');
76
+ const lines = raw.split('\n').filter((l) => l.trim());
77
+ const records = lines.map((l) => JSON.parse(l));
78
+ const sessionKey = file.replace(/\.jsonl$/, '');
79
+ store.set(sessionKey, records.slice(-maxRecords));
80
+ totalRecords += records.length;
81
+ }
82
+ catch {
83
+ // Skip corrupt files.
84
+ }
85
+ }
86
+ if (store.size > 0) {
87
+ console.log(`[disk-session] loaded ${store.size} session(s), ${totalRecords} record(s) from ${dir}`);
88
+ }
89
+ }
90
+ catch {
91
+ // Directory doesn't exist — start empty.
92
+ }
93
+ }
94
+ function filePath(sessionKey) {
95
+ const safe = sessionKey.replace(/[^a-zA-Z0-9_-]/g, '_');
96
+ return join(dir, `${safe}.jsonl`);
97
+ }
98
+ function appendToFile(sessionKey, records) {
99
+ if (records.length === 0)
100
+ return;
101
+ const lines = records.map((r) => JSON.stringify(r)).join('\n') + '\n';
102
+ try {
103
+ appendFileSync(filePath(sessionKey), lines);
104
+ }
105
+ catch (err) {
106
+ console.error(`[disk-session] failed to append to ${sessionKey}:`, err);
107
+ }
108
+ }
109
+ // ── Convert between Message and SessionRecord ──────────────────
110
+ function toMessage(r) {
111
+ return {
112
+ // `toMessage` is only called on records that passed
113
+ // `isConversationMessage`, which excludes 'control' (and 'tool').
114
+ role: r.role,
115
+ content: r.content,
116
+ ...(r.toolCalls ? { toolCalls: r.toolCalls } : {}),
117
+ ...(r.toolCallId ? { toolCallId: r.toolCallId } : {}),
118
+ ...(r.name ? { name: r.name } : {}),
119
+ };
120
+ }
121
+ function toRecord(msg, ts, threadTs) {
122
+ return {
123
+ role: msg.role,
124
+ content: msg.content,
125
+ ...(msg.toolCalls ? { toolCalls: msg.toolCalls } : {}),
126
+ ...(msg.toolCallId ? { toolCallId: msg.toolCallId } : {}),
127
+ ...(msg.name ? { name: msg.name } : {}),
128
+ ts,
129
+ ...(threadTs ? { threadTs } : {}),
130
+ recordedAt: new Date().toISOString(),
131
+ };
132
+ }
133
+ // Filter for context windows: only keep user messages, system
134
+ // messages (background channel context), and assistant messages with
135
+ // text content (not tool-call intermediates or control records).
136
+ // Tool messages, control records, and assistant messages that only
137
+ // contain toolCalls are turn-internal mechanics — including them
138
+ // without the full tool-call flow produces invalid message sequences
139
+ // for the LLM. Control records (e.g. /halt) are persisted for audit
140
+ // but never shown to the model.
141
+ const isConversationMessage = (r) => {
142
+ if (r.role === 'tool')
143
+ return false;
144
+ if (r.role === 'control')
145
+ return false;
146
+ // Drop any assistant message that has tool_calls — even if it also
147
+ // has text content. Keeping it without the matching tool response
148
+ // messages produces an invalid sequence for the LLM.
149
+ if (r.role === 'assistant' && r.toolCalls?.length)
150
+ return false;
151
+ return true; // user, system, assistant (without tool calls)
152
+ };
153
+ // ── Build context (the "fork") from a snapshot ────────────────
154
+ // The snapshot is a read-only copy of the session records at the
155
+ // moment the turn started. This function builds the messages array
156
+ // that the agent will see.
157
+ //
158
+ // For top-level messages and DMs: last N records (simple slice).
159
+ // For thread replies: channel context before parent + thread history.
160
+ function buildContext(snapshot, threadTs, isDirect) {
161
+ if (snapshot.length === 0)
162
+ return [];
163
+ // DM or top-level message: last N messages, but filter out
164
+ // tool-call intermediates that might get split by the slice.
165
+ if (isDirect || !threadTs) {
166
+ return snapshot
167
+ .filter(isConversationMessage)
168
+ .slice(-maxContextMessages)
169
+ .map(toMessage);
170
+ }
171
+ // Thread: fork from channel context.
172
+ const parentIdx = snapshot.findIndex((r) => r.ts === threadTs);
173
+ if (parentIdx === -1) {
174
+ // Parent not in snapshot (evicted from cache or very old).
175
+ // Fall back to last N messages.
176
+ return snapshot
177
+ .filter(isConversationMessage)
178
+ .slice(-maxContextMessages)
179
+ .map(toMessage);
180
+ }
181
+ // Top-level messages before the parent (channel context at fork point).
182
+ const topLevelBefore = snapshot
183
+ .slice(0, parentIdx)
184
+ .filter((r) => !r.threadTs && isConversationMessage(r))
185
+ .slice(-channelContextMessages);
186
+ // All messages in this thread (including the parent).
187
+ // Filter out tool-call intermediates — they produce invalid message
188
+ // sequences if they get split or truncated by the context window.
189
+ const threadMessages = snapshot
190
+ .filter((r) => (r.ts === threadTs || r.threadTs === threadTs) && isConversationMessage(r));
191
+ // Recent top-level messages after the last thread reply.
192
+ // This gives the agent awareness of what's happened in the channel
193
+ // since the thread was last active — without pulling in the entire
194
+ // channel history. Find the last thread message's index in the
195
+ // snapshot by ts, then take top-level messages after it.
196
+ const lastThreadTs = threadMessages[threadMessages.length - 1]?.ts ?? threadTs;
197
+ let lastThreadIdx = parentIdx;
198
+ for (let i = snapshot.length - 1; i >= 0; i--) {
199
+ if (snapshot[i].ts === lastThreadTs) {
200
+ lastThreadIdx = i;
201
+ break;
202
+ }
203
+ }
204
+ const recentTopLevel = recentChannelMessages > 0
205
+ ? snapshot
206
+ .slice(lastThreadIdx + 1)
207
+ .filter((r) => !r.threadTs && isConversationMessage(r))
208
+ .slice(-recentChannelMessages)
209
+ : [];
210
+ return [...topLevelBefore, ...threadMessages, ...recentTopLevel].map(toMessage);
211
+ }
212
+ // ── Resolve session identity from turn metadata ───────────────
213
+ function identityFromCtx(ctx) {
214
+ return resolver.resolve(ctx.turn.request.metadata ?? {});
215
+ }
216
+ return {
217
+ name: 'disk-session',
218
+ priority: -100,
219
+ install(agent) {
220
+ // ── beforeTurn: take snapshot, build context, prepend ─────
221
+ // The snapshot is a copy of the session records at this moment.
222
+ // Concurrent turns each get their own snapshot — they don't
223
+ // see each other's in-progress work.
224
+ agent.hook('beforeTurn', 'disk-session', async (ctx) => {
225
+ const identity = identityFromCtx(ctx);
226
+ if (!identity)
227
+ return;
228
+ const { key, messageTs, threadTs, isDirect } = identity;
229
+ const isDm = isDirect ?? false;
230
+ // Snapshot: copy the current records (read-only).
231
+ const liveRecords = store.get(key) ?? [];
232
+ const snapshot = [...liveRecords];
233
+ // Build the forked context from the snapshot.
234
+ const history = buildContext(snapshot, threadTs, isDm);
235
+ // ── Persist the user message immediately ────────────────
236
+ // Write the incoming user message to disk NOW, before the
237
+ // agent runs. If the process crashes mid-turn, the user's
238
+ // message is already saved. The assistant response is
239
+ // appended later in afterTurn.
240
+ //
241
+ // The system prompt is captured here (on the user record) so
242
+ // each turn's JSONL entry shows exactly what instructions the
243
+ // model was operating under. It's available from beforeTurn
244
+ // onward via ctx.turn.systemPrompt (resolved by the core from
245
+ // the per-turn override ?? agent config).
246
+ //
247
+ // Enrichment bag: any extension that ran before us in
248
+ // beforeTurn may have written into `sessionMeta`. We persist
249
+ // it opaquely — disk-session does not inspect the contents.
250
+ const sessionMeta = ctx.turn.metadata.sessionMeta;
251
+ const systemPrompt = ctx.turn.systemPrompt;
252
+ const userRecord = {
253
+ ...toRecord({ role: 'user', content: ctx.turn.request.message }, messageTs, threadTs),
254
+ ...(systemPrompt ? { systemPrompt } : {}),
255
+ ...(sessionMeta ? { meta: sessionMeta } : {}),
256
+ };
257
+ const records = store.get(key) ?? [];
258
+ records.push(userRecord);
259
+ if (records.length > maxRecords) {
260
+ records.splice(0, records.length - maxRecords);
261
+ }
262
+ store.set(key, records);
263
+ appendToFile(key, [userRecord]);
264
+ // Track how many history messages we prepended so afterTurn
265
+ // knows where new messages start. +1 for the user message
266
+ // we just added to the log (it's in the snapshot but not in
267
+ // the history we built — the history was built from the
268
+ // snapshot BEFORE adding the user record).
269
+ ctx.turn.metadata['_diskSessionHistoryLen'] = history.length;
270
+ if (history.length > 0) {
271
+ ctx.turn.messages = [...history, ...ctx.turn.messages];
272
+ }
273
+ });
274
+ // ── afterLLM: accumulate token usage across iterations ─────
275
+ // Each LLM call returns usage (prompt/completion tokens). We
276
+ // accumulate them across all iterations in the turn so the final
277
+ // assistant record has the total cost.
278
+ agent.hook('afterLLM', 'disk-session', async (ctx) => {
279
+ const usage = ctx.modelResponse?.usage;
280
+ if (!usage)
281
+ return;
282
+ const prev = ctx.turn.metadata['_diskSessionUsage'] ?? { promptTokens: 0, completionTokens: 0, iterations: 0 };
283
+ ctx.turn.metadata['_diskSessionUsage'] = {
284
+ promptTokens: prev.promptTokens + usage.promptTokens,
285
+ completionTokens: prev.completionTokens + usage.completionTokens,
286
+ iterations: prev.iterations + 1,
287
+ ...(usage.cachedPromptTokens && {
288
+ cachedPromptTokens: (prev.cachedPromptTokens ?? 0) + usage.cachedPromptTokens,
289
+ }),
290
+ ...(usage.reasoningTokens && {
291
+ reasoningTokens: (prev.reasoningTokens ?? 0) + usage.reasoningTokens,
292
+ }),
293
+ };
294
+ });
295
+ // ── afterTurn: append assistant response + tool calls ─────
296
+ // The user message was already persisted in beforeTurn.
297
+ // Here we only append the NEW non-user, non-system messages
298
+ // (assistant responses, tool calls, tool results).
299
+ agent.hook('afterTurn', 'disk-session', async (ctx) => {
300
+ const identity = identityFromCtx(ctx);
301
+ if (!identity)
302
+ return;
303
+ const { key, messageTs, threadTs } = identity;
304
+ const historyLen = ctx.turn.metadata['_diskSessionHistoryLen'] ?? 0;
305
+ delete ctx.turn.metadata['_diskSessionHistoryLen'];
306
+ // ── Compaction: rotate the session file ──────────────────
307
+ // When metadata.compacting is set, this turn was a compaction
308
+ // request (triggered by /compact). The agent's response is the
309
+ // summary. Instead of appending to the old session, we:
310
+ // 1. Rename the old JSONL file with a timestamp suffix
311
+ // 2. Seed a new session with the summary as the first record
312
+ // The compaction request/response are already in the old file
313
+ // (written by beforeTurn + the normal append below would have
314
+ // run, but we return early before that).
315
+ const isCompacting = ctx.turn.request.metadata?.compacting === true;
316
+ if (isCompacting) {
317
+ const summary = ctx.turn.response?.message ?? '';
318
+ // Archive the old file with a timestamp suffix.
319
+ const oldPath = filePath(key);
320
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
321
+ const archivedName = `${key.replace(/[^a-zA-Z0-9_-]/g, '_')}_${timestamp}.jsonl`;
322
+ const archivedPath = join(dir, archivedName);
323
+ if (existsSync(oldPath)) {
324
+ renameSync(oldPath, archivedPath);
325
+ }
326
+ // Seed the new session with the summary.
327
+ const summaryRecord = {
328
+ role: 'assistant',
329
+ content: `[Session summary — prior conversation compacted ${new Date().toISOString()}]\n\n${summary}`,
330
+ ts: messageTs || String(Date.now() / 1000),
331
+ recordedAt: new Date().toISOString(),
332
+ };
333
+ store.set(key, [summaryRecord]);
334
+ appendToFile(key, [summaryRecord]);
335
+ // Store the archived filename so the host can report it.
336
+ ctx.turn.metadata['_compactArchivedFile'] = archivedName;
337
+ return;
338
+ }
339
+ // Messages: [history...] [system?] [user msg] [assistant] [tool calls...]
340
+ // The beforeContext hook may have inserted a system message.
341
+ // Skip it when counting new messages.
342
+ const systemMsgCount = ctx.turn.messages[historyLen]?.role === 'system' ? 1 : 0;
343
+ const newStart = historyLen + systemMsgCount;
344
+ const newMessages = ctx.turn.messages.slice(newStart);
345
+ // Only persist non-user, non-system messages here.
346
+ // The user message was already written in beforeTurn.
347
+ const filteredMessages = newMessages.filter((m) => m.role !== 'system' && m.role !== 'user');
348
+ if (filteredMessages.length === 0)
349
+ return;
350
+ // Pull accumulated usage for this turn (set by afterLLM hook).
351
+ const usage = ctx.turn.metadata['_diskSessionUsage'];
352
+ delete ctx.turn.metadata['_diskSessionUsage'];
353
+ // Enrichment bag (same convention as beforeTurn): any
354
+ // extension may have written into `sessionMeta` during the
355
+ // turn. Persist it opaquely on each new record.
356
+ const sessionMeta = ctx.turn.metadata.sessionMeta;
357
+ // Convert to records with correlation metadata.
358
+ const newRecords = filteredMessages.map((msg) => {
359
+ const record = {
360
+ ...toRecord(msg, messageTs, threadTs),
361
+ ...(sessionMeta ? { meta: sessionMeta } : {}),
362
+ };
363
+ // Attach usage to the LAST assistant record (the final response).
364
+ // Intermediate assistant records (tool-call requests) don't get usage.
365
+ return record;
366
+ });
367
+ // Attach accumulated usage to the last assistant record.
368
+ if (usage) {
369
+ let lastAssistantIdx = -1;
370
+ for (let i = newRecords.length - 1; i >= 0; i--) {
371
+ if (newRecords[i].role === 'assistant') {
372
+ lastAssistantIdx = i;
373
+ break;
374
+ }
375
+ }
376
+ if (lastAssistantIdx !== -1) {
377
+ newRecords[lastAssistantIdx].usage = usage;
378
+ }
379
+ }
380
+ // Append to in-memory log.
381
+ const records = store.get(key) ?? [];
382
+ records.push(...newRecords);
383
+ // Trim in-memory cache (file keeps full history).
384
+ if (records.length > maxRecords) {
385
+ records.splice(0, records.length - maxRecords);
386
+ }
387
+ store.set(key, records);
388
+ // Append to JSONL file (append-only, never rewritten).
389
+ appendToFile(key, newRecords);
390
+ });
391
+ },
392
+ /** Get all records for a session. */
393
+ getRecords(sessionKey = 'default') {
394
+ return store.get(sessionKey) ?? [];
395
+ },
396
+ /**
397
+ * Append a control record (e.g. /halt) to the session log.
398
+ * Control records are persisted to disk but never included in the
399
+ * LLM context — they're for audit/logging only.
400
+ */
401
+ appendControl(sessionKey, content, ts, threadTs) {
402
+ const record = {
403
+ role: 'control',
404
+ content,
405
+ ts: ts ?? String(Date.now() / 1000),
406
+ ...(threadTs ? { threadTs } : {}),
407
+ recordedAt: new Date().toISOString(),
408
+ };
409
+ const records = store.get(sessionKey);
410
+ if (records) {
411
+ records.push(record);
412
+ if (records.length > maxRecords) {
413
+ store.set(sessionKey, records.slice(-maxRecords));
414
+ }
415
+ }
416
+ else {
417
+ store.set(sessionKey, [record]);
418
+ }
419
+ appendToFile(sessionKey, [record]);
420
+ },
421
+ /**
422
+ * Append a background message to the session log. Stored as a
423
+ * `system` record so the agent sees it as context ("here's what
424
+ * the channel discussed") rather than a message to respond to.
425
+ *
426
+ * Unlike `appendControl`, system records ARE included in the LLM
427
+ * context window — they appear in the conversation history when
428
+ * the agent is triggered, giving it awareness of recent channel
429
+ * activity without needing to be mentioned on every message.
430
+ *
431
+ * The host should include sender info in the content (e.g.
432
+ * `[U123]: hey anyone seen the invoice?`) so the agent can
433
+ * distinguish who said what. Additional metadata (files, blocks,
434
+ * etc.) goes in `opts.meta` and is persisted to the JSONL for
435
+ * audit/debugging but not shown to the LLM.
436
+ */
437
+ appendMessage(sessionKey, content, opts) {
438
+ const record = {
439
+ role: 'system',
440
+ content,
441
+ ts: opts?.ts ?? String(Date.now() / 1000),
442
+ ...(opts?.threadTs ? { threadTs: opts.threadTs } : {}),
443
+ recordedAt: new Date().toISOString(),
444
+ ...(opts?.meta ? { meta: opts.meta } : {}),
445
+ };
446
+ const records = store.get(sessionKey);
447
+ if (records) {
448
+ records.push(record);
449
+ if (records.length > maxRecords) {
450
+ store.set(sessionKey, records.slice(-maxRecords));
451
+ }
452
+ }
453
+ else {
454
+ store.set(sessionKey, [record]);
455
+ }
456
+ appendToFile(sessionKey, [record]);
457
+ },
458
+ /** List all session keys. */
459
+ getSessions() {
460
+ return Array.from(store.keys());
461
+ },
462
+ /** Clear a single session (memory + disk). */
463
+ clear(sessionKey = 'default') {
464
+ store.delete(sessionKey);
465
+ const path = filePath(sessionKey);
466
+ if (existsSync(path)) {
467
+ try {
468
+ writeFileSync(path, '');
469
+ }
470
+ catch {
471
+ // ignore
472
+ }
473
+ }
474
+ },
475
+ /** Clear all sessions. */
476
+ clearAll() {
477
+ for (const key of store.keys()) {
478
+ const path = filePath(key);
479
+ if (existsSync(path)) {
480
+ try {
481
+ writeFileSync(path, '');
482
+ }
483
+ catch {
484
+ // ignore
485
+ }
486
+ }
487
+ }
488
+ store.clear();
489
+ },
490
+ };
491
+ }
492
+ //# sourceMappingURL=extension.js.map