@vellumai/assistant 0.8.11 → 0.8.12-staging.2

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 (244) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +633 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  37. package/src/__tests__/llm-usage-store.test.ts +11 -0
  38. package/src/__tests__/log-export-workspace.test.ts +468 -3
  39. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  40. package/src/__tests__/model-intents.test.ts +1 -1
  41. package/src/__tests__/oauth-cli.test.ts +19 -8
  42. package/src/__tests__/openai-provider.test.ts +34 -0
  43. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  44. package/src/__tests__/recurrence-engine.test.ts +45 -0
  45. package/src/__tests__/schedule-routes.test.ts +34 -0
  46. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  48. package/src/__tests__/secret-fixtures.ts +20 -0
  49. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  50. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  51. package/src/__tests__/subagent-tools.test.ts +1 -0
  52. package/src/__tests__/system-prompt.test.ts +109 -11
  53. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  55. package/src/__tests__/tool-error-hook.test.ts +1 -0
  56. package/src/__tests__/tool-result-spool.test.ts +337 -0
  57. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  58. package/src/__tests__/validate-input.test.ts +95 -1
  59. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  60. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  61. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  62. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  63. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  64. package/src/acp/__tests__/agent-process.test.ts +315 -2
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  66. package/src/acp/agent-process.ts +163 -34
  67. package/src/acp/prepare-agent-env.ts +55 -15
  68. package/src/agent/loop.ts +81 -24
  69. package/src/api/events/usage-progress.ts +28 -0
  70. package/src/api/index.ts +6 -0
  71. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  72. package/src/bundler/app-bundler.ts +25 -42
  73. package/src/bundler/app-compiler.ts +8 -0
  74. package/src/calls/call-controller.ts +1 -1
  75. package/src/cli/commands/plugins.ts +248 -15
  76. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  77. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  78. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  79. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  80. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  81. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
  82. package/src/cli/lib/inspect-plugin.ts +252 -0
  83. package/src/cli/lib/install-from-github.ts +214 -21
  84. package/src/cli/lib/list-installed-plugins.ts +17 -6
  85. package/src/cli/lib/plugin-artifact.ts +103 -0
  86. package/src/cli/lib/plugin-details.ts +18 -1
  87. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  88. package/src/cli/lib/upgrade-plugin.ts +225 -0
  89. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  90. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  91. package/src/config/call-site-defaults.ts +13 -2
  92. package/src/config/feature-flag-registry.json +8 -16
  93. package/src/config/loader.ts +52 -59
  94. package/src/config/schema.ts +0 -2
  95. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  96. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  97. package/src/config/schemas/llm.ts +10 -0
  98. package/src/config/schemas/memory-v2.ts +13 -0
  99. package/src/config/schemas/memory-v3.ts +92 -0
  100. package/src/config/seed-inference-profiles.ts +4 -8
  101. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  102. package/src/context/tool-result-spool.ts +104 -0
  103. package/src/credential-execution/feature-gates.ts +0 -1
  104. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  105. package/src/daemon/conversation-error.ts +6 -15
  106. package/src/daemon/conversation.ts +9 -0
  107. package/src/daemon/disk-pressure-policy.ts +0 -1
  108. package/src/daemon/lifecycle.ts +1 -20
  109. package/src/daemon/message-types/conversations.ts +2 -15
  110. package/src/daemon/trust-context.ts +1 -1
  111. package/src/events/tool-audit-listener.ts +40 -9
  112. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  113. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  114. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  115. package/src/home/home-content-refresh.ts +43 -31
  116. package/src/home/home-greeting-cache.ts +8 -1
  117. package/src/home/home-greeting.ts +13 -9
  118. package/src/home/suggested-prompts.ts +77 -24
  119. package/src/ipc/routes/trust-rules.test.ts +66 -72
  120. package/src/media/image-credentials.ts +2 -2
  121. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  122. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  123. package/src/memory/conversation-attention-store.ts +1 -0
  124. package/src/memory/conversation-bootstrap.ts +18 -9
  125. package/src/memory/conversation-crud.ts +12 -2
  126. package/src/memory/conversation-title-service.ts +53 -9
  127. package/src/memory/delivery-channels.ts +0 -69
  128. package/src/memory/graph/extraction-job.ts +0 -15
  129. package/src/memory/guardian-action-store.ts +1 -376
  130. package/src/memory/llm-usage-store.ts +5 -1
  131. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  132. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  133. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  134. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  135. package/src/memory/v2/consolidation-job.ts +93 -9
  136. package/src/memory/v2/injection.ts +53 -0
  137. package/src/memory/v2/prompts/consolidation.ts +1 -0
  138. package/src/memory/v2/static-context.ts +13 -1
  139. package/src/memory/v2/sweep-job.ts +1 -1
  140. package/src/memory/v2/types.ts +5 -0
  141. package/src/plugin-api/types.ts +7 -0
  142. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  143. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  144. package/src/plugins/defaults/index.ts +25 -0
  145. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  146. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  147. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  148. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  149. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  150. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  151. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  152. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  153. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  154. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  155. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  156. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  157. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  158. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  159. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  160. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  161. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  162. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  163. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  164. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  165. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  166. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  167. package/src/prompts/cache-boundary.ts +17 -0
  168. package/src/prompts/sections.ts +50 -17
  169. package/src/prompts/system-prompt.ts +12 -4
  170. package/src/prompts/templates/system-sections.ts +22 -0
  171. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  172. package/src/providers/anthropic/client.ts +74 -28
  173. package/src/providers/gemini/client.ts +5 -1
  174. package/src/providers/minimax/client.ts +9 -0
  175. package/src/providers/model-catalog.ts +28 -0
  176. package/src/providers/model-intents.ts +3 -3
  177. package/src/providers/openai/chat-completions-provider.ts +4 -2
  178. package/src/providers/openai/responses-provider.ts +7 -2
  179. package/src/providers/retry.ts +8 -0
  180. package/src/providers/types.ts +11 -0
  181. package/src/providers/unparseable-tool-args.ts +56 -0
  182. package/src/runtime/AGENTS.md +6 -0
  183. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  184. package/src/runtime/agent-wake.ts +5 -5
  185. package/src/runtime/background-job-runner.ts +2 -2
  186. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  187. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  188. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  189. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  190. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  191. package/src/runtime/pre-first-message-gate.ts +1 -1
  192. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  193. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  194. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  195. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  196. package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
  197. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  198. package/src/runtime/routes/btw-routes.ts +0 -14
  199. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  200. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  201. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  202. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  203. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  204. package/src/runtime/routes/home-feed-routes.ts +10 -0
  205. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  206. package/src/runtime/routes/identity-routes.ts +76 -20
  207. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  208. package/src/runtime/routes/log-export-routes.ts +143 -96
  209. package/src/runtime/routes/plugins-routes.ts +380 -0
  210. package/src/runtime/routes/redact-staged-export.ts +259 -0
  211. package/src/runtime/routes/schedule-routes.ts +19 -2
  212. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  213. package/src/schedule/recurrence-engine.ts +34 -0
  214. package/src/schedule/scheduler.ts +1 -0
  215. package/src/security/redact-json.ts +61 -0
  216. package/src/skills/validate-input.ts +41 -1
  217. package/src/subagent/types.ts +26 -1
  218. package/src/telemetry/types.ts +15 -1
  219. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  220. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  221. package/src/tools/apps/executors.ts +1 -1
  222. package/src/tools/skills/skill-tool-factory.ts +19 -8
  223. package/src/tools/tool-approval-handler.ts +31 -0
  224. package/src/usage/types.ts +8 -1
  225. package/src/util/platform.ts +16 -0
  226. package/src/watcher/engine.ts +1 -0
  227. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  228. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  229. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  230. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  231. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  232. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  233. package/src/workspace/migrations/registry.ts +8 -0
  234. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  235. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  236. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  237. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  238. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  239. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  240. package/src/config/schemas/updates.ts +0 -14
  241. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  242. package/src/memory/conversation-starters-cadence.ts +0 -78
  243. package/src/prompts/update-bulletin-job.ts +0 -180
  244. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Tests for the ClickHouse compaction log store.
