@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * ClickHouse compaction log writer.
2
+ * ClickHouse compaction log store.
3
3
  *
4
4
  * Consumes the agent loop's compaction start/end event pair
5
5
  * (`context_compacting` / `compaction_completed`, correlated by
@@ -10,8 +10,12 @@
10
10
  *
11
11
  * The table is event-sourced rather than upserted: a `start` row records
12
12
  * that an attempt began (so attempts that never complete — pipeline throw,
13
- * turn abort — are still visible), and an `end` row carries the full
14
- * `ContextWindowResult`. Consumers pair rows by `compaction_id`.
13
+ * turn abort — are still visible), and an `end` row carries the
14
+ * `ContextWindowResult` fields unnested on the event. Consumers pair rows
15
+ * by `compaction_id`.
16
+ *
17
+ * The store also serves reads: the compaction-trail route pairs the rows
18
+ * back into per-attempt events via `getEventsBetween`.
15
19
  *
16
20
  * Writes are strictly best-effort: every entry point swallows and logs
17
21
  * failures so a ClickHouse outage can never abort a turn. URL and password
@@ -27,7 +31,7 @@ import { credentialKey } from "../security/credential-key.js";
27
31
  import { getSecureKeyAsync } from "../security/secure-keys.js";
28
32
  import { getLogger } from "../util/logger.js";
29
33
 
30
- const log = getLogger("compaction-log-writer");
34
+ const log = getLogger("compaction-log-store");
31
35
 
32
36
  export type CompactionStartEvent = Extract<
33
37
  AgentEvent,
