@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
@@ -96,9 +96,14 @@ mock.module("../util/logger.js", () => ({
96
96
  }),
97
97
  }));
98
98
 
99
- // Mock conversation title service
99
+ // Mock conversation title service. `deriveDeterministicTitle` mirrors the
100
+ // real implementation's systemHint passthrough so bootstrap-created
101
+ // conversations carry their job hint as the title.
100
102
  mock.module("../memory/conversation-title-service.js", () => ({
101
103
  GENERATING_TITLE: "Generating title...",
104
+ AUTO_TITLE_DETERMINISTIC: 2,
105
+ deriveDeterministicTitle: (context: { systemHint?: string }) =>
106
+ context.systemHint ?? "Untitled Conversation",
102
107
  queueGenerateConversationTitle: () => {},
103
108
  }));
104
109
 
@@ -230,12 +235,12 @@ describe("FilingService", () => {
230
235
  );
231
236
  });
232
237
 
233
- test("creates background conversation with generating title placeholder", async () => {
238
+ test("creates background conversation with the deterministic filing title", async () => {
234
239
  const service = createService();
235
240
  await service.runOnce();
236
241
 
237
242
  expect(createdConversations).toHaveLength(1);
238
- expect(createdConversations[0].title).toBe("Generating title...");
243
+ expect(createdConversations[0].title).toBe("Knowledge base filing");
239
244
  expect(createdConversations[0].conversationType).toBe("background");
240
245
  // Confirms FilingService routes through runBackgroundJob:
241
246
  // source="filing" + runner-default groupId="system:background".
@@ -17,13 +17,8 @@ import {
17
17
  cancelGuardianActionRequest,
18
18
  createGuardianActionDelivery,
19
19
  createGuardianActionRequest,
20
- expireGuardianActionRequest,
21
20
  getDeliveriesByRequestId,
22
- getExpiredDeliveriesByConversation,
23
- getFollowupDeliveriesByConversation,
24
21
  getGuardianActionRequest,
25
- getPendingDeliveriesByConversation,
26
- startFollowupFromExpiredRequest,
27
22
  updateDeliveryStatus,
28
23
  } from "../memory/guardian-action-store.js";
29
24
  import { conversations } from "../memory/schema.js";
@@ -58,168 +53,6 @@ describe("guardian-action-store", () => {
58
53
  resetTables();
59
54
  });
60
55
 
61
- // ── Helper to create a pending request+delivery targeting a conversation ──
62
- function createPendingRequestWithDelivery(
63
- convId: string,
64
- deliveryConvId: string,
65
- ) {
66
- ensureConversation(convId);
67
- const session = createCallSession({
68
- conversationId: convId,
69
- provider: "twilio",
70
- fromNumber: "+15550001111",
71
- toNumber: "+15550002222",
72
- });
73
- const pq = createPendingQuestion(session.id, `Question for ${convId}`);
74
- const request = createGuardianActionRequest({
75
- kind: "ask_guardian",
76
- sourceChannel: "phone",
77
- sourceConversationId: convId,
78
- callSessionId: session.id,
79
- pendingQuestionId: pq.id,
80
- questionText: pq.questionText,
81
- expiresAt: Date.now() + 60_000,
82
- });
83
- const delivery = createGuardianActionDelivery({
84
- requestId: request.id,
85
- destinationChannel: "vellum",
86
- destinationConversationId: deliveryConvId,
87
- });
88
- updateDeliveryStatus(delivery.id, "sent");
89
- return { request, delivery };
90
- }
91
-
92
- // ── getPendingDeliveriesByConversation ──────────────────────────────
93
-
94
- test("getPendingDeliveriesByConversation returns all pending deliveries for a conversation", () => {
95
- const sharedConvId = "shared-pending-conv";
96
- ensureConversation(sharedConvId);
97
-
98
- const { request: req1 } = createPendingRequestWithDelivery(
99
- "source-conv-p1",
100
- sharedConvId,
101
- );
102
- const { request: req2 } = createPendingRequestWithDelivery(
103
- "source-conv-p2",
104
- sharedConvId,
105
- );
106
-
107
- const deliveries = getPendingDeliveriesByConversation(sharedConvId);
108
- expect(deliveries).toHaveLength(2);
109
-
110
- const requestIds = deliveries.map((d) => d.requestId);
111
- expect(requestIds).toContain(req1.id);
112
- expect(requestIds).toContain(req2.id);
113
- });
114
-
115
- test("getPendingDeliveriesByConversation returns single delivery (fast path preserved)", () => {
116
- const convId = "single-pending-conv";
117
- ensureConversation(convId);
118
-
119
- const { request } = createPendingRequestWithDelivery(
120
- "source-conv-single-p",
121
- convId,
122
- );
123
-
124
- const deliveries = getPendingDeliveriesByConversation(convId);
125
- expect(deliveries).toHaveLength(1);
126
- expect(deliveries[0].requestId).toBe(request.id);
127
- });
128
-
129
- test("getPendingDeliveriesByConversation returns empty for non-matching conversation", () => {
130
- ensureConversation("other-conv");
131
- createPendingRequestWithDelivery("source-conv-no-match", "other-conv");
132
-
133
- const deliveries = getPendingDeliveriesByConversation("nonexistent-conv");
134
- expect(deliveries).toHaveLength(0);
135
- });
136
-
137
- // ── getExpiredDeliveriesByConversation ──────────────────────────────
138
-
139
- test("getExpiredDeliveriesByConversation returns all expired deliveries for a conversation", () => {
140
- const sharedConvId = "shared-expired-conv";
141
- ensureConversation(sharedConvId);
142
-
143
- const { request: req1 } = createPendingRequestWithDelivery(
144
- "source-conv-e1",
145
- sharedConvId,
146
- );
147
- const { request: req2 } = createPendingRequestWithDelivery(
148
- "source-conv-e2",
149
- sharedConvId,
150
- );
151
-
152
- expireGuardianActionRequest(req1.id, "sweep_timeout");
153
- expireGuardianActionRequest(req2.id, "sweep_timeout");
154
-
155
- const deliveries = getExpiredDeliveriesByConversation(sharedConvId);
156
- expect(deliveries).toHaveLength(2);
157
-
158
- const requestIds = deliveries.map((d) => d.requestId);
159
- expect(requestIds).toContain(req1.id);
160
- expect(requestIds).toContain(req2.id);
161
- });
162
-
163
- test("getExpiredDeliveriesByConversation excludes deliveries with followup already started", () => {
164
- const convId = "expired-with-followup-conv";
165
- ensureConversation(convId);
166
-
167
- const { request: req1 } = createPendingRequestWithDelivery(
168
- "source-conv-ef1",
169
- convId,
170
- );
171
- const { request: req2 } = createPendingRequestWithDelivery(
172
- "source-conv-ef2",
173
- convId,
174
- );
175
-
176
- expireGuardianActionRequest(req1.id, "sweep_timeout");
177
- expireGuardianActionRequest(req2.id, "sweep_timeout");
178
-
179
- // Start followup on req1 — only req2 should remain in the expired query
180
- startFollowupFromExpiredRequest(req1.id, "late answer");
181
-
182
- const deliveries = getExpiredDeliveriesByConversation(convId);
183
- expect(deliveries).toHaveLength(1);
184
- expect(deliveries[0].requestId).toBe(req2.id);
185
- });
186
-
187
- // ── getFollowupDeliveriesByConversation ─────────────────────────────
188
-
189
- test("getFollowupDeliveriesByConversation returns all awaiting_guardian_choice deliveries", () => {
190
- const convId = "shared-followup-conv";
191
- ensureConversation(convId);
192
-
193
- const { request: req1 } = createPendingRequestWithDelivery(
194
- "source-conv-f1",
195
- convId,
196
- );
197
- const { request: req2 } = createPendingRequestWithDelivery(
198
- "source-conv-f2",
199
- convId,
200
- );
201
-
202
- expireGuardianActionRequest(req1.id, "sweep_timeout");
203
- expireGuardianActionRequest(req2.id, "sweep_timeout");
204
-
205
- startFollowupFromExpiredRequest(req1.id, "late answer 1");
206
- startFollowupFromExpiredRequest(req2.id, "late answer 2");
207
-
208
- const deliveries = getFollowupDeliveriesByConversation(convId);
209
- expect(deliveries).toHaveLength(2);
210
-
211
- const requestIds = deliveries.map((d) => d.requestId);
212
- expect(requestIds).toContain(req1.id);
213
- expect(requestIds).toContain(req2.id);
214
- });
215
-
216
- test("getFollowupDeliveriesByConversation returns empty for non-matching conversation", () => {
217
- const deliveries = getFollowupDeliveriesByConversation("nonexistent-conv");
218
- expect(deliveries).toHaveLength(0);
219
- });
220
-
221
- // ── cancelGuardianActionRequest ─────────────────────────────────────
222
-
223
56
  test("cancelGuardianActionRequest cancels both pending and sent deliveries", () => {
224
57
  const conversationId = "conv-guardian-cancel";
225
58
  ensureConversation(conversationId);
@@ -65,7 +65,7 @@ mock.module("../config/loader.js", () => ({
65
65
  saveRawConfig: () => {},
66
66
  getNestedValue: () => undefined,
67
67
  setNestedValue: () => {},
68
- _appendQuarantineBulletin: () => {},
68
+ _writeQuarantineNotice: () => {},
69
69
  }));
70
70
 
71
71
  mock.module("../config/skill-state.js", () => ({
@@ -37,7 +37,7 @@ mock.module("../config/loader.js", () => ({
37
37
  getNestedValue: () => undefined,
38
38
  setNestedValue: () => {},
39
39
  API_KEY_PROVIDERS: [],
40
- _appendQuarantineBulletin: () => {},
40
+ _writeQuarantineNotice: () => {},
41
41
  invalidateConfigCache: () => {},
42
42
  }));
43
43
 
@@ -102,6 +102,9 @@ mock.module("../prompts/persona-resolver.js", () => ({
102
102
 
103
103
  mock.module("../memory/conversation-title-service.js", () => ({
104
104
  GENERATING_TITLE: "Generating title...",
105
+ AUTO_TITLE_DETERMINISTIC: 2,
106
+ deriveDeterministicTitle: (context: { systemHint?: string }) =>
107
+ context.systemHint ?? "Untitled Conversation",
105
108
  queueGenerateConversationTitle: () => {},
106
109
  }));
107
110
 
@@ -250,6 +250,9 @@ mock.module("../notifications/emit-signal.js", () => ({
250
250
  // Mock conversation title service
251
251
  mock.module("../memory/conversation-title-service.js", () => ({
252
252
  GENERATING_TITLE: "Generating title...",
253
+ AUTO_TITLE_DETERMINISTIC: 2,
254
+ deriveDeterministicTitle: (context: { systemHint?: string }) =>
255
+ context.systemHint ?? "Untitled Conversation",
253
256
  queueGenerateConversationTitle: () => {},
254
257
  }));
255
258
 
@@ -497,12 +500,12 @@ describe("HeartbeatService", () => {
497
500
  expect(processMessageCalls[0].content).toContain("Check in with yourself");
498
501
  });
499
502
 
500
- test("creates background conversation with generating title placeholder", async () => {
503
+ test("creates background conversation with the deterministic heartbeat title", async () => {
501
504
  const service = createService();
502
505
  await service.runOnce();
503
506
 
504
507
  expect(createdConversations).toHaveLength(1);
505
- expect(createdConversations[0].title).toBe("Generating title...");
508
+ expect(createdConversations[0].title).toBe("Heartbeat");
506
509
  expect(createdConversations[0].conversationType).toBe("background");
507
510
  });
508
511
 
@@ -119,19 +119,26 @@ describe("identity intro cache", () => {
119
119
 
120
120
  setCachedIntro(["Hello!"]);
121
121
 
122
- const fortyNineHoursAgo = String(Date.now() - 49 * 60 * 60 * 1000);
123
- checkpointStore.set("identity:intro:cached_at", fortyNineHoursAgo);
122
+ const fourHoursAndOneMinuteAgo = String(
123
+ Date.now() - (4 * 60 + 1) * 60 * 1000,
124
+ );
125
+ checkpointStore.set("identity:intro:cached_at", fourHoursAndOneMinuteAgo);
124
126
 
125
127
  expect(getCachedIntro()).toBeNull();
126
128
  });
127
129
 
128
- test("returns cached greetings when within TTL (47 hours ago)", () => {
130
+ test("returns cached greetings when within TTL (3 hours 59 minutes ago)", () => {
129
131
  workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
130
132
 
131
133
  setCachedIntro(["Hello!"]);
132
134
 
133
- const fortySevenHoursAgo = String(Date.now() - 47 * 60 * 60 * 1000);
134
- checkpointStore.set("identity:intro:cached_at", fortySevenHoursAgo);
135
+ const threeHoursAndFiftyNineMinutesAgo = String(
136
+ Date.now() - (3 * 60 + 59) * 60 * 1000,
137
+ );
138
+ checkpointStore.set(
139
+ "identity:intro:cached_at",
140
+ threeHoursAndFiftyNineMinutesAgo,
141
+ );
135
142
 
136
143
  const cached = getCachedIntro();
137
144
  expect(cached).not.toBeNull();
@@ -73,13 +73,6 @@ mock.module("../runtime/btw-sidechain.js", () => ({
73
73
  }),
74
74
  }));
75
75
 
76
- const assistantFeatureFlags: Record<string, boolean> = {};
77
-
78
- mock.module("../config/assistant-feature-flags.js", () => ({
79
- isAssistantFeatureFlagEnabled: (key: string) =>
80
- assistantFeatureFlags[key] ?? false,
81
- }));
82
-
83
76
  import {
84
77
  handleDetailedHealth,
85
78
  handleReadyz,
@@ -206,9 +199,6 @@ beforeEach(() => {
206
199
  sidechainCalls.length = 0;
207
200
  sidechainText = "";
208
201
  sidechainResultPromise = null;
209
- for (const key of Object.keys(assistantFeatureFlags)) {
210
- delete assistantFeatureFlags[key];
211
- }
212
202
  });
213
203
 
214
204
  afterEach(() => {
@@ -576,53 +566,7 @@ describe("identity routes — createdAt selection", () => {
576
566
  });
577
567
 
578
568
  describe("identity routes — intro greetings", () => {
579
- test("returns static fallback and does not generate when the dynamic greetings flag is off", async () => {
580
- const workspaceDir = getWorkspaceDir();
581
- writeFileSync(
582
- join(workspaceDir, "IDENTITY.md"),
583
- "# Identity\n\n- **Name:** Example Assistant\n",
584
- "utf-8",
585
- );
586
- writeFileSync(
587
- join(workspaceDir, "SOUL.md"),
588
- "# Soul\n\nNo explicit greetings section here.\n",
589
- "utf-8",
590
- );
591
-
592
- const route = ROUTES.find(
593
- (candidate) => candidate.operationId === "identity_intro",
594
- );
595
- expect(route).toBeDefined();
596
-
597
- const body = route!.handler({}) as {
598
- greetings: string[];
599
- text: string;
600
- source: string;
601
- refreshing: boolean;
602
- };
603
-
604
- expect(body).toEqual({
605
- greetings: [
606
- "What are we working on?",
607
- "I'm here whenever you need me.",
608
- "What's on your mind?",
609
- "Ready when you are.",
610
- ],
611
- text: "What are we working on?",
612
- source: "fallback",
613
- refreshing: false,
614
- });
615
-
616
- await Promise.resolve();
617
- await Promise.resolve();
618
-
619
- expect(getConfiguredProviderCalls).toEqual([]);
620
- expect(sidechainCalls).toEqual([]);
621
- });
622
-
623
569
  test("returns fallback immediately, generates personalized greetings in the background, then reuses the cache", async () => {
624
- assistantFeatureFlags["empty-state-dynamic-greetings"] = true;
625
-
626
570
  const workspaceDir = getWorkspaceDir();
627
571
  writeFileSync(
628
572
  join(workspaceDir, "IDENTITY.md"),
@@ -655,7 +599,9 @@ describe("identity routes — intro greetings", () => {
655
599
  );
656
600
  expect(route).toBeDefined();
657
601
 
658
- const body = route!.handler({}) as {
602
+ const body = route!.handler({
603
+ queryParams: { localHour: "8", localMinute: "15" },
604
+ }) as {
659
605
  greetings: string[];
660
606
  text: string;
661
607
  source: string;
@@ -683,6 +629,14 @@ describe("identity routes — intro greetings", () => {
683
629
  expect(sidechainCalls).toHaveLength(1);
684
630
  expect(sidechainCalls[0]?.callSite).toBe("emptyStateGreeting");
685
631
  expect(sidechainCalls[0]?.tools).toEqual([]);
632
+ expect(sidechainCalls[0]?.content).toContain("Generate 5 short");
633
+ expect(sidechainCalls[0]?.content).not.toContain("Current time of day:");
634
+ expect(sidechainCalls[0]?.content).toContain(
635
+ "do not mention the current time",
636
+ );
637
+ expect(sidechainCalls[0]?.content).toMatch(
638
+ /Current user-local time for subtle tone only: morning \(08:15\)\.$/,
639
+ );
686
640
  expect(sidechainCalls[0]?.content).toContain("JSON array");
687
641
  expect(sidechainCalls[0]?.systemPrompt).toContain(
688
642
  "Identity sentinel: chartreuse compass.",
@@ -695,6 +649,9 @@ describe("identity routes — intro greetings", () => {
695
649
  "Charting the next useful thing?",
696
650
  "I brought the compass. Where to?",
697
651
  "Ready to make this lighter.",
652
+ "Morning momentum?",
653
+ "Five options, one good start.",
654
+ "A useful next step?",
698
655
  ]),
699
656
  hadTextDeltas: false,
700
657
  response: { content: [] },
@@ -718,6 +675,8 @@ describe("identity routes — intro greetings", () => {
718
675
  "Charting the next useful thing?",
719
676
  "I brought the compass. Where to?",
720
677
  "Ready to make this lighter.",
678
+ "Five options, one good start.",
679
+ "A useful next step?",
721
680
  ],
722
681
  text: "Charting the next useful thing?",
723
682
  source: "cache",
@@ -5,9 +5,10 @@
5
5
  *
6
6
  * 1. The default injectors ({@link defaultInjectors}) are listed in the
7
7
  * documented order (disk-pressure-warning → workspace-context →
8
- * background-turn → unified-turn-context → pkb-context → pkb-reminder
9
- * memory-v2-staticnow-mdactive-documentsdocument-comments
10
- * subagent-statusslack-messagesthread-focus).
8
+ * background-turn → unified-turn-context → config-quarantine-notice
9
+ * pkb-contextpkb-remindermemory-v2-staticnow-md
10
+ * active-documentsdocument-commentssubagent-status →
11
+ * slack-messages → thread-focus).
11
12
  * 2. The assembled {@link injectorChain} sorts the defaults together with the
12
13
  * memory-v3 injector by ascending `order`, so memory-v3 (order 1000) lands
13
14
  * last.
@@ -298,6 +299,7 @@ describe("injector chain", () => {
298
299
  "workspace-context",
299
300
  "background-turn",
300
301
  "unified-turn-context",
302
+ "config-quarantine-notice",
301
303
  "pkb-context",
302
304
  "pkb-reminder",
303
305
  "memory-v2-static",
@@ -324,6 +326,9 @@ describe("injector chain", () => {
324
326
  expect(byName.get("unified-turn-context")).toBe(
325
327
  DEFAULT_INJECTOR_ORDER.unifiedTurnContext,
326
328
  );
329
+ expect(byName.get("config-quarantine-notice")).toBe(
330
+ DEFAULT_INJECTOR_ORDER.configQuarantineNotice,
331
+ );
327
332
  expect(byName.get("pkb-context")).toBe(DEFAULT_INJECTOR_ORDER.pkbContext);
328
333
  expect(byName.get("pkb-reminder")).toBe(DEFAULT_INJECTOR_ORDER.pkbReminder);
329
334
  expect(byName.get("memory-v2-static")).toBe(
@@ -0,0 +1,115 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+
5
+ import {
6
+ DEFAULT_INJECTOR_ORDER,
7
+ defaultInjectors,
8
+ } from "../plugins/defaults/memory-retrieval/injectors.js";
9
+ import type { Injector, TurnContext } from "../plugins/types.js";
10
+ import { getConfigQuarantineNoticePath } from "../util/platform.js";
11
+
12
+ function findInjector(name: string): Injector {
13
+ const injector = defaultInjectors.find(
14
+ (candidate) => candidate.name === name,
15
+ );
16
+ if (!injector) {
17
+ throw new Error(`injector '${name}' not registered`);
18
+ }
19
+ return injector;
20
+ }
21
+
22
+ function makeContext(overrides: Partial<TurnContext> = {}): TurnContext {
23
+ return {
24
+ requestId: "req-test",
25
+ conversationId: "conv-test",
26
+ turnIndex: 0,
27
+ trust: { sourceChannel: "vellum", trustClass: "guardian" },
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ const NOTICE_PATH = getConfigQuarantineNoticePath();
33
+
34
+ function writeNotice(quarantinedAt: string): void {
35
+ const dir = dirname(NOTICE_PATH);
36
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
37
+ writeFileSync(
38
+ NOTICE_PATH,
39
+ JSON.stringify(
40
+ {
41
+ quarantinedAt,
42
+ quarantinePath: "/ws/config.json.corrupt-x.json",
43
+ originalPath: "/ws/config.json",
44
+ },
45
+ null,
46
+ 2,
47
+ ),
48
+ "utf-8",
49
+ );
50
+ }
51
+
52
+ function clearNotice(): void {
53
+ rmSync(NOTICE_PATH, { force: true });
54
+ }
55
+
56
+ const injector = findInjector("config-quarantine-notice");
57
+
58
+ describe("config-quarantine-notice injector", () => {
59
+ beforeEach(() => {
60
+ clearNotice();
61
+ });
62
+
63
+ afterEach(() => {
64
+ clearNotice();
65
+ });
66
+
67
+ test("registered at the expected order with prepend-user-tail placement", async () => {
68
+ expect(injector.order).toBe(DEFAULT_INJECTOR_ORDER.configQuarantineNotice);
69
+ writeNotice(new Date().toISOString());
70
+ const block = await injector.produce(makeContext());
71
+ expect(block?.placement).toBe("prepend-user-tail");
72
+ });
73
+
74
+ test("returns null when no sentinel is present", async () => {
75
+ const block = await injector.produce(makeContext());
76
+ expect(block).toBeNull();
77
+ });
78
+
79
+ test("injects a config-reset block when the sentinel is present and fresh", async () => {
80
+ writeNotice(new Date().toISOString());
81
+
82
+ const block = await injector.produce(makeContext());
83
+ expect(block).not.toBeNull();
84
+ expect(block?.id).toBe("config-quarantine-notice");
85
+ expect(block?.text).toContain("<config_reset_notice>");
86
+ expect(block?.text).toContain("</config_reset_notice>");
87
+ expect(block?.text).toContain("/ws/config.json.corrupt-x.json");
88
+ // The sentinel is left in place for subsequent turns until it ages out.
89
+ expect(existsSync(NOTICE_PATH)).toBe(true);
90
+ });
91
+
92
+ test("injects nothing on non-guardian turns even when the sentinel is fresh", async () => {
93
+ writeNotice(new Date().toISOString());
94
+
95
+ for (const trustClass of ["trusted_contact", "unknown"] as const) {
96
+ const block = await injector.produce(
97
+ makeContext({ trust: { sourceChannel: "slack", trustClass } }),
98
+ );
99
+ expect(block).toBeNull();
100
+ }
101
+ // The sentinel is untouched — it remains for guardian turns.
102
+ expect(existsSync(NOTICE_PATH)).toBe(true);
103
+ });
104
+
105
+ test("deletes the sentinel and injects nothing when it is stale (>7 days)", async () => {
106
+ const eightDaysAgo = new Date(
107
+ Date.now() - 8 * 24 * 60 * 60 * 1000,
108
+ ).toISOString();
109
+ writeNotice(eightDaysAgo);
110
+
111
+ const block = await injector.produce(makeContext());
112
+ expect(block).toBeNull();
113
+ expect(existsSync(NOTICE_PATH)).toBe(false);
114
+ });
115
+ });
@@ -309,6 +309,22 @@ describe("LLM catalog parity: daemon vs client", () => {
309
309
  });
310
310
  });
311
311
 
312
+ test("MiniMax catalog includes MiniMax M3", () => {
313
+ const minimax = PROVIDER_CATALOG.find((entry) => entry.id === "minimax");
314
+ expect(
315
+ minimax?.models.find((model) => model.id === "MiniMax-M3"),
316
+ ).toMatchObject({
317
+ displayName: "MiniMax M3",
318
+ contextWindowTokens: 1000000,
319
+ defaultContextWindowTokens: 200000,
320
+ maxOutputTokens: 512000,
321
+ longContextMode: "native-model",
322
+ supportsThinking: true,
323
+ supportsVision: true,
324
+ supportsToolUse: true,
325
+ });
326
+ });
327
+
312
328
  // -----------------------------------------------------------------------
313
329
  // pricing.ts ↔ model-catalog.ts parity
314
330
  //
@@ -1653,6 +1653,17 @@ describe("queryUnreportedUsageEvents", () => {
1653
1653
  expect(events[0].turnIndex).toBe(0);
1654
1654
  });
1655
1655
 
1656
+ test("surfaces llmCallCount on unreported events", () => {
1657
+ insertEventAt(1000, { llmCallCount: 4 });
1658
+ insertEventAt(2000, {});
1659
+
1660
+ const events = queryUnreportedUsageEvents(0, undefined, 100);
1661
+ expect(events).toHaveLength(2);
1662
+ expect(events[0].llmCallCount).toBe(4);
1663
+ // recordUsageEvent defaults llmCallCount to 1 when unset.
1664
+ expect(events[1].llmCallCount).toBe(1);
1665
+ });
1666
+
1656
1667
  test("surfaces raw_usage on unreported events", () => {
1657
1668
  // The telemetry reporter consumes `queryUnreportedUsageEvents` output
1658
1669
  // directly and forwards it to BigQuery. If the SELECT projection