3
+ *
4
+ * The store is exercised with an injected `fetchImpl` so the tests pin the
5
+ * wire shape: lazy CREATE TABLE on first write, INSERT statements via the
6
+ * `query` URL param with JSONEachRow bodies, the start/end row mappings
7
+ * (sentinels on the start row, full `ContextWindowResult` projection on the
8
+ * end row), and the read path that pairs rows back into per-attempt events.
9
+ */
10
+ import { describe, expect, test } from "bun:test";
11
+
12
+ import type { ContextWindowResult } from "../../plugins/defaults/compaction/window-manager.js";
13
+ import {
14
+ ClickHouseCompactionLogStore,
15
+ type CompactionEndEvent,
16
+ type CompactionStartEvent,
17
+ } from "../compaction-log-store-clickhouse.js";
18
+
19
+ interface CapturedRequest {
20
+ url: URL;
21
+ body: string;
22
+ }
23
+
24
+ function createWriter(overrides?: {
25
+ fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;
26
+ }): { writer: ClickHouseCompactionLogStore; requests: CapturedRequest[] } {
27
+ const requests: CapturedRequest[] = [];
28
+ const fetchImpl = (overrides?.fetchImpl ??
29
+ (async (url: string, init?: RequestInit) => {
30
+ requests.push({ url: new URL(url), body: String(init?.body ?? "") });
31
+ return new Response("", { status: 200 });
32
+ })) as typeof fetch;
33
+ const writer = new ClickHouseCompactionLogStore(
34
+ { database: "analytics", table: "compaction_logs", user: "writer" },
35
+ {
36
+ resolveUrl: async () => "http://clickhouse.example.com:8123",
37
+ resolvePassword: async () => "secret",
38
+ resolveAssistantId: async () => "assistant-123",
39
+ fetchImpl,
40
+ },
41
+ );
42
+ return { writer, requests };
43
+ }
44
+
45
+ const startEvent: CompactionStartEvent = {
46
+ type: "context_compacting",
47
+ compactionId: "comp-1",
48
+ requestId: "req-1",
49
+ trigger: "budget",
50
+ startedAt: 1000,
51
+ messages: [
52
+ { role: "user", content: [{ type: "text", text: "hi" }] },
53
+ { role: "assistant", content: [{ type: "text", text: "hello" }] },
54
+ ],
55
+ };
56
+
57
+ const result: ContextWindowResult = {
58
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
59
+ compacted: true,
60
+ previousEstimatedInputTokens: 900,
61
+ estimatedInputTokens: 300,
62
+ maxInputTokens: 1000,
63
+ thresholdTokens: 850,
64
+ compactedMessages: 10,
65
+ compactedPersistedMessages: 8,
66
+ preservedTailMessages: 2,
67
+ summaryCalls: 1,
68
+ summaryInputTokens: 880,
69
+ summaryOutputTokens: 120,
70
+ summaryModel: "test-model",
71
+ summaryText: "summary text",
72
+ reason: "auto",
73
+ exhausted: false,
74
+ };
75
+
76
+ const endEvent: CompactionEndEvent = {
77
+ type: "compaction_completed",
78
+ compactionId: "comp-1",
79
+ requestId: "req-1",
80
+ trigger: "budget",
81
+ startedAt: 1000,
82
+ finishedAt: 1500,
83
+ ...result,
84
+ };
85
+
86
+ describe("ClickHouseCompactionLogStore", () => {
87
+ test("first write creates the table then inserts", async () => {
88
+ const { writer, requests } = createWriter();
89
+
90
+ await writer.writeStart("conv-1", startEvent);
91
+
92
+ expect(requests.length).toBe(2);
93
+ expect(requests[0]!.body).toContain("CREATE TABLE IF NOT EXISTS");
94
+ expect(requests[0]!.body).toContain("`compaction_logs`");
95
+ expect(requests[0]!.url.searchParams.get("database")).toBe("analytics");
96
+
97
+ const insert = requests[1]!;
98
+ expect(insert.url.searchParams.get("query")).toBe(
99
+ "INSERT INTO `compaction_logs` FORMAT JSONEachRow",
100
+ );
101
+ const row = JSON.parse(insert.body) as Record<string, unknown>;
102
+ expect(row.assistant_id).toBe("assistant-123");
103
+ expect(row.conversation_id).toBe("conv-1");
104
+ expect(row.compaction_id).toBe("comp-1");
105
+ expect(row.request_id).toBe("req-1");
106
+ expect(row.phase).toBe("start");
107
+ expect(row.trigger).toBe("budget");
108
+ expect(row.started_at).toBe(1000);
109
+ expect(row.pre_message_count).toBe(2);
110
+ // End-only fields stay at their sentinels on the start row.
111
+ expect(row.finished_at).toBe(0);
112
+ expect(row.duration_ms).toBe(-1);
113
+ expect(row.estimated_input_tokens).toBe(-1);
114
+ expect(row.summary_model).toBe("");
115
+ });
116
+
117
+ test("table creation happens once across writes", async () => {
118
+ const { writer, requests } = createWriter();
119
+
120
+ await writer.writeStart("conv-1", startEvent);
121
+ await writer.writeEnd("conv-1", endEvent);
122
+
123
+ const ddl = requests.filter((r) => r.body.includes("CREATE TABLE"));
124
+ expect(ddl.length).toBe(1);
125
+ expect(requests.length).toBe(3);
126
+ });
127
+
128
+ test("end row projects the full ContextWindowResult", async () => {
129
+ const { writer, requests } = createWriter();
130
+
131
+ await writer.writeEnd("conv-1", endEvent);
132
+
133
+ const row = JSON.parse(requests[1]!.body) as Record<string, unknown>;
134
+ expect(row.phase).toBe("end");
135
+ expect(row.finished_at).toBe(1500);
136
+ expect(row.duration_ms).toBe(500);
137
+ expect(row.result_message_count).toBe(1);
138
+ expect(row.compacted).toBe(1);
139
+ expect(row.previous_estimated_input_tokens).toBe(900);
140
+ expect(row.estimated_input_tokens).toBe(300);
141
+ expect(row.max_input_tokens).toBe(1000);
142
+ expect(row.threshold_tokens).toBe(850);
143
+ expect(row.compacted_messages).toBe(10);
144
+ expect(row.compacted_persisted_messages).toBe(8);
145
+ expect(row.preserved_tail_messages).toBe(2);
146
+ expect(row.summary_calls).toBe(1);
147
+ expect(row.summary_input_tokens).toBe(880);
148
+ expect(row.summary_output_tokens).toBe(120);
149
+ expect(row.summary_model).toBe("test-model");
150
+ expect(row.summary_failed).toBe(-1);
151
+ expect(row.reason).toBe("auto");
152
+ expect(row.exhausted).toBe(0);
153
+ expect(row.summary_text).toBe("summary text");
154
+ });
155
+
156
+ test("end row maps explicit summaryFailed and omitted optionals", async () => {
157
+ const { writer, requests } = createWriter();
158
+
159
+ await writer.writeEnd("conv-1", {
160
+ ...endEvent,
161
+ compacted: false,
162
+ summaryFailed: true,
163
+ preservedTailMessages: undefined,
164
+ reason: undefined,
165
+ exhausted: true,
166
+ });
167
+
168
+ const row = JSON.parse(requests[1]!.body) as Record<string, unknown>;
169
+ expect(row.compacted).toBe(0);
170
+ expect(row.summary_failed).toBe(1);
171
+ expect(row.preserved_tail_messages).toBe(-1);
172
+ expect(row.reason).toBe("");
173
+ expect(row.exhausted).toBe(1);
174
+ });
175
+
176
+ test("failed table creation retries on the next write", async () => {
177
+ let calls = 0;
178
+ const requests: CapturedRequest[] = [];
179
+ const { writer } = createWriter({
180
+ fetchImpl: async (url: string, init?: RequestInit) => {
181
+ calls++;
182
+ requests.push({ url: new URL(url), body: String(init?.body ?? "") });
183
+ if (calls === 1) return new Response("boom", { status: 500 });
184
+ return new Response("", { status: 200 });
185
+ },
186
+ });
187
+
188
+ await expect(writer.writeStart("conv-1", startEvent)).rejects.toThrow(
189
+ "ClickHouse compaction-log request failed",
190
+ );
191
+
192
+ await writer.writeStart("conv-1", startEvent);
193
+ const ddl = requests.filter((r) => r.body.includes("CREATE TABLE"));
194
+ expect(ddl.length).toBe(2);
195
+ });
196
+
197
+ test("write failures surface the HTTP status and body", async () => {
198
+ let calls = 0;
199
+ const { writer } = createWriter({
200
+ fetchImpl: async () => {
201
+ calls++;
202
+ if (calls === 1) return new Response("", { status: 200 });
203
+ return new Response("table is read-only", { status: 403 });
204
+ },
205
+ });
206
+
207
+ await expect(writer.writeEnd("conv-1", endEvent)).rejects.toThrow(
208
+ /HTTP 403.*table is read-only/,
209
+ );
210
+ });
211
+
212
+ test("long summary text is truncated", async () => {
213
+ const { writer, requests } = createWriter();
214
+
215
+ await writer.writeEnd("conv-1", {
216
+ ...endEvent,
217
+ summaryText: "x".repeat(10_000),
218
+ });
219
+
220
+ const row = JSON.parse(requests[1]!.body) as Record<string, unknown>;
221
+ expect((row.summary_text as string).length).toBe(4000);
222
+ });
223
+ });
224
+
225
+ // ---------------------------------------------------------------------
226
+ // Read path — getEventsBetween
227
+ // ---------------------------------------------------------------------
228
+
229
+ function startRow(overrides: Record<string, unknown> = {}) {
230
+ return {
231
+ compaction_id: "comp-1",
232
+ request_id: "req-1",
233
+ phase: "start",
234
+ trigger: "budget",
235
+ started_at: 1000,
236
+ finished_at: 0,
237
+ duration_ms: -1,
238
+ pre_message_count: 12,
239
+ result_message_count: -1,
240
+ compacted: 0,
241
+ previous_estimated_input_tokens: -1,
242
+ estimated_input_tokens: -1,
243
+ max_input_tokens: -1,
244
+ threshold_tokens: -1,
245
+ compacted_messages: -1,
246
+ compacted_persisted_messages: -1,
247
+ preserved_tail_messages: -1,
248
+ summary_calls: -1,
249
+ summary_input_tokens: -1,
250
+ summary_output_tokens: -1,
251
+ summary_model: "",
252
+ summary_failed: -1,
253
+ reason: "",
254
+ exhausted: 0,
255
+ injection_mode: "",
256
+ auto_compress_applied: 0,
257
+ summary_text: "",
258
+ ...overrides,
259
+ };
260
+ }
261
+
262
+ function endRow(overrides: Record<string, unknown> = {}) {
263
+ return startRow({
264
+ phase: "end",
265
+ finished_at: 1500,
266
+ duration_ms: 500,
267
+ pre_message_count: -1,
268
+ result_message_count: 4,
269
+ compacted: 1,
270
+ previous_estimated_input_tokens: 900,
271
+ estimated_input_tokens: 300,
272
+ max_input_tokens: 1000,
273
+ threshold_tokens: 850,
274
+ compacted_messages: 10,
275
+ compacted_persisted_messages: 8,
276
+ preserved_tail_messages: 2,
277
+ summary_calls: 1,
278
+ summary_input_tokens: 880,
279
+ summary_output_tokens: 120,
280
+ summary_model: "test-model",
281
+ summary_failed: 0,
282
+ reason: "auto",
283
+ summary_text: "summary text",
284
+ ...overrides,
285
+ });
286
+ }
287
+
288
+ function createReader(rows: Record<string, unknown>[]): {
289
+ writer: ClickHouseCompactionLogStore;
290
+ requests: CapturedRequest[];
291
+ } {
292
+ const requests: CapturedRequest[] = [];
293
+ const { writer } = createWriter({
294
+ fetchImpl: async (url: string, init?: RequestInit) => {
295
+ requests.push({ url: new URL(url), body: String(init?.body ?? "") });
296
+ const body =
297
+ rows.length === 0
298
+ ? ""
299
+ : rows.map((r) => JSON.stringify(r)).join("\n") + "\n";
300
+ return new Response(body, { status: 200 });
301
+ },
302
+ });
303
+ return { writer, requests };
304
+ }
305
+
306
+ describe("ClickHouseCompactionLogStore — getEventsBetween", () => {
307
+ test("binds the window via param_ URL slots, never string interpolation", async () => {
308
+ const { writer, requests } = createReader([]);
309
+
310
+ await writer.getEventsBetween("conv-1", 1999, 9001);
311
+
312
+ expect(requests.length).toBe(1);
313
+ const req = requests[0]!;
314
+ expect(req.url.searchParams.get("database")).toBe("analytics");
315
+ expect(req.url.searchParams.get("param_assistant_id")).toBe(
316
+ "assistant-123",
317
+ );
318
+ expect(req.url.searchParams.get("param_conversation_id")).toBe("conv-1");
319
+ expect(req.url.searchParams.get("param_after_started_at")).toBe("1999");
320
+ expect(req.url.searchParams.get("param_before_started_at")).toBe("9001");
321
+ expect(req.body).toContain("{assistant_id:String}");
322
+ expect(req.body).toContain(
323
+ "started_at > fromUnixTimestamp64Milli({after_started_at:Int64})",
324
+ );
325
+ expect(req.body).toContain("LIMIT 1 BY compaction_id, phase");
326
+ expect(req.body).toContain("FORMAT JSONEachRow");
327
+ });
328
+
329
+ test("omits the floor predicate when afterStartedAt is null", async () => {
330
+ const { writer, requests } = createReader([]);
331
+
332
+ await writer.getEventsBetween("conv-1", null, 9001);
333
+
334
+ const req = requests[0]!;
335
+ expect(req.url.searchParams.get("param_after_started_at")).toBeNull();
336
+ expect(req.body).not.toContain("after_started_at");
337
+ });
338
+
339
+ test("pairs start and end rows into one completed event", async () => {
340
+ const { writer } = createReader([startRow(), endRow()]);
341
+
342
+ const events = await writer.getEventsBetween("conv-1", null, 9001);
343
+
344
+ expect(events).toHaveLength(1);
345
+ const event = events[0]!;
346
+ expect(event.compactionId).toBe("comp-1");
347
+ expect(event.completed).toBe(true);
348
+ expect(event.trigger).toBe("budget");
349
+ expect(event.startedAt).toBe(1000);
350
+ expect(event.finishedAt).toBe(1500);
351
+ expect(event.durationMs).toBe(500);
352
+ // Start-phase field comes from the start row even though the end
353
+ // row carries the -1 sentinel for it.
354
+ expect(event.preMessageCount).toBe(12);
355
+ expect(event.resultMessageCount).toBe(4);
356
+ expect(event.compacted).toBe(true);
357
+ expect(event.summaryInputTokens).toBe(880);
358
+ expect(event.summaryOutputTokens).toBe(120);
359
+ expect(event.summaryModel).toBe("test-model");
360
+ expect(event.summaryFailed).toBe(false);
361
+ expect(event.reason).toBe("auto");
362
+ expect(event.exhausted).toBe(false);
363
+ expect(event.summaryText).toBe("summary text");
364
+ });
365
+
366
+ test("start-only rows surface as incomplete events with null end fields", async () => {
367
+ const { writer } = createReader([startRow()]);
368
+
369
+ const events = await writer.getEventsBetween("conv-1", null, 9001);
370
+
371
+ expect(events).toHaveLength(1);
372
+ const event = events[0]!;
373
+ expect(event.completed).toBe(false);
374
+ expect(event.preMessageCount).toBe(12);
375
+ expect(event.finishedAt).toBeNull();
376
+ expect(event.durationMs).toBeNull();
377
+ expect(event.compacted).toBeNull();
378
+ expect(event.summaryModel).toBeNull();
379
+ expect(event.summaryFailed).toBeNull();
380
+ expect(event.summaryText).toBeNull();
381
+ });
382
+
383
+ test("maps sentinels back to null and coerces quoted Int64 values", async () => {
384
+ // ClickHouse may serialize Int64 columns as JSON strings.
385
+ const { writer } = createReader([
386
+ endRow({
387
+ compaction_id: "comp-2",
388
+ started_at: "2000",
389
+ duration_ms: "700",
390
+ preserved_tail_messages: -1,
391
+ summary_failed: -1,
392
+ reason: "",
393
+ summary_text: "",
394
+ }),
395
+ ]);
396
+
397
+ const events = await writer.getEventsBetween("conv-1", null, 9001);
398
+
399
+ const event = events[0]!;
400
+ expect(event.startedAt).toBe(2000);
401
+ expect(event.durationMs).toBe(700);
402
+ expect(event.preservedTailMessages).toBeNull();
403
+ expect(event.summaryFailed).toBeNull();
404
+ expect(event.reason).toBeNull();
405
+ expect(event.summaryText).toBeNull();
406
+ // End-only pair: start-phase field is unknown.
407
+ expect(event.preMessageCount).toBeNull();
408
+ });
409
+
410
+ test("sorts paired events by startedAt ascending", async () => {
411
+ const { writer } = createReader([
412
+ startRow({ compaction_id: "comp-late", started_at: 5000 }),
413
+ startRow({ compaction_id: "comp-early", started_at: 1000 }),
414
+ endRow({ compaction_id: "comp-late", started_at: 5000 }),
415
+ ]);
416
+
417
+ const events = await writer.getEventsBetween("conv-1", null, 9001);
418
+
419
+ expect(events.map((e) => e.compactionId)).toEqual([
420
+ "comp-early",
421
+ "comp-late",
422
+ ]);
423
+ });
424
+
425
+ test("empty response body yields an empty event list", async () => {
426
+ const { writer } = createReader([]);
427
+
428
+ const events = await writer.getEventsBetween("conv-1", null, 9001);
429
+
430
+ expect(events).toEqual([]);
431
+ });
432
+ });