@@ -58,7 +62,7 @@ interface CompactionLogRow {
58
62
  finished_at: number;
59
63
  duration_ms: number;
60
64
  pre_message_count: number;
61
- basis_message_count: number;
65
+ result_message_count: number;
62
66
  compacted: number;
63
67
  previous_estimated_input_tokens: number;
64
68
  estimated_input_tokens: number;
@@ -87,10 +91,47 @@ async function readCredentialOrNull(
87
91
  return value ?? null;
88
92
  }
89
93
 
94
+ /**
95
+ * One compaction attempt, reconstructed from its start/end row pair.
96
+ * `null` means the value isn't known — either the field belongs to the
97
+ * other phase's row, the attempt never completed, or the stored value was
98
+ * the column's sentinel.
99
+ */
100
+ export interface CompactionLogEvent {
101
+ compactionId: string;
102
+ requestId: string;
103
+ trigger: string;
104
+ startedAt: number;
105
+ finishedAt: number | null;
106
+ durationMs: number | null;
107
+ preMessageCount: number | null;
108
+ resultMessageCount: number | null;
109
+ compacted: boolean | null;
110
+ previousEstimatedInputTokens: number | null;
111
+ estimatedInputTokens: number | null;
112
+ maxInputTokens: number | null;
113
+ thresholdTokens: number | null;
114
+ compactedMessages: number | null;
115
+ compactedPersistedMessages: number | null;
116
+ preservedTailMessages: number | null;
117
+ summaryCalls: number | null;
118
+ summaryInputTokens: number | null;
119
+ summaryOutputTokens: number | null;
120
+ summaryModel: string | null;
121
+ summaryFailed: boolean | null;
122
+ reason: string | null;
123
+ exhausted: boolean | null;
124
+ injectionMode: string | null;
125
+ autoCompressApplied: boolean | null;
126
+ summaryText: string | null;
127
+ /** True when the end row exists — the attempt ran to completion. */
128
+ completed: boolean;
129
+ }
130
+
90
131
  /** Injectable fetch override for tests. Defaults to globalThis.fetch. */
91
132
  export type CompactionLogFetch = typeof fetch;
92
133
 
93
- export interface ClickHouseCompactionLogWriterDeps {
134
+ export interface ClickHouseCompactionLogStoreDeps {
94
135
  /** Override the credential read for `clickhouse:url`. */
95
136
  resolveUrl?: () => Promise<string | null>;
96
137
  /** Override the credential read for `clickhouse:password`. */
@@ -101,7 +142,7 @@ export interface ClickHouseCompactionLogWriterDeps {
101
142
  fetchImpl?: CompactionLogFetch;
102
143
  }
103
144
 
104
- export class ClickHouseCompactionLogWriter {
145
+ export class ClickHouseCompactionLogStore {
105
146
  private cachedUrl: string | null = null;
106
147
  private cachedPassword: string | null = null;
107
148
  private cachedAssistantId: string | null = null;
@@ -114,7 +155,7 @@ export class ClickHouseCompactionLogWriter {
114
155
 
115
156
  constructor(
116
157
  private readonly config: CompactionLogsClickHouseConfig,
117
- deps: ClickHouseCompactionLogWriterDeps = {},
158
+ deps: ClickHouseCompactionLogStoreDeps = {},
118
159
  ) {
119
160
  this.resolveUrl =
120
161
  deps.resolveUrl ?? (() => readCredentialOrNull("clickhouse", "url"));
@@ -142,35 +183,105 @@ export class ClickHouseCompactionLogWriter {
142
183
  conversationId: string,
143
184
  event: CompactionEndEvent,
144
185
  ): Promise<void> {
145
- const r = event.result;
146
186
  await this.insert({
147
187
  ...emptyRow(await this.assistantId(), conversationId, event),
148
188
  phase: "end",
149
189
  finished_at: event.finishedAt,
150
190
  duration_ms: event.finishedAt - event.startedAt,
151
- basis_message_count: event.messages.length,
152
- compacted: r.compacted ? 1 : 0,
153
- previous_estimated_input_tokens: r.previousEstimatedInputTokens,
154
- estimated_input_tokens: r.estimatedInputTokens,
155
- max_input_tokens: r.maxInputTokens,
156
- threshold_tokens: r.thresholdTokens,
157
- compacted_messages: r.compactedMessages,
158
- compacted_persisted_messages: r.compactedPersistedMessages,
159
- preserved_tail_messages: r.preservedTailMessages ?? -1,
160
- summary_calls: r.summaryCalls,
161
- summary_input_tokens: r.summaryInputTokens,
162
- summary_output_tokens: r.summaryOutputTokens,
163
- summary_model: r.summaryModel,
191
+ result_message_count: event.messages.length,
192
+ compacted: event.compacted ? 1 : 0,
193
+ previous_estimated_input_tokens: event.previousEstimatedInputTokens,
194
+ estimated_input_tokens: event.estimatedInputTokens,
195
+ max_input_tokens: event.maxInputTokens,
196
+ threshold_tokens: event.thresholdTokens,
197
+ compacted_messages: event.compactedMessages,
198
+ compacted_persisted_messages: event.compactedPersistedMessages,
199
+ preserved_tail_messages: event.preservedTailMessages ?? -1,
200
+ summary_calls: event.summaryCalls,
201
+ summary_input_tokens: event.summaryInputTokens,
202
+ summary_output_tokens: event.summaryOutputTokens,
203
+ summary_model: event.summaryModel,
164
204
  summary_failed:
165
- r.summaryFailed === undefined ? -1 : r.summaryFailed ? 1 : 0,
166
- reason: r.reason ?? "",
167
- exhausted: r.exhausted ? 1 : 0,
168
- injection_mode: r.injectionMode ?? "",
169
- auto_compress_applied: r.autoCompressApplied ? 1 : 0,
170
- summary_text: r.summaryText.slice(0, SUMMARY_TEXT_MAX_CHARS),
205
+ event.summaryFailed === undefined ? -1 : event.summaryFailed ? 1 : 0,
206
+ reason: event.reason ?? "",
207
+ exhausted: event.exhausted ? 1 : 0,
208
+ injection_mode: event.injectionMode ?? "",
209
+ auto_compress_applied: event.autoCompressApplied ? 1 : 0,
210
+ summary_text: event.summaryText.slice(0, SUMMARY_TEXT_MAX_CHARS),
171
211
  });
172
212
  }
173
213
 
214
+ /**
215
+ * Read all compaction attempts whose `started_at` falls strictly inside
216
+ * the given window, pairing start/end rows by `compaction_id`. Mirrors
217
+ * the strict `>` / `<` predicate contract of
218
+ * `getCompactionLogsBetween` on the llm-request-log sources, so the
219
+ * route can apply the same ±1ms boundary shifts to both.
220
+ */
221
+ async getEventsBetween(
222
+ conversationId: string,
223
+ afterStartedAt: number | null,
224
+ beforeStartedAt: number,
225
+ ): Promise<CompactionLogEvent[]> {
226
+ const params: Record<string, string> = {
227
+ assistant_id: await this.assistantId(),
228
+ conversation_id: conversationId,
229
+ before_started_at: String(beforeStartedAt),
230
+ };
231
+ // Type-bound parameter slots referenced in the SQL but unbound at exec
232
+ // time return a server error, so the floor predicate is only templated
233
+ // in when the caller has one.
234
+ let afterPredicate = "";
235
+ if (afterStartedAt !== null) {
236
+ params.after_started_at = String(afterStartedAt);
237
+ afterPredicate =
238
+ " AND started_at > fromUnixTimestamp64Milli({after_started_at:Int64})";
239
+ }
240
+ const sql = `SELECT
241
+ compaction_id,
242
+ request_id,
243
+ phase,
244
+ trigger,
245
+ toUnixTimestamp64Milli(started_at) AS started_at,
246
+ toUnixTimestamp64Milli(finished_at) AS finished_at,
247
+ duration_ms,
248
+ pre_message_count,
249
+ result_message_count,
250
+ compacted,
251
+ previous_estimated_input_tokens,
252
+ estimated_input_tokens,
253
+ max_input_tokens,
254
+ threshold_tokens,
255
+ compacted_messages,
256
+ compacted_persisted_messages,
257
+ preserved_tail_messages,
258
+ summary_calls,
259
+ summary_input_tokens,
260
+ summary_output_tokens,
261
+ summary_model,
262
+ summary_failed,
263
+ reason,
264
+ exhausted,
265
+ injection_mode,
266
+ auto_compress_applied,
267
+ summary_text
268
+ FROM ${this.tableRef()}
269
+ WHERE assistant_id = {assistant_id:String}
270
+ AND conversation_id = {conversation_id:String}
271
+ AND started_at < fromUnixTimestamp64Milli({before_started_at:Int64})${afterPredicate}
272
+ ORDER BY started_at ASC, created_at ASC
273
+ LIMIT 1 BY compaction_id, phase
274
+ FORMAT JSONEachRow`;
275
+ const text = await this.exec(sql, undefined, params);
276
+ const rows: Record<string, unknown>[] = [];
277
+ for (const line of text.split("\n")) {
278
+ const trimmed = line.trim();
279
+ if (trimmed.length === 0) continue;
280
+ rows.push(JSON.parse(trimmed) as Record<string, unknown>);
281
+ }
282
+ return pairRowsIntoEvents(rows);
283
+ }
284
+
174
285
  private async insert(row: CompactionLogRow): Promise<void> {
175
286
  await this.ensureTable();
176
287
  await this.exec(
@@ -198,7 +309,7 @@ export class ClickHouseCompactionLogWriter {
198
309
  finished_at DateTime64(3),
199
310
  duration_ms Int64,
200
311
  pre_message_count Int64,
201
- basis_message_count Int64,
312
+ result_message_count Int64,
202
313
  compacted UInt8,
203
314
  previous_estimated_input_tokens Int64,
204
315
  estimated_input_tokens Int64,
@@ -220,10 +331,13 @@ export class ClickHouseCompactionLogWriter {
220
331
  created_at DateTime64(3) DEFAULT now64(3)
221
332
  ) ENGINE = MergeTree
222
333
  ORDER BY (assistant_id, conversation_id, started_at, compaction_id)`,
223
- ).catch((err: unknown) => {
224
- this.ensureTablePromise = null;
225
- throw err;
226
- });
334
+ ).then(
335
+ () => undefined,
336
+ (err: unknown) => {
337
+ this.ensureTablePromise = null;
338
+ throw err;
339
+ },
340
+ );
227
341
  }
228
342
  return this.ensureTablePromise;
229
343
  }
@@ -235,7 +349,11 @@ export class ClickHouseCompactionLogWriter {
235
349
  return `\`${this.config.table.replace(/`/g, "``")}\``;
236
350
  }
237
351
 
238
- private async exec(sql: string, body?: string): Promise<void> {
352
+ private async exec(
353
+ sql: string,
354
+ body?: string,
355
+ params?: Record<string, string>,
356
+ ): Promise<string> {
239
357
  const baseUrl = await this.url();
240
358
  const password = await this.password();
241
359
 
@@ -248,6 +366,9 @@ export class ClickHouseCompactionLogWriter {
248
366
  );
249
367
  }
250
368
  target.searchParams.set("database", this.config.database);
369
+ for (const [k, v] of Object.entries(params ?? {})) {
370
+ target.searchParams.set(`param_${k}`, v);
371
+ }
251
372
  // For INSERTs the statement goes in the `query` param and the row data
252
373
  // in the body; DDL ships as the body itself.
253
374
  // Integer timestamps land correctly in DateTime64(3) columns: ClickHouse
@@ -270,11 +391,10 @@ export class ClickHouseCompactionLogWriter {
270
391
  if (!res.ok) {
271
392
  const text = await res.text().catch(() => "");
272
393
  throw new Error(
273
- `ClickHouse compaction-log write failed (HTTP ${res.status}): ${text.slice(0, 500)}`,
394
+ `ClickHouse compaction-log request failed (HTTP ${res.status}): ${text.slice(0, 500)}`,
274
395
  );
275
396
  }
276
- // Drain the body so the connection can be reused.
277
- await res.text().catch(() => "");
397
+ return res.text();
278
398
  }
279
399
 
280
400
  private async assistantId(): Promise<string> {
@@ -331,7 +451,7 @@ function emptyRow(
331
451
  finished_at: 0,
332
452
  duration_ms: -1,
333
453
  pre_message_count: -1,
334
- basis_message_count: -1,
454
+ result_message_count: -1,
335
455
  compacted: 0,
336
456
  previous_estimated_input_tokens: -1,
337
457
  estimated_input_tokens: -1,
@@ -353,32 +473,121 @@ function emptyRow(
353
473
  };
354
474
  }
355
475
 
476
+ function num(value: unknown): number {
477
+ return typeof value === "number" ? value : Number(value);
478
+ }
479
+
480
+ /** Map the `-1` "unknown count" column sentinel back to null. */
481
+ function countOrNull(value: unknown): number | null {
482
+ const n = num(value);
483
+ return Number.isFinite(n) && n >= 0 ? n : null;
484
+ }
485
+
486
+ /** Map the `''` "not set" string column sentinel back to null. */
487
+ function strOrNull(value: unknown): string | null {
488
+ return typeof value === "string" && value.length > 0 ? value : null;
489
+ }
490
+
356
491
  /**
357
- * Writer cache keyed by the serialized connection config so config edits
492
+ * Pair raw start/end rows by `compaction_id` into per-attempt events.
493
+ * Start-only pairs (the attempt never completed) surface with end-phase
494
+ * fields null and `completed: false`.
495
+ */
496
+ function pairRowsIntoEvents(
497
+ rows: Record<string, unknown>[],
498
+ ): CompactionLogEvent[] {
499
+ const byId = new Map<
500
+ string,
501
+ { start?: Record<string, unknown>; end?: Record<string, unknown> }
502
+ >();
503
+ for (const row of rows) {
504
+ const id = String(row.compaction_id);
505
+ const pair = byId.get(id) ?? {};
506
+ if (row.phase === "end") pair.end = row;
507
+ else pair.start = row;
508
+ byId.set(id, pair);
509
+ }
510
+ const events: CompactionLogEvent[] = [];
511
+ for (const [compactionId, { start, end }] of byId) {
512
+ const base = end ?? start;
513
+ if (!base) continue;
514
+ events.push({
515
+ compactionId,
516
+ requestId: String(base.request_id ?? ""),
517
+ trigger: String(base.trigger ?? ""),
518
+ startedAt: num(base.started_at),
519
+ finishedAt: end ? num(end.finished_at) : null,
520
+ durationMs: end ? countOrNull(end.duration_ms) : null,
521
+ preMessageCount: start ? countOrNull(start.pre_message_count) : null,
522
+ resultMessageCount: end ? countOrNull(end.result_message_count) : null,
523
+ compacted: end ? num(end.compacted) === 1 : null,
524
+ previousEstimatedInputTokens: end
525
+ ? countOrNull(end.previous_estimated_input_tokens)
526
+ : null,
527
+ estimatedInputTokens: end
528
+ ? countOrNull(end.estimated_input_tokens)
529
+ : null,
530
+ maxInputTokens: end ? countOrNull(end.max_input_tokens) : null,
531
+ thresholdTokens: end ? countOrNull(end.threshold_tokens) : null,
532
+ compactedMessages: end ? countOrNull(end.compacted_messages) : null,
533
+ compactedPersistedMessages: end
534
+ ? countOrNull(end.compacted_persisted_messages)
535
+ : null,
536
+ preservedTailMessages: end
537
+ ? countOrNull(end.preserved_tail_messages)
538
+ : null,
539
+ summaryCalls: end ? countOrNull(end.summary_calls) : null,
540
+ summaryInputTokens: end ? countOrNull(end.summary_input_tokens) : null,
541
+ summaryOutputTokens: end ? countOrNull(end.summary_output_tokens) : null,
542
+ summaryModel: end ? strOrNull(end.summary_model) : null,
543
+ summaryFailed:
544
+ end && num(end.summary_failed) !== -1
545
+ ? num(end.summary_failed) === 1
546
+ : null,
547
+ reason: end ? strOrNull(end.reason) : null,
548
+ exhausted: end ? num(end.exhausted) === 1 : null,
549
+ injectionMode: end ? strOrNull(end.injection_mode) : null,
550
+ autoCompressApplied: end ? num(end.auto_compress_applied) === 1 : null,
551
+ summaryText: end ? strOrNull(end.summary_text) : null,
552
+ completed: end !== undefined,
553
+ });
554
+ }
555
+ return events.sort(
556
+ (a, b) =>
557
+ a.startedAt - b.startedAt || a.compactionId.localeCompare(b.compactionId),
558
+ );
559
+ }
560
+
561
+ /**
562
+ * Store cache keyed by the serialized connection config so config edits
358
563
  * take effect on the next event without restarting, while steady-state
359
564
  * writes reuse one instance (and its cached credentials / ensured table).
360
565
  */
361
- let cachedWriter: {
566
+ let cachedStore: {
362
567
  key: string;
363
- writer: ClickHouseCompactionLogWriter;
568
+ store: ClickHouseCompactionLogStore;
364
569
  } | null = null;
365
570
 
366
- function getWriter(): ClickHouseCompactionLogWriter | null {
571
+ /**
572
+ * Resolve the configured ClickHouse compaction log store, or null when
573
+ * `compactionLogs.destination` is not `"clickhouse"`.
574
+ */
575
+ export function getCompactionLogStore(): ClickHouseCompactionLogStore | null {
367
576
  const cfg = getConfig().compactionLogs;
368
577
  if (!cfg || cfg.destination !== "clickhouse") return null;
369
578
  const key = JSON.stringify(cfg.clickhouse);
370
- if (!cachedWriter || cachedWriter.key !== key) {
371
- cachedWriter = {
579
+ if (!cachedStore || cachedStore.key !== key) {
580
+ cachedStore = {
372
581
  key,
373
- writer: new ClickHouseCompactionLogWriter(cfg.clickhouse),
582
+ store: new ClickHouseCompactionLogStore(cfg.clickhouse),
374
583
  };
375
584
  }
376
- return cachedWriter.writer;
585
+ return cachedStore.store;
377
586
  }
378
587
 
379
- /** Test hook: drop the memoized writer so config/dep changes are picked up. */
380
- export function resetCompactionLogWriterForTests(): void {
381
- cachedWriter = null;
588
+ /** Test hook: drop the memoized store so config/dep changes are picked up. */
589
+ export function resetCompactionLogStoreForTests(): void {
590
+ cachedStore = null;
382
591
  }
383
592
 
384
593
  /**
@@ -389,9 +598,9 @@ export function recordCompactionStartBestEffort(
389
598
  conversationId: string,
390
599
  event: CompactionStartEvent,
391
600
  ): void {
392
- const writer = getWriter();
393
- if (!writer) return;
394
- writer.writeStart(conversationId, event).catch((err: unknown) => {
601
+ const store = getCompactionLogStore();
602
+ if (!store) return;
603
+ store.writeStart(conversationId, event).catch((err: unknown) => {
395
604
  log.warn(
396
605
  { err, conversationId, compactionId: event.compactionId },
397
606
  "Failed to write compaction start log (non-fatal)",
@@ -407,9 +616,9 @@ export function recordCompactionEndBestEffort(
407
616
  conversationId: string,
408
617
  event: CompactionEndEvent,
409
618
  ): void {
410
- const writer = getWriter();
411
- if (!writer) return;
412
- writer.writeEnd(conversationId, event).catch((err: unknown) => {
619
+ const store = getCompactionLogStore();
620
+ if (!store) return;
621
+ store.writeEnd(conversationId, event).catch((err: unknown) => {
413
622
  log.warn(
414
623
  { err, conversationId, compactionId: event.compactionId },
415
624
  "Failed to write compaction end log (non-fatal)",
@@ -24,6 +24,7 @@ export type SignalType =
24
24
  | "macos_notification_view"
25
25
  | "macos_conversation_opened"
26
26
  | "ios_conversation_opened"
27
+ | "web_bulk_mark_read"
27
28
  | "telegram_inbound_message"
28
29
  | "telegram_callback"
29
30
  | "slack_inbound_message"
@@ -1,7 +1,7 @@
1
1
  import { createConversation } from "./conversation-crud.js";
2
2
  import {
3
- GENERATING_TITLE,
4
- queueGenerateConversationTitle,
3
+ AUTO_TITLE_DETERMINISTIC,
4
+ deriveDeterministicTitle,
5
5
  type TitleOrigin,
6
6
  } from "./conversation-title-service.js";
7
7
 
@@ -22,9 +22,23 @@ export interface BootstrapConversationOptions {
22
22
  forkParentConversationId?: string;
23
23
  }
24
24
 
25
+ /**
26
+ * Create a system-initiated conversation with a deterministic title derived
27
+ * from `systemHint` — no LLM call. Background conversations (heartbeat runs,
28
+ * scheduled jobs, subagents, retrospectives) are created in bulk and rarely
29
+ * read by name, so an LLM-generated title is not worth the tokens. The title
30
+ * is persisted with `AUTO_TITLE_DETERMINISTIC`, which keeps it replaceable:
31
+ * if a user ever sends a message in the conversation, the title-generate
32
+ * hook upgrades it to an LLM title (see
33
+ * `plugins/defaults/title-generate/hooks/user-prompt-submit.ts`).
34
+ */
25
35
  export function bootstrapConversation(opts: BootstrapConversationOptions) {
26
- const conversation = createConversation({
27
- title: GENERATING_TITLE,
36
+ return createConversation({
37
+ title: deriveDeterministicTitle({
38
+ origin: opts.origin,
39
+ systemHint: opts.systemHint,
40
+ }),
41
+ isAutoTitle: AUTO_TITLE_DETERMINISTIC,
28
42
  ...(opts.conversationType && { conversationType: opts.conversationType }),
29
43
  ...(opts.source && { source: opts.source }),
30
44
  ...(opts.scheduleJobId && { scheduleJobId: opts.scheduleJobId }),
@@ -33,9 +47,4 @@ export function bootstrapConversation(opts: BootstrapConversationOptions) {
33
47
  forkParentConversationId: opts.forkParentConversationId,
34
48
  }),
35
49
  });
36
- queueGenerateConversationTitle({
37
- conversationId: conversation.id,
38
- context: { origin: opts.origin, systemHint: opts.systemHint },
39
- });
40
- return conversation;
41
50
  }
@@ -537,6 +537,13 @@ export function createConversation(
537
537
  | string
538
538
  | {
539
539
  title?: string;
540
+ /**
541
+ * Override the `is_auto_title` column (schema default 1). Pass
542
+ * `AUTO_TITLE_DETERMINISTIC` (2) when the title was derived
543
+ * deterministically at bootstrap so later generation passes know
544
+ * they may replace it.
545
+ */
546
+ isAutoTitle?: number;
540
547
  conversationType?: ConversationCreateType;
541
548
  source?: string;
542
549
  scheduleJobId?: string;
@@ -566,6 +573,9 @@ export function createConversation(
566
573
  const conversation = {
567
574
  id,
568
575
  title: opts.title ?? null,
576
+ ...(opts.isAutoTitle !== undefined
577
+ ? { isAutoTitle: opts.isAutoTitle }
578
+ : {}),
569
579
  createdAt: now,
570
580
  updatedAt: now,
571
581
  totalInputTokens: 0,
@@ -2039,8 +2049,8 @@ export interface OverrideProfileFields {
2039
2049
  /**
2040
2050
  * Resolve the per-turn inference-profile override from a conversation's
2041
2051
  * fields. Returns the `inferenceProfile` for interactive conversations,
2042
- * `undefined` for automation threads (subagent fan-out, scheduled tasks,
2043
- * update bulletins) so they run on the workspace defaults rather than
2052
+ * `undefined` for automation threads (subagent fan-out, scheduled
2053
+ * tasks) so they run on the workspace defaults rather than
2044
2054
  * inheriting an interactive override.
2045
2055
  */
2046
2056
  export function resolveOverrideProfile(
@@ -39,7 +39,6 @@ export type TitleOrigin =
39
39
  | "filing"
40
40
  | "local"
41
41
  | "task_submit"
42
- | "updates_bulletin"
43
42
  | "memory_consolidation"
44
43
  | "memory_retrospective"
45
44
  | "misc";
@@ -63,6 +62,21 @@ export interface TitleContext {
63
62
  export const GENERATING_TITLE = "Generating title...";
64
63
  const UNTITLED_FALLBACK = "Untitled Conversation";
65
64
 
65
+ // ── `conversations.isAutoTitle` values ───────────────────────────────
66
+ //
67
+ // Most readers treat the column as a boolean ("not user-set"); the two
68
+ // distinct non-zero values let the title pipeline tell HOW a title was set.
69
+
70
+ /** Title was generated by an LLM from conversation content. */
71
+ export const AUTO_TITLE_LLM = 1;
72
+ /**
73
+ * Title was derived deterministically from bootstrap context — no LLM call,
74
+ * no user input. Unlike LLM titles, these stay replaceable: the first
75
+ * genuine user message in the conversation upgrades them to an LLM title
76
+ * (see `canReplaceTitle`).
77
+ */
78
+ export const AUTO_TITLE_DETERMINISTIC = 2;
79
+
66
80
  // ── Replaceability check ─────────────────────────────────────────────
67
81
 
68
82
  const REPLACEABLE_PATTERNS = [
@@ -83,6 +97,35 @@ export function isReplaceableTitle(title: string | null): boolean {
83
97
  return REPLACEABLE_PATTERNS.some((pattern) => pattern.test(title));
84
98
  }
85
99
 
100
+ /**
101
+ * Whether auto-generation may overwrite the conversation's current title.
102
+ * True for placeholder/system titles and for deterministic bootstrap titles
103
+ * (`AUTO_TITLE_DETERMINISTIC`); false for user-set custom titles and titles
104
+ * already generated by an LLM.
105
+ */
106
+ function canReplaceTitle(conversation: {
107
+ title: string | null;
108
+ isAutoTitle: number;
109
+ }): boolean {
110
+ return (
111
+ isReplaceableTitle(conversation.title) ||
112
+ conversation.isAutoTitle === AUTO_TITLE_DETERMINISTIC
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Derive a deterministic title from bootstrap context without an LLM call.
118
+ * Used for background/system conversations (heartbeat runs, scheduled jobs,
119
+ * subagents, retrospectives) where an LLM-generated title is not worth the
120
+ * tokens — the bootstrap `systemHint` already names the work. Persist it
121
+ * with `AUTO_TITLE_DETERMINISTIC` so a later genuine user message can still
122
+ * upgrade it to an LLM title.
123
+ */
124
+ export function deriveDeterministicTitle(context: TitleContext): string {
125
+ const base = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
126
+ return truncateTitle(base.replace(/\s+/g, " ").trim());
127
+ }
128
+
86
129
  // ── Title generation ─────────────────────────────────────────────────
87
130
 
88
131
  export interface GenerateTitleParams {
@@ -111,16 +154,17 @@ export async function generateAndPersistConversationTitle(
111
154
 
112
155
  // Check current title is replaceable
113
156
  const conversation = getConversation(conversationId);
114
- if (conversation && !isReplaceableTitle(conversation.title)) {
157
+ if (conversation && !canReplaceTitle(conversation)) {
115
158
  return { title: conversation.title!, updated: false };
116
159
  }
117
160
 
118
161
  const provider =
119
162
  params.provider ?? (await getConfiguredProvider("conversationTitle"));
120
163
  if (!provider) {
121
- // No provider available — fall back to context-derived title or untitled
164
+ // No provider available — fall back to context-derived title or untitled.
165
+ // Deterministic, so keep it upgradeable by a later generation pass.
122
166
  const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
123
- updateConversationTitle(conversationId, fallback, 1);
167
+ updateConversationTitle(conversationId, fallback, AUTO_TITLE_DETERMINISTIC);
124
168
  publishConversationTitleChanged(conversationId, fallback);
125
169
  return { title: fallback, updated: true };
126
170
  }
@@ -139,11 +183,11 @@ export async function generateAndPersistConversationTitle(
139
183
  if (title) {
140
184
  // Re-check replaceability before persisting (race guard)
141
185
  const current = getConversation(conversationId);
142
- if (current && !isReplaceableTitle(current.title)) {
186
+ if (current && !canReplaceTitle(current)) {
143
187
  return { title: current.title!, updated: false };
144
188
  }
145
189
 
146
- updateConversationTitle(conversationId, title, 1);
190
+ updateConversationTitle(conversationId, title, AUTO_TITLE_LLM);
147
191
  publishConversationTitleChanged(conversationId, title);
148
192
  log.info({ conversationId, title }, "Auto-generated conversation title");
149
193
  return { title, updated: true };
@@ -155,12 +199,12 @@ export async function generateAndPersistConversationTitle(
155
199
  // while the LLM request was in-flight; writing unconditionally would
156
200
  // clobber the user's intent.
157
201
  const currentForFallback = getConversation(conversationId);
158
- if (currentForFallback && !isReplaceableTitle(currentForFallback.title)) {
202
+ if (currentForFallback && !canReplaceTitle(currentForFallback)) {
159
203
  return { title: currentForFallback.title!, updated: false };
160
204
  }
161
205
 
162
206
  const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
163
- updateConversationTitle(conversationId, fallback, 1);
207
+ updateConversationTitle(conversationId, fallback, AUTO_TITLE_DETERMINISTIC);
164
208
  publishConversationTitleChanged(conversationId, fallback);
165
209
  return { title: fallback, updated: true };
166
210
  }
@@ -277,7 +321,7 @@ export async function regenerateConversationTitle(
277
321
  return { title: current?.title ?? UNTITLED_FALLBACK, updated: false };
278
322
  }
279
323
 
280
- updateConversationTitle(conversationId, title, 1);
324
+ updateConversationTitle(conversationId, title, AUTO_TITLE_LLM);
281
325
  publishConversationTitleChanged(conversationId, title);
282
326
  log.info(
283
327
  { conversationId, title },