@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,10 +1,11 @@
1
1
  /**
2
2
  * Route handlers for conversation listing, detail, and seen/unread state.
3
3
  *
4
- * GET /v1/conversations — paginated conversation list
5
- * POST /v1/conversations/seen — record a seen signal
6
- * POST /v1/conversations/unread — mark a conversation unread
7
- * GET /v1/conversations/:id — conversation detail
4
+ * GET /v1/conversations — paginated conversation list
5
+ * POST /v1/conversations/seen — record a seen signal (single)
6
+ * POST /v1/conversations/seen/bulk — record seen signals (batch)
7
+ * POST /v1/conversations/unread — mark a conversation unread
8
+ * GET /v1/conversations/:id — conversation detail
8
9
  */
9
10
 
10
11
  import { z } from "zod";
@@ -37,7 +38,10 @@ import {
37
38
  buildConversationDetailResponse,
38
39
  serializeConversationSummary,
39
40
  } from "../services/conversation-serializer.js";
40
- import { publishConversationListAndMetadataChanged } from "../sync/resource-sync-events.js";
41
+ import {
42
+ publishConversationListAndMetadataChanged,
43
+ publishConversationListChanged,
44
+ } from "../sync/resource-sync-events.js";
41
45
  import {
42
46
  BadRequestError,
43
47
  InternalError,
@@ -73,6 +77,7 @@ const assistantAttentionSchema = z.object({
73
77
  "macos_notification_view",
74
78
  "macos_conversation_opened",
75
79
  "ios_conversation_opened",
80
+ "web_bulk_mark_read",
76
81
  "telegram_inbound_message",
77
82
  "telegram_callback",
78
83
  "slack_inbound_message",
@@ -318,6 +323,55 @@ function handleRecordSeen({ body = {}, headers }: RouteHandlerArgs) {
318
323
  }
319
324
  }
320
325
 
326
+ function handleRecordSeenBulk({ body = {}, headers }: RouteHandlerArgs) {
327
+ const rawIds = body.conversationIds as string[] | undefined;
328
+ if (!Array.isArray(rawIds) || rawIds.length === 0) {
329
+ throw new BadRequestError("conversationIds must be a non-empty array");
330
+ }
331
+
332
+ const originClientId = headers?.["x-vellum-client-id"]?.trim() || undefined;
333
+ const changedIds: string[] = [];
334
+
335
+ for (const rawId of rawIds) {
336
+ try {
337
+ const conversationId = resolveOrThrow(rawId);
338
+ const priorState = getAttentionStateByConversationIds([
339
+ conversationId,
340
+ ]).get(conversationId);
341
+ const wasUnseen =
342
+ priorState != null &&
343
+ priorState.latestAssistantMessageAt != null &&
344
+ (priorState.lastSeenAssistantMessageAt == null ||
345
+ priorState.lastSeenAssistantMessageAt <
346
+ priorState.latestAssistantMessageAt);
347
+
348
+ recordConversationSeenSignal({
349
+ conversationId,
350
+ sourceChannel: "vellum",
351
+ signalType: "web_bulk_mark_read",
352
+ confidence: "explicit",
353
+ source: "http-api",
354
+ });
355
+
356
+ if (wasUnseen) {
357
+ changedIds.push(conversationId);
358
+ }
359
+ } catch (err) {
360
+ log.error(
361
+ { err, conversationId: rawId },
362
+ "POST /v1/conversations/seen/bulk: failed for conversation",
363
+ );
364
+ // Best-effort: continue with remaining conversations.
365
+ }
366
+ }
367
+
368
+ if (changedIds.length > 0) {
369
+ publishConversationListChanged("seen_changed", originClientId);
370
+ }
371
+
372
+ return { ok: true, updated: changedIds.length };
373
+ }
374
+
321
375
  function handleMarkUnread({ body = {}, headers }: RouteHandlerArgs) {
322
376
  const rawConversationId = body.conversationId as string | undefined;
323
377
  if (!rawConversationId) {
@@ -426,6 +480,24 @@ export const ROUTES: RouteDefinition[] = [
426
480
  responseBody: z.object({ ok: z.boolean() }),
427
481
  handler: handleRecordSeen,
428
482
  },
483
+ {
484
+ operationId: "recordConversationSeenBulk",
485
+ endpoint: "conversations/seen/bulk",
486
+ method: "POST",
487
+ policy: {
488
+ requiredScopes: ["chat.write"],
489
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
490
+ },
491
+ summary: "Bulk mark conversations as seen",
492
+ description:
493
+ "Mark multiple conversations as seen in one request. Emits a single sync invalidation for the entire batch instead of per-conversation events.",
494
+ tags: ["conversations"],
495
+ requestBody: z.object({
496
+ conversationIds: z.array(z.string()).min(1),
497
+ }),
498
+ responseBody: z.object({ ok: z.boolean(), updated: z.number() }),
499
+ handler: handleRecordSeenBulk,
500
+ },
429
501
  {
430
502
  operationId: "markConversationUnread",
431
503
  endpoint: "conversations/unread",
@@ -11,6 +11,7 @@
11
11
  * DELETE /v1/conversations/:id — delete a single conversation
12
12
  * POST /v1/conversations/:id/archive — archive a conversation
13
13
  * POST /v1/conversations/:id/unarchive — restore an archived conversation
14
+ * POST /v1/conversations/archive/bulk — archive multiple conversations
14
15
  * POST /v1/conversations/:id/surface — promote to / demote from Recents
15
16
  * POST /v1/conversations/:id/cancel — cancel generation
16
17
  * POST /v1/conversations/:id/undo — undo last message
@@ -362,6 +363,41 @@ function handleUnarchiveConversation({
362
363
  return { ok: true, conversationId: resolvedId };
363
364
  }
364
365
 
366
+ function handleArchiveConversationsBulk({
367
+ body = {},
368
+ headers,
369
+ }: RouteHandlerArgs) {
370
+ const rawIds = body.conversationIds as string[] | undefined;
371
+ if (!Array.isArray(rawIds) || rawIds.length === 0) {
372
+ throw new BadRequestError("conversationIds must be a non-empty array");
373
+ }
374
+
375
+ const originClientId = headers?.["x-vellum-client-id"]?.trim() || undefined;
376
+ const archivedIds: string[] = [];
377
+
378
+ for (const rawId of rawIds) {
379
+ try {
380
+ const conversationId = resolveOrThrow(rawId);
381
+ const archived = archiveConversation(conversationId);
382
+ if (archived) {
383
+ archivedIds.push(conversationId);
384
+ }
385
+ } catch (err) {
386
+ log.error(
387
+ { err, conversationId: rawId },
388
+ "POST /v1/conversations/archive/bulk: failed for conversation",
389
+ );
390
+ // Best-effort: continue with remaining conversations.
391
+ }
392
+ }
393
+
394
+ if (archivedIds.length > 0) {
395
+ publishConversationListChanged("reordered", originClientId);
396
+ }
397
+
398
+ return { ok: true, archived: archivedIds.length };
399
+ }
400
+
365
401
  /**
366
402
  * Set or clear the `surfacedAt` promotion marker on a conversation.
367
403
  *
@@ -695,6 +731,24 @@ export const ROUTES: RouteDefinition[] = [
695
731
  }),
696
732
  handler: handleUnarchiveConversation,
697
733
  },
734
+ {
735
+ operationId: "archiveConversationsBulk",
736
+ endpoint: "conversations/archive/bulk",
737
+ method: "POST",
738
+ policy: {
739
+ requiredScopes: ["chat.write"],
740
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
741
+ },
742
+ summary: "Bulk archive conversations",
743
+ description:
744
+ "Archive multiple conversations in one request. Emits a single sync invalidation for the entire batch.",
745
+ tags: ["conversations"],
746
+ requestBody: z.object({
747
+ conversationIds: z.array(z.string()).min(1),
748
+ }),
749
+ responseBody: z.object({ ok: z.boolean(), archived: z.number() }),
750
+ handler: handleArchiveConversationsBulk,
751
+ },
698
752
  {
699
753
  operationId: "surfaceConversation",
700
754
  endpoint: "conversations/:id/surface",
@@ -14,6 +14,7 @@
14
14
  * GET /v1/messages/:id/content — full message content
15
15
  * GET /v1/messages/:id/llm-context — LLM request logs for a message
16
16
  * GET /v1/llm-request-logs/:id/payload — raw payload for a single log
17
+ * GET /v1/llm-request-logs/:id/context — normalized context for a single log
17
18
  * DELETE /v1/messages/queued/:id — delete queued message
18
19
  * POST /v1/messages/queued/:id/steer — steer to a queued message
19
20
  */
@@ -21,6 +22,7 @@
21
22
  import { z } from "zod";
22
23
 
23
24
  import { LlmContextResponseSchema } from "../../api/responses/llm-context-response.js";
25
+ import { LLMRequestLogEntrySchema } from "../../api/responses/llm-request-log-entry.js";
24
26
  import {
25
27
  deepMergeOverwrite,
26
28
  fillContextDefaultsForMissingKeys,
@@ -236,7 +238,26 @@ function applyStoredProviderToLlmContextResult(
236
238
  return { ...normalized, summary };
237
239
  }
238
240
 
239
- function normalizeLlmContextLog(log: LogRow): LlmContextRouteResult & {
241
+ /**
242
+ * `full` returns the complete normalized entry including request/response
243
+ * sections; `summary` omits the sections so list responses stay small —
244
+ * sections for a single call are fetched lazily through
245
+ * `/v1/llm-request-logs/{logId}/context`.
246
+ */
247
+ type LlmContextView = "full" | "summary";
248
+
249
+ function resolveLlmContextView(view: string | undefined): LlmContextView {
250
+ if (view === undefined || view === "full") return "full";
251
+ if (view === "summary") return "summary";
252
+ throw new BadRequestError(
253
+ `Invalid view parameter: ${view}. Expected "full" or "summary".`,
254
+ );
255
+ }
256
+
257
+ function normalizeLlmContextLog(
258
+ log: LogRow,
259
+ view: LlmContextView = "full",
260
+ ): LlmContextRouteResult & {
240
261
  id: string;
241
262
  requestPayload: null;
242
263
  responsePayload: null;
@@ -283,6 +304,9 @@ function normalizeLlmContextLog(log: LogRow): LlmContextRouteResult & {
283
304
  // existing `agent_loop_exit_reason` column tells it WHICH error fired.
284
305
  callSite: log.callSite ?? null,
285
306
  ...result,
307
+ ...(view === "summary"
308
+ ? { requestSections: undefined, responseSections: undefined }
309
+ : {}),
286
310
  };
287
311
  }
288
312
 
@@ -898,11 +922,15 @@ function resolveConversationKind(
898
922
  return "user";
899
923
  }
900
924
 
901
- async function handleGetLlmContext({ pathParams = {} }: RouteHandlerArgs) {
925
+ async function handleGetLlmContext({
926
+ pathParams = {},
927
+ queryParams = {},
928
+ }: RouteHandlerArgs) {
902
929
  const messageId = pathParams.id;
903
930
  if (!messageId) {
904
931
  throw new BadRequestError("message id is required");
905
932
  }
933
+ const view = resolveLlmContextView(queryParams.view);
906
934
  const source = await getLlmRequestLogSource();
907
935
  const logs = await source.getRequestLogsByMessageId(messageId);
908
936
  const turnMessageIds = getAssistantMessageIdsInTurn(messageId);
@@ -932,7 +960,7 @@ async function handleGetLlmContext({ pathParams = {} }: RouteHandlerArgs) {
932
960
  messageId,
933
961
  conversationKind,
934
962
  conversationTotalEstimatedCostUsd,
935
- logs: logs.map(normalizeLlmContextLog),
963
+ logs: logs.map((log) => normalizeLlmContextLog(log, view)),
936
964
  memoryRecall: memoryRecallLog ?? null,
937
965
  memoryV2Activation: memoryV2Activation ?? null,
938
966
  memoryV3Selection,
@@ -944,6 +972,7 @@ async function handleGetConversationLlmContext({
944
972
  }: RouteHandlerArgs) {
945
973
  const conversationKey = queryParams.conversationKey;
946
974
  const requestedConversationId = queryParams.conversationId;
975
+ const view = resolveLlmContextView(queryParams.view);
947
976
 
948
977
  let conversationId: string | undefined = requestedConversationId;
949
978
  if (!conversationId && conversationKey) {
@@ -1000,7 +1029,7 @@ async function handleGetConversationLlmContext({
1000
1029
  conversationId: conversation.id,
1001
1030
  conversationKind,
1002
1031
  conversationTotalEstimatedCostUsd,
1003
- logs: logs.map(normalizeLlmContextLog),
1032
+ logs: logs.map((log) => normalizeLlmContextLog(log, view)),
1004
1033
  memoryRecall: null,
1005
1034
  memoryV2Activation: null,
1006
1035
  memoryV3Selection: null,
@@ -1034,6 +1063,21 @@ async function handleGetLlmRequestLogPayload({
1034
1063
  return { id: log.id, requestPayload, responsePayload };
1035
1064
  }
1036
1065
 
1066
+ async function handleGetLlmRequestLogContext({
1067
+ pathParams = {},
1068
+ }: RouteHandlerArgs) {
1069
+ const logId = pathParams.id;
1070
+ if (!logId) {
1071
+ throw new BadRequestError("log id is required");
1072
+ }
1073
+ const source = await getLlmRequestLogSource();
1074
+ const log = await source.getRequestLogById(logId);
1075
+ if (!log) {
1076
+ throw new NotFoundError("log not found");
1077
+ }
1078
+ return normalizeLlmContextLog(log);
1079
+ }
1080
+
1037
1081
  function handleDeleteQueuedMessage({
1038
1082
  queryParams = {},
1039
1083
  pathParams = {},
@@ -1322,6 +1366,13 @@ export const ROUTES: RouteDefinition[] = [
1322
1366
  schema: { type: "string" },
1323
1367
  description: "Internal conversation identifier.",
1324
1368
  },
1369
+ {
1370
+ name: "view",
1371
+ required: false,
1372
+ schema: { type: "string", enum: ["full", "summary"] },
1373
+ description:
1374
+ "Response shape. 'summary' omits per-log request/response sections; defaults to 'full'.",
1375
+ },
1325
1376
  ],
1326
1377
  responseBody: LlmContextResponseSchema,
1327
1378
  handler: handleGetConversationLlmContext,
@@ -1338,6 +1389,15 @@ export const ROUTES: RouteDefinition[] = [
1338
1389
  description:
1339
1390
  "Return request/response logs and memory recall data for a specific message.",
1340
1391
  tags: ["messages"],
1392
+ queryParams: [
1393
+ {
1394
+ name: "view",
1395
+ required: false,
1396
+ schema: { type: "string", enum: ["full", "summary"] },
1397
+ description:
1398
+ "Response shape. 'summary' omits per-log request/response sections; defaults to 'full'.",
1399
+ },
1400
+ ],
1341
1401
  responseBody: LlmContextResponseSchema,
1342
1402
  handler: handleGetLlmContext,
1343
1403
  },
@@ -1360,6 +1420,21 @@ export const ROUTES: RouteDefinition[] = [
1360
1420
  }),
1361
1421
  handler: handleGetLlmRequestLogPayload,
1362
1422
  },
1423
+ {
1424
+ operationId: "llm_request_logs_context_get",
1425
+ endpoint: "llm-request-logs/:id/context",
1426
+ method: "GET",
1427
+ policy: {
1428
+ requiredScopes: ["chat.read"],
1429
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
1430
+ },
1431
+ summary: "Get normalized context for a single LLM request log",
1432
+ description:
1433
+ "Return the normalized summary and request/response sections for a specific log entry.",
1434
+ tags: ["messages"],
1435
+ responseBody: LLMRequestLogEntrySchema,
1436
+ handler: handleGetLlmRequestLogContext,
1437
+ },
1363
1438
  {
1364
1439
  operationId: "messages_queued_delete",
1365
1440
  endpoint: "messages/queued/:id",
@@ -1,80 +1,30 @@
1
1
  /**
2
- * Gateway log tail route — gateway HTTP proxy.
2
+ * Gateway log tail route — gateway IPC proxy.
3
3
  *
4
- * The handler makes a single HTTP call to the gateway's log-tail API
5
- * and surfaces the body's `.error` message on non-OK responses.
4
+ * The handler calls the gateway over the local IPC socket so the assistant
5
+ * does not need gateway signing material.
6
6
  */
7
- import { z } from "zod";
7
+ import {
8
+ type GatewayLogsTailIpcResponse,
9
+ GatewayLogsTailIpcResponseSchema,
10
+ GatewayLogsTailRouteParamsSchema,
11
+ } from "@vellumai/gateway-client/gateway-ipc-contracts";
8
12
 
9
- import { getGatewayInternalBaseUrl } from "../../config/env.js";
13
+ import { ipcCallPersistent } from "../../ipc/gateway-client.js";
10
14
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
11
15
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
12
16
 
13
- // ── Shared helper ───────────────────────────────────────────────────────
14
-
15
- async function gatewayFetch(
16
- path: string,
17
- init?: RequestInit,
18
- ): Promise<unknown> {
19
- const base = getGatewayInternalBaseUrl();
20
- const res = await fetch(`${base}${path}`, init);
21
- if (!res.ok) {
22
- let message = `Gateway request failed (${res.status})`;
23
- try {
24
- const body = (await res.json()) as { error?: unknown };
25
- if (typeof body.error === "string") {
26
- message = body.error;
27
- }
28
- } catch {
29
- // ignore JSON parse failures
30
- }
31
- throw new Error(message);
32
- }
33
- return res.json();
34
- }
35
-
36
- // ── Schemas ─────────────────────────────────────────────────────────────
37
-
38
- const LEVEL_NAMES = [
39
- "trace",
40
- "debug",
41
- "info",
42
- "warn",
43
- "error",
44
- "fatal",
45
- ] as const;
46
-
47
- const GatewayLogsTailParams = z
48
- .object({
49
- n: z.coerce.number().int().min(1).max(1000).optional(),
50
- level: z.enum(LEVEL_NAMES).optional(),
51
- module: z.string().optional(),
52
- })
53
- .strict();
54
-
55
- const GatewayLogsTailResponseSchema = z.object({
56
- lines: z.array(z.record(z.string(), z.unknown())),
57
- truncated: z.boolean(),
58
- });
59
- type GatewayLogsTailResponse = z.infer<typeof GatewayLogsTailResponseSchema>;
60
-
61
17
  // ── Handlers ────────────────────────────────────────────────────────────
62
18
 
63
19
  async function handleGatewayLogsTail({
64
20
  queryParams = {},
65
21
  body = {},
66
- }: RouteHandlerArgs): Promise<GatewayLogsTailResponse> {
22
+ }: RouteHandlerArgs): Promise<GatewayLogsTailIpcResponse> {
67
23
  // HTTP GET delivers filters via queryParams; CLI IPC puts them in body.
68
24
  const source = Object.keys(queryParams).length > 0 ? queryParams : body;
69
- const p = GatewayLogsTailParams.parse(source);
70
- const qs = new URLSearchParams();
71
- if (p.n !== undefined) qs.set("n", String(p.n));
72
- if (p.level !== undefined) qs.set("level", p.level);
73
- if (p.module !== undefined) qs.set("module", p.module);
74
- const query = qs.toString();
75
- return gatewayFetch(
76
- `/v1/logs/tail${query ? `?${query}` : ""}`,
77
- ) as Promise<GatewayLogsTailResponse>;
25
+ const p = GatewayLogsTailRouteParamsSchema.parse(source);
26
+ const result = await ipcCallPersistent("gateway_logs_tail", p);
27
+ return GatewayLogsTailIpcResponseSchema.parse(result);
78
28
  }
79
29
 
80
30
  // ── Route definitions ───────────────────────────────────────────────────
@@ -93,7 +43,7 @@ export const ROUTES: RouteDefinition[] = [
93
43
  description:
94
44
  "Return the last N structured log entries from the gateway log files.",
95
45
  tags: ["gateway-logs"],
96
- responseBody: GatewayLogsTailResponseSchema,
46
+ responseBody: GatewayLogsTailIpcResponseSchema,
97
47
  queryParams: [
98
48
  {
99
49
  name: "n",
@@ -29,6 +29,7 @@ import {
29
29
  HomeFeedResponseSchema,
30
30
  } from "../../api/responses/home.js";
31
31
  import { patchFeedItemStatus, readHomeFeed } from "../../home/feed-writer.js";
32
+ import { revalidateHomeContentInBackground } from "../../home/home-content-refresh.js";
32
33
  import { getPersonalizedGreeting } from "../../home/home-greeting.js";
33
34
  import { getSuggestedPrompts } from "../../home/suggested-prompts.js";
34
35
  import {
@@ -139,6 +140,15 @@ export async function handleGetHomeFeed({
139
140
 
140
141
  const now = new Date();
141
142
 
143
+ // Stale-while-revalidate: serve whatever is cached right now and kick
144
+ // off a bounded background regeneration of any stale LLM content. The
145
+ // refresh publishes `home_feed_updated` when fresh content lands, so
146
+ // connected clients refetch and the personalized content swaps in.
147
+ // This is the accepted exception to GET-handler idempotency documented
148
+ // in `src/runtime/AGENTS.md` — the handler itself stays read-only and
149
+ // returns immediately with cached/fallback copy.
150
+ revalidateHomeContentInBackground();
151
+
142
152
  const personalizedGreeting = getPersonalizedGreeting();
143
153
  const suggestedPrompts = await getSuggestedPrompts();
144
154
 
@@ -25,7 +25,7 @@ import { getWorkspacePromptPath } from "../../util/platform.js";
25
25
  // Constants
26
26
  // ---------------------------------------------------------------------------
27
27
 
28
- const CACHE_TTL_MS = 2 * 24 * 60 * 60 * 1000; // 2 days
28
+ const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
29
29
 
30
30
  const CHECKPOINT_KEY_GREETINGS = "identity:intro:greetings";
31
31
  const CHECKPOINT_KEY_TIMESTAMP = "identity:intro:cached_at";
@@ -7,7 +7,6 @@ import { availableParallelism, cpus, totalmem } from "node:os";
7
7
 
8
8
  import { z } from "zod";
9
9
 
10
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
11
10
  import { getCpuLimit, getIsPlatform } from "../../config/env-registry.js";
12
11
  import { resolveCallSiteConfig } from "../../config/llm-resolver.js";
13
12
  import { getConfig } from "../../config/loader.js";
@@ -36,7 +35,7 @@ import {
36
35
  readWorkspaceIdentityIntro,
37
36
  setCachedIntro,
38
37
  } from "./identity-intro-cache.js";
39
- import type { RouteDefinition } from "./types.js";
38
+ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
40
39
 
41
40
  interface MemoryInfo {
42
41
  currentMb: number;
@@ -413,12 +412,12 @@ const FALLBACK_GREETINGS = [
413
412
  "Ready when you are.",
414
413
  ];
415
414
 
416
- const GENERATED_GREETING_LIMIT = 4;
415
+ const GENERATED_GREETING_LIMIT = 5;
417
416
  const GREETING_GENERATION_TIMEOUT_MS = 10_000;
418
417
  const GREETING_GENERATION_FAILURE_COOLDOWN_MS = 60_000;
419
418
  const EMPTY_STATE_GREETING_CALLSITE = "emptyStateGreeting" as const;
420
- const EMPTY_STATE_DYNAMIC_GREETINGS_FLAG =
421
- "empty-state-dynamic-greetings" as const;
419
+ const EXPLICIT_TIME_OF_DAY_PATTERN =
420
+ /\b(?:morning|afternoon|evening|tonight|midnight|noon|sunrise|sunset)\b/i;
422
421
 
423
422
  type IdentityIntroSource = "workspace" | "cache" | "fallback";
424
423
 
@@ -445,21 +444,18 @@ function identityIntroResponse(
445
444
  };
446
445
  }
447
446
 
448
- function getIdentityIntro(): IdentityIntroResponse {
447
+ function getIdentityIntro({
448
+ queryParams = {},
449
+ }: RouteHandlerArgs = {}): IdentityIntroResponse {
450
+ const localTimeContext = buildLocalTimeContext(queryParams);
451
+
449
452
  // 1. User-defined greetings from SOUL.md `## Greetings`
450
453
  const workspaceGreetings = readWorkspaceGreetings();
451
454
  if (workspaceGreetings) {
452
455
  return identityIntroResponse(workspaceGreetings, "workspace");
453
456
  }
454
457
 
455
- const config = getConfig();
456
- if (
457
- !isAssistantFeatureFlagEnabled(EMPTY_STATE_DYNAMIC_GREETINGS_FLAG, config)
458
- ) {
459
- return identityIntroResponse(FALLBACK_GREETINGS, "fallback");
460
- }
461
-
462
- // 2. Cached LLM-generated greetings (populated by background refresh)
458
+ // 2. Cached LLM-generated greetings
463
459
  const cached = getCachedIntro();
464
460
  if (cached) {
465
461
  return identityIntroResponse(cached.greetings, "cache");
@@ -471,7 +467,7 @@ function getIdentityIntro(): IdentityIntroResponse {
471
467
  if (identityIntro) {
472
468
  // Still trigger background generation so the next request gets
473
469
  // LLM-generated greetings instead of the static tagline.
474
- const refreshing = triggerEmptyStateGreetingGeneration();
470
+ const refreshing = triggerEmptyStateGreetingGeneration(localTimeContext);
475
471
  return identityIntroResponse(
476
472
  [identityIntro, ...FALLBACK_GREETINGS],
477
473
  "workspace",
@@ -480,13 +476,50 @@ function getIdentityIntro(): IdentityIntroResponse {
480
476
  }
481
477
 
482
478
  // 4. Trigger fresh generation without blocking the empty-state UI.
483
- const refreshing = triggerEmptyStateGreetingGeneration();
479
+ const refreshing = triggerEmptyStateGreetingGeneration(localTimeContext);
484
480
 
485
481
  // 5. Generic fallback only when generation is unavailable.
486
482
  return identityIntroResponse(FALLBACK_GREETINGS, "fallback", refreshing);
487
483
  }
488
484
 
489
- function triggerEmptyStateGreetingGeneration(): boolean {
485
+ function buildLocalTimeContext(
486
+ queryParams: Record<string, string>,
487
+ ): string | null {
488
+ const rawHour = queryParams.localHour;
489
+ const rawMinute = queryParams.localMinute;
490
+ if (rawHour === undefined || rawMinute === undefined) {
491
+ return null;
492
+ }
493
+
494
+ const hour = Number(rawHour);
495
+ const minute = Number(rawMinute);
496
+ if (
497
+ !Number.isInteger(hour) ||
498
+ !Number.isInteger(minute) ||
499
+ hour < 0 ||
500
+ hour > 23 ||
501
+ minute < 0 ||
502
+ minute > 59
503
+ ) {
504
+ return null;
505
+ }
506
+
507
+ const period =
508
+ hour >= 5 && hour < 12
509
+ ? "morning"
510
+ : hour >= 12 && hour < 17
511
+ ? "afternoon"
512
+ : hour >= 17 && hour < 21
513
+ ? "evening"
514
+ : "late night";
515
+ const paddedHour = String(hour).padStart(2, "0");
516
+ const paddedMinute = String(minute).padStart(2, "0");
517
+ return `${period} (${paddedHour}:${paddedMinute})`;
518
+ }
519
+
520
+ function triggerEmptyStateGreetingGeneration(
521
+ localTimeContext: string | null,
522
+ ): boolean {
490
523
  if (greetingGenerationInFlight) {
491
524
  return true;
492
525
  }
@@ -501,7 +534,7 @@ function triggerEmptyStateGreetingGeneration(): boolean {
501
534
 
502
535
  greetingGenerationInFlight = new Promise<void>((resolve) => {
503
536
  queueMicrotask(() => {
504
- void generateEmptyStateGreetings()
537
+ void generateEmptyStateGreetings(localTimeContext)
505
538
  .then((greetings) => {
506
539
  lastGreetingGenerationFailureAt = greetings === null ? Date.now() : 0;
507
540
  })
@@ -515,7 +548,9 @@ function triggerEmptyStateGreetingGeneration(): boolean {
515
548
  return true;
516
549
  }
517
550
 
518
- async function generateEmptyStateGreetings(): Promise<string[] | null> {
551
+ async function generateEmptyStateGreetings(
552
+ localTimeContext: string | null,
553
+ ): Promise<string[] | null> {
519
554
  try {
520
555
  const provider = await getConfiguredProvider(EMPTY_STATE_GREETING_CALLSITE);
521
556
  if (!provider) {
@@ -530,12 +565,18 @@ async function generateEmptyStateGreetings(): Promise<string[] | null> {
530
565
  excludeBootstrap: true,
531
566
  excludeCustomPrefix: true,
532
567
  });
568
+ const localTimeInstruction = localTimeContext
569
+ ? ` Current user-local time for subtle tone only: ${localTimeContext}.`
570
+ : "";
533
571
  const result = await runBtwSidechain({
534
572
  content:
535
573
  `Generate ${GENERATED_GREETING_LIMIT} short first-person greeting options for the empty new-chat screen. ` +
536
574
  "Use the assistant identity, voice, and relationship guidance from IDENTITY.md and SOUL.md. " +
537
575
  "Each greeting should feel personal and inviting, not like a generic assistant introduction. " +
538
- "Return only a JSON array of strings. No markdown, keys, or explanation.",
576
+ "Return only a JSON array of strings. No markdown, keys, or explanation. " +
577
+ "Generated greetings are cached for 4 hours, so do not mention the current time " +
578
+ "or use explicit time-of-day words like morning, afternoon, evening, or tonight." +
579
+ localTimeInstruction,
539
580
  provider,
540
581
  systemPrompt,
541
582
  messages: [],
@@ -601,6 +642,7 @@ function normalizeGeneratedGreetings(values: unknown[]): string[] {
601
642
  .replace(/^["'`]+|["'`]+$/g, "")
602
643
  .trim();
603
644
  if (!greeting) continue;
645
+ if (EXPLICIT_TIME_OF_DAY_PATTERN.test(greeting)) continue;
604
646
 
605
647
  const key = greeting.toLowerCase();
606
648
  if (seen.has(key)) continue;
@@ -758,6 +800,20 @@ export const ROUTES: RouteDefinition[] = [
758
800
  description:
759
801
  "Returns greetings sourced from SOUL.md, the generated cache, or generic fallbacks while background generation refreshes the cache.",
760
802
  tags: ["identity"],
803
+ queryParams: [
804
+ {
805
+ name: "localHour",
806
+ schema: { type: "integer", minimum: 0, maximum: 23 },
807
+ description:
808
+ "Optional client-local hour of day used only when refreshing generated greetings.",
809
+ },
810
+ {
811
+ name: "localMinute",
812
+ schema: { type: "integer", minimum: 0, maximum: 59 },
813
+ description:
814
+ "Optional client-local minute used only when refreshing generated greetings.",
815
+ },
816
+ ],
761
817
  responseBody: z.object({
762
818
  greetings: z.array(z.string()),
763
819
  text: z.string(),