@vellumai/assistant 0.8.11 → 0.8.12-staging.1

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 (219) 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 +126 -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-usage-store.test.ts +11 -0
  37. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  38. package/src/__tests__/model-intents.test.ts +1 -1
  39. package/src/__tests__/oauth-cli.test.ts +19 -8
  40. package/src/__tests__/openai-provider.test.ts +34 -0
  41. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  42. package/src/__tests__/recurrence-engine.test.ts +45 -0
  43. package/src/__tests__/schedule-routes.test.ts +34 -0
  44. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  45. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  46. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  47. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  48. package/src/__tests__/subagent-tools.test.ts +1 -0
  49. package/src/__tests__/system-prompt.test.ts +109 -11
  50. package/src/__tests__/tool-error-hook.test.ts +1 -0
  51. package/src/__tests__/tool-result-spool.test.ts +337 -0
  52. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  53. package/src/__tests__/validate-input.test.ts +95 -1
  54. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  55. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  56. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  57. package/src/agent/loop.ts +81 -24
  58. package/src/api/events/usage-progress.ts +28 -0
  59. package/src/api/index.ts +6 -0
  60. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  61. package/src/bundler/app-bundler.ts +25 -42
  62. package/src/calls/call-controller.ts +1 -1
  63. package/src/cli/commands/plugins.ts +248 -15
  64. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  65. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  66. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  67. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  68. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  69. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
  70. package/src/cli/lib/inspect-plugin.ts +252 -0
  71. package/src/cli/lib/install-from-github.ts +214 -21
  72. package/src/cli/lib/list-installed-plugins.ts +17 -6
  73. package/src/cli/lib/plugin-artifact.ts +103 -0
  74. package/src/cli/lib/plugin-details.ts +18 -1
  75. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  76. package/src/cli/lib/upgrade-plugin.ts +219 -0
  77. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  78. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  79. package/src/config/call-site-defaults.ts +13 -2
  80. package/src/config/feature-flag-registry.json +8 -16
  81. package/src/config/loader.ts +52 -59
  82. package/src/config/schema.ts +0 -2
  83. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  84. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  85. package/src/config/schemas/llm.ts +10 -0
  86. package/src/config/schemas/memory-v2.ts +13 -0
  87. package/src/config/schemas/memory-v3.ts +92 -0
  88. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  89. package/src/context/tool-result-spool.ts +104 -0
  90. package/src/credential-execution/feature-gates.ts +0 -1
  91. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  92. package/src/daemon/conversation-error.ts +6 -15
  93. package/src/daemon/conversation.ts +9 -0
  94. package/src/daemon/disk-pressure-policy.ts +0 -1
  95. package/src/daemon/lifecycle.ts +1 -20
  96. package/src/daemon/message-types/conversations.ts +2 -15
  97. package/src/daemon/trust-context.ts +1 -1
  98. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  99. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  100. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  101. package/src/home/home-content-refresh.ts +43 -31
  102. package/src/home/home-greeting-cache.ts +8 -1
  103. package/src/home/home-greeting.ts +13 -9
  104. package/src/home/suggested-prompts.ts +77 -24
  105. package/src/ipc/routes/trust-rules.test.ts +66 -72
  106. package/src/media/image-credentials.ts +2 -2
  107. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  108. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  109. package/src/memory/conversation-attention-store.ts +1 -0
  110. package/src/memory/conversation-bootstrap.ts +18 -9
  111. package/src/memory/conversation-crud.ts +12 -2
  112. package/src/memory/conversation-title-service.ts +53 -9
  113. package/src/memory/delivery-channels.ts +0 -69
  114. package/src/memory/graph/extraction-job.ts +0 -15
  115. package/src/memory/guardian-action-store.ts +1 -376
  116. package/src/memory/llm-usage-store.ts +5 -1
  117. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  119. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  120. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  121. package/src/memory/v2/consolidation-job.ts +93 -9
  122. package/src/memory/v2/injection.ts +53 -0
  123. package/src/memory/v2/prompts/consolidation.ts +1 -0
  124. package/src/memory/v2/static-context.ts +13 -1
  125. package/src/memory/v2/sweep-job.ts +1 -1
  126. package/src/memory/v2/types.ts +5 -0
  127. package/src/plugin-api/types.ts +7 -0
  128. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  129. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  130. package/src/plugins/defaults/index.ts +25 -0
  131. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  132. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  133. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  134. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  135. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  136. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  137. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  138. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  139. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  140. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  141. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  142. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  143. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  144. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  145. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  146. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  147. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  148. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  149. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  150. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  151. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  152. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  153. package/src/prompts/cache-boundary.ts +17 -0
  154. package/src/prompts/sections.ts +50 -17
  155. package/src/prompts/system-prompt.ts +12 -4
  156. package/src/prompts/templates/system-sections.ts +22 -0
  157. package/src/providers/anthropic/client.ts +74 -28
  158. package/src/providers/gemini/client.ts +5 -1
  159. package/src/providers/minimax/client.ts +9 -0
  160. package/src/providers/model-intents.ts +2 -2
  161. package/src/providers/openai/chat-completions-provider.ts +2 -1
  162. package/src/providers/openai/responses-provider.ts +5 -1
  163. package/src/providers/retry.ts +8 -0
  164. package/src/providers/types.ts +11 -0
  165. package/src/runtime/AGENTS.md +6 -0
  166. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  167. package/src/runtime/agent-wake.ts +5 -5
  168. package/src/runtime/background-job-runner.ts +2 -2
  169. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  170. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  171. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  172. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  173. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  174. package/src/runtime/pre-first-message-gate.ts +1 -1
  175. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  176. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  177. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  178. package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
  179. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  180. package/src/runtime/routes/btw-routes.ts +0 -14
  181. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  182. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  183. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  184. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  185. package/src/runtime/routes/home-feed-routes.ts +10 -0
  186. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  187. package/src/runtime/routes/identity-routes.ts +76 -20
  188. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  189. package/src/runtime/routes/plugins-routes.ts +21 -0
  190. package/src/runtime/routes/schedule-routes.ts +19 -2
  191. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  192. package/src/schedule/recurrence-engine.ts +34 -0
  193. package/src/schedule/scheduler.ts +1 -0
  194. package/src/skills/validate-input.ts +41 -1
  195. package/src/subagent/types.ts +26 -1
  196. package/src/telemetry/types.ts +15 -1
  197. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  198. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  199. package/src/tools/apps/executors.ts +1 -1
  200. package/src/tools/skills/skill-tool-factory.ts +19 -8
  201. package/src/usage/types.ts +8 -1
  202. package/src/util/platform.ts +16 -0
  203. package/src/watcher/engine.ts +1 -0
  204. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  205. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  206. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  207. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  210. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  211. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  212. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  213. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  214. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  215. package/src/config/schemas/updates.ts +0 -14
  216. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  217. package/src/memory/conversation-starters-cadence.ts +0 -78
  218. package/src/prompts/update-bulletin-job.ts +0 -180
  219. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -322,7 +322,10 @@ export function commitImport(options: ImportCommitOptions): ImportCommitResult {
322
322
  const diskPath = pathResolver.resolve(fileEntry.path);
323
323
 
324
324
  if (!diskPath) {
325
- // Unknown archive path — skip it
325
+ // Unknown archive path — skip it. Retired-feature paths (see
326
+ // `policy.RETIRED_ARCHIVE_PATHS`) are expected in legacy bundles and
327
+ // skip silently so the import report matches preflight; anything else
328
+ // unresolvable earns a warning.
326
329
  importedFiles.push({
327
330
  path: fileEntry.path,
328
331
  disk_path: "",
@@ -331,9 +334,11 @@ export function commitImport(options: ImportCommitOptions): ImportCommitResult {
331
334
  sha256: fileEntry.sha256,
332
335
  backup_path: null,
333
336
  });
334
- warnings.push(
335
- `Skipped "${fileEntry.path}": no known disk target for this archive path`,
336
- );
337
+ if (!policy.isRetiredArchivePath(fileEntry.path)) {
338
+ warnings.push(
339
+ `Skipped "${fileEntry.path}": no known disk target for this archive path`,
340
+ );
341
+ }
337
342
  continue;
338
343
  }
339
344
 
@@ -695,9 +695,14 @@ export async function streamCommitImport(
695
695
  sha256: expectedEntry.sha256,
696
696
  backup_path: null,
697
697
  });
698
- warnings.push(
699
- `Skipped "${archivePath}": no known disk target for this archive path`,
700
- );
698
+ // Retired-feature paths (see `policy.RETIRED_ARCHIVE_PATHS`) are
699
+ // expected in legacy bundles and skip silently so the import report
700
+ // matches preflight; anything else unresolvable earns a warning.
701
+ if (!policy.isRetiredArchivePath(archivePath)) {
702
+ warnings.push(
703
+ `Skipped "${archivePath}": no known disk target for this archive path`,
704
+ );
705
+ }
701
706
  seen.add(archivePath);
702
707
  onProgress?.({
703
708
  archivePath,
@@ -25,7 +25,7 @@
25
25
  * interaction resumes, so re-querying on miss is the correct behavior.)
26
26
  *
27
27
  * Callers should treat the gate as advisory + defense-in-depth: prefer to
28
- * skip at the service level (heartbeat, update-bulletin, etc.) so no run
28
+ * skip at the service level (heartbeat, etc.) so no run
29
29
  * row / conversation row is created at all, and rely on the gate inside
30
30
  * `runBackgroundJob` as the universal backstop.
31
31
  */
@@ -34,6 +34,7 @@ mock.module("../../../config/loader.js", () => ({
34
34
  // Source + conversation-crud module mocks
35
35
  // ---------------------------------------------------------------------
36
36
 
37
+ import type { CompactionLogEvent } from "../../../memory/compaction-log-store-clickhouse.js";
37
38
  import type { LogRow } from "../../../memory/llm-request-log-store.js";
38
39
 
39
40
  interface FakeSourceState {
@@ -41,6 +42,9 @@ interface FakeSourceState {
41
42
  selectedCall: LogRow | null;
42
43
  turnBounds: { startTime: number; endTime: number } | null;
43
44
  compactionLogs: LogRow[];
45
+ /** null = compactionLogs destination not configured (legacy-only). */
46
+ compactionStoreEvents: CompactionLogEvent[] | null;
47
+ compactionStoreError: Error | null;
44
48
  }
45
49
 
46
50
  const state: FakeSourceState = {
@@ -48,6 +52,8 @@ const state: FakeSourceState = {
48
52
  selectedCall: null,
49
53
  turnBounds: null,
50
54
  compactionLogs: [],
55
+ compactionStoreEvents: null,
56
+ compactionStoreError: null,
51
57
  };
52
58
 
53
59
  // Records the inputs the handler passed to its collaborators so tests
@@ -63,8 +69,34 @@ const sourceCalls = {
63
69
  afterCreatedAt: number | null;
64
70
  beforeCreatedAt: number;
65
71
  }>,
72
+ getEventsBetweenArgs: [] as Array<{
73
+ conversationId: string;
74
+ afterStartedAt: number | null;
75
+ beforeStartedAt: number;
76
+ }>,
66
77
  };
67
78
 
79
+ mock.module("../../../memory/compaction-log-store-clickhouse.js", () => ({
80
+ getCompactionLogStore: () =>
81
+ state.compactionStoreEvents === null && state.compactionStoreError === null
82
+ ? null
83
+ : {
84
+ getEventsBetween: async (
85
+ conversationId: string,
86
+ afterStartedAt: number | null,
87
+ beforeStartedAt: number,
88
+ ) => {
89
+ sourceCalls.getEventsBetweenArgs.push({
90
+ conversationId,
91
+ afterStartedAt,
92
+ beforeStartedAt,
93
+ });
94
+ if (state.compactionStoreError) throw state.compactionStoreError;
95
+ return state.compactionStoreEvents ?? [];
96
+ },
97
+ },
98
+ }));
99
+
68
100
  mock.module("../../../memory/conversation-crud.js", () => ({
69
101
  getConversation: (id: string) =>
70
102
  state.conversation && state.conversation.id === id
@@ -105,6 +137,7 @@ mock.module("../../../memory/llm-request-log-source.js", () => ({
105
137
 
106
138
  // Imported AFTER the mocks so the handler picks up the fakes.
107
139
  import {
140
+ projectCompactionLogEventToTrailEvent,
108
141
  projectLogRowToCompactionTrailEvent,
109
142
  ROUTES,
110
143
  } from "../conversation-compaction-routes.js";
@@ -132,14 +165,52 @@ function fakeLogRow(overrides: Partial<LogRow> = {}): LogRow {
132
165
  };
133
166
  }
134
167
 
168
+ function fakeCompactionLogEvent(
169
+ overrides: Partial<CompactionLogEvent> = {},
170
+ ): CompactionLogEvent {
171
+ return {
172
+ compactionId: "comp-1",
173
+ requestId: "req-1",
174
+ trigger: "budget",
175
+ startedAt: 3000,
176
+ finishedAt: 3500,
177
+ durationMs: 500,
178
+ preMessageCount: 12,
179
+ resultMessageCount: 4,
180
+ compacted: true,
181
+ previousEstimatedInputTokens: 900,
182
+ estimatedInputTokens: 300,
183
+ maxInputTokens: 1000,
184
+ thresholdTokens: 850,
185
+ compactedMessages: 10,
186
+ compactedPersistedMessages: 8,
187
+ preservedTailMessages: 2,
188
+ summaryCalls: 1,
189
+ summaryInputTokens: 880,
190
+ summaryOutputTokens: 120,
191
+ summaryModel: "test-model",
192
+ summaryFailed: false,
193
+ reason: "auto",
194
+ exhausted: false,
195
+ injectionMode: null,
196
+ autoCompressApplied: false,
197
+ summaryText: "summary text",
198
+ completed: true,
199
+ ...overrides,
200
+ };
201
+ }
202
+
135
203
  beforeEach(() => {
136
204
  state.conversation = null;
137
205
  state.selectedCall = null;
138
206
  state.turnBounds = null;
139
207
  state.compactionLogs = [];
208
+ state.compactionStoreEvents = null;
209
+ state.compactionStoreError = null;
140
210
  sourceCalls.getRequestLogByIdArgs.length = 0;
141
211
  sourceCalls.getTurnTimeBoundsArgs.length = 0;
142
212
  sourceCalls.getCompactionLogsBetweenArgs.length = 0;
213
+ sourceCalls.getEventsBetweenArgs.length = 0;
143
214
  });
144
215
 
145
216
  // ---------------------------------------------------------------------
@@ -353,10 +424,180 @@ describe("handleGetCompactionTrail — happy path", () => {
353
424
  });
354
425
  });
355
426
 
427
+ // ---------------------------------------------------------------------
428
+ // Handler — compaction-log store path
429
+ // ---------------------------------------------------------------------
430
+
431
+ describe("handleGetCompactionTrail — compaction log store", () => {
432
+ test("serves the trail from the store and skips the legacy projection", async () => {
433
+ state.conversation = { id: "conv-1" };
434
+ state.selectedCall = fakeLogRow({
435
+ id: "call-1",
436
+ conversationId: "conv-1",
437
+ createdAt: 5000,
438
+ });
439
+ state.turnBounds = { startTime: 2000, endTime: 9000 };
440
+ state.compactionStoreEvents = [fakeCompactionLogEvent()];
441
+
442
+ const result = await handler({
443
+ pathParams: { id: "conv-1" },
444
+ queryParams: { callId: "call-1" },
445
+ });
446
+
447
+ // Same ±1ms-shifted turn window as the legacy path.
448
+ expect(sourceCalls.getEventsBetweenArgs).toEqual([
449
+ {
450
+ conversationId: "conv-1",
451
+ afterStartedAt: 1999,
452
+ beforeStartedAt: 9001,
453
+ },
454
+ ]);
455
+ expect(sourceCalls.getCompactionLogsBetweenArgs).toEqual([]);
456
+ expect(result.events).toEqual([
457
+ {
458
+ id: "comp-1",
459
+ createdAt: 3000,
460
+ model: "test-model",
461
+ provider: null,
462
+ inputTokens: 880,
463
+ outputTokens: 120,
464
+ durationMs: 500,
465
+ responsePreview: "summary text",
466
+ requestMessageCount: 12,
467
+ stopReason: "auto",
468
+ estimatedCostUsd: null,
469
+ },
470
+ ]);
471
+ });
472
+
473
+ test("falls back to the legacy projection when the store has no rows for the window", async () => {
474
+ state.conversation = { id: "conv-1" };
475
+ state.selectedCall = fakeLogRow({
476
+ id: "call-1",
477
+ conversationId: "conv-1",
478
+ createdAt: 5000,
479
+ });
480
+ state.turnBounds = { startTime: 2000, endTime: 9000 };
481
+ state.compactionStoreEvents = [];
482
+ state.compactionLogs = [
483
+ fakeLogRow({ id: "compaction-legacy", conversationId: "conv-1" }),
484
+ ];
485
+
486
+ const result = await handler({
487
+ pathParams: { id: "conv-1" },
488
+ queryParams: { callId: "call-1" },
489
+ });
490
+
491
+ expect(sourceCalls.getEventsBetweenArgs).toHaveLength(1);
492
+ expect(sourceCalls.getCompactionLogsBetweenArgs).toHaveLength(1);
493
+ expect(result.events).toHaveLength(1);
494
+ expect((result.events[0] as { id: string }).id).toBe("compaction-legacy");
495
+ });
496
+
497
+ test("falls back to the legacy projection when the store read throws", async () => {
498
+ state.conversation = { id: "conv-1" };
499
+ state.selectedCall = fakeLogRow({
500
+ id: "call-1",
501
+ conversationId: "conv-1",
502
+ createdAt: 5000,
503
+ });
504
+ state.turnBounds = { startTime: 2000, endTime: 9000 };
505
+ state.compactionStoreError = new Error("clickhouse unreachable");
506
+ state.compactionLogs = [
507
+ fakeLogRow({ id: "compaction-legacy", conversationId: "conv-1" }),
508
+ ];
509
+
510
+ const result = await handler({
511
+ pathParams: { id: "conv-1" },
512
+ queryParams: { callId: "call-1" },
513
+ });
514
+
515
+ expect(sourceCalls.getCompactionLogsBetweenArgs).toHaveLength(1);
516
+ expect(result.events).toHaveLength(1);
517
+ expect((result.events[0] as { id: string }).id).toBe("compaction-legacy");
518
+ });
519
+
520
+ test("falls back to the legacy projection when any event is missing its end row", async () => {
521
+ state.conversation = { id: "conv-1" };
522
+ state.selectedCall = fakeLogRow({
523
+ id: "call-1",
524
+ conversationId: "conv-1",
525
+ createdAt: 5000,
526
+ });
527
+ state.turnBounds = { startTime: 2000, endTime: 9000 };
528
+ state.compactionStoreEvents = [
529
+ fakeCompactionLogEvent(),
530
+ fakeCompactionLogEvent({
531
+ compactionId: "comp-2",
532
+ finishedAt: null,
533
+ durationMs: null,
534
+ completed: false,
535
+ }),
536
+ ];
537
+ state.compactionLogs = [
538
+ fakeLogRow({ id: "compaction-legacy", conversationId: "conv-1" }),
539
+ ];
540
+
541
+ const result = await handler({
542
+ pathParams: { id: "conv-1" },
543
+ queryParams: { callId: "call-1" },
544
+ });
545
+
546
+ expect(sourceCalls.getEventsBetweenArgs).toHaveLength(1);
547
+ expect(sourceCalls.getCompactionLogsBetweenArgs).toHaveLength(1);
548
+ expect(result.events).toHaveLength(1);
549
+ expect((result.events[0] as { id: string }).id).toBe("compaction-legacy");
550
+ });
551
+ });
552
+
356
553
  // ---------------------------------------------------------------------
357
554
  // Projection unit tests
358
555
  // ---------------------------------------------------------------------
359
556
 
557
+ describe("projectCompactionLogEventToTrailEvent", () => {
558
+ test("maps a completed event onto the wire shape", () => {
559
+ const event = projectCompactionLogEventToTrailEvent(
560
+ fakeCompactionLogEvent(),
561
+ );
562
+ expect(event).toEqual({
563
+ id: "comp-1",
564
+ createdAt: 3000,
565
+ model: "test-model",
566
+ provider: null,
567
+ inputTokens: 880,
568
+ outputTokens: 120,
569
+ durationMs: 500,
570
+ responsePreview: "summary text",
571
+ requestMessageCount: 12,
572
+ stopReason: "auto",
573
+ estimatedCostUsd: null,
574
+ });
575
+ });
576
+
577
+ test("maps an incomplete (start-only) event with nulls for end-phase fields", () => {
578
+ const event = projectCompactionLogEventToTrailEvent(
579
+ fakeCompactionLogEvent({
580
+ completed: false,
581
+ finishedAt: null,
582
+ durationMs: null,
583
+ summaryInputTokens: null,
584
+ summaryOutputTokens: null,
585
+ summaryModel: null,
586
+ reason: null,
587
+ summaryText: null,
588
+ }),
589
+ );
590
+ expect(event.durationMs).toBeNull();
591
+ expect(event.model).toBeNull();
592
+ expect(event.inputTokens).toBeNull();
593
+ expect(event.outputTokens).toBeNull();
594
+ expect(event.responsePreview).toBeNull();
595
+ expect(event.stopReason).toBeNull();
596
+ // The start row always carries the pre-compaction message count.
597
+ expect(event.requestMessageCount).toBe(12);
598
+ });
599
+ });
600
+
360
601
  describe("projectLogRowToCompactionTrailEvent", () => {
361
602
  test("returns null for every summary-derived field when payloads are empty", () => {
362
603
  const event = projectLogRowToCompactionTrailEvent(fakeLogRow());