@vellumai/assistant 0.8.11 → 0.8.12-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +126 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-usage-store.test.ts +11 -0
  37. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  38. package/src/__tests__/model-intents.test.ts +1 -1
  39. package/src/__tests__/oauth-cli.test.ts +19 -8
  40. package/src/__tests__/openai-provider.test.ts +34 -0
  41. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  42. package/src/__tests__/recurrence-engine.test.ts +45 -0
  43. package/src/__tests__/schedule-routes.test.ts +34 -0
  44. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  45. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  46. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  47. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  48. package/src/__tests__/subagent-tools.test.ts +1 -0
  49. package/src/__tests__/system-prompt.test.ts +109 -11
  50. package/src/__tests__/tool-error-hook.test.ts +1 -0
  51. package/src/__tests__/tool-result-spool.test.ts +337 -0
  52. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  53. package/src/__tests__/validate-input.test.ts +95 -1
  54. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  55. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  56. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  57. package/src/agent/loop.ts +81 -24
  58. package/src/api/events/usage-progress.ts +28 -0
  59. package/src/api/index.ts +6 -0
  60. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  61. package/src/bundler/app-bundler.ts +25 -42
  62. package/src/calls/call-controller.ts +1 -1
  63. package/src/cli/commands/plugins.ts +248 -15
  64. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  65. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  66. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  67. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  68. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  69. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
  70. package/src/cli/lib/inspect-plugin.ts +252 -0
  71. package/src/cli/lib/install-from-github.ts +214 -21
  72. package/src/cli/lib/list-installed-plugins.ts +17 -6
  73. package/src/cli/lib/plugin-artifact.ts +103 -0
  74. package/src/cli/lib/plugin-details.ts +18 -1
  75. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  76. package/src/cli/lib/upgrade-plugin.ts +219 -0
  77. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  78. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  79. package/src/config/call-site-defaults.ts +13 -2
  80. package/src/config/feature-flag-registry.json +8 -16
  81. package/src/config/loader.ts +52 -59
  82. package/src/config/schema.ts +0 -2
  83. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  84. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  85. package/src/config/schemas/llm.ts +10 -0
  86. package/src/config/schemas/memory-v2.ts +13 -0
  87. package/src/config/schemas/memory-v3.ts +92 -0
  88. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  89. package/src/context/tool-result-spool.ts +104 -0
  90. package/src/credential-execution/feature-gates.ts +0 -1
  91. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  92. package/src/daemon/conversation-error.ts +6 -15
  93. package/src/daemon/conversation.ts +9 -0
  94. package/src/daemon/disk-pressure-policy.ts +0 -1
  95. package/src/daemon/lifecycle.ts +1 -20
  96. package/src/daemon/message-types/conversations.ts +2 -15
  97. package/src/daemon/trust-context.ts +1 -1
  98. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  99. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  100. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  101. package/src/home/home-content-refresh.ts +43 -31
  102. package/src/home/home-greeting-cache.ts +8 -1
  103. package/src/home/home-greeting.ts +13 -9
  104. package/src/home/suggested-prompts.ts +77 -24
  105. package/src/ipc/routes/trust-rules.test.ts +66 -72
  106. package/src/media/image-credentials.ts +2 -2
  107. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  108. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  109. package/src/memory/conversation-attention-store.ts +1 -0
  110. package/src/memory/conversation-bootstrap.ts +18 -9
  111. package/src/memory/conversation-crud.ts +12 -2
  112. package/src/memory/conversation-title-service.ts +53 -9
  113. package/src/memory/delivery-channels.ts +0 -69
  114. package/src/memory/graph/extraction-job.ts +0 -15
  115. package/src/memory/guardian-action-store.ts +1 -376
  116. package/src/memory/llm-usage-store.ts +5 -1
  117. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  119. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  120. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  121. package/src/memory/v2/consolidation-job.ts +93 -9
  122. package/src/memory/v2/injection.ts +53 -0
  123. package/src/memory/v2/prompts/consolidation.ts +1 -0
  124. package/src/memory/v2/static-context.ts +13 -1
  125. package/src/memory/v2/sweep-job.ts +1 -1
  126. package/src/memory/v2/types.ts +5 -0
  127. package/src/plugin-api/types.ts +7 -0
  128. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  129. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  130. package/src/plugins/defaults/index.ts +25 -0
  131. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  132. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  133. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  134. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  135. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  136. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  137. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  138. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  139. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  140. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  141. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  142. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  143. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  144. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  145. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  146. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  147. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  148. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  149. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  150. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  151. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  152. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  153. package/src/prompts/cache-boundary.ts +17 -0
  154. package/src/prompts/sections.ts +50 -17
  155. package/src/prompts/system-prompt.ts +12 -4
  156. package/src/prompts/templates/system-sections.ts +22 -0
  157. package/src/providers/anthropic/client.ts +74 -28
  158. package/src/providers/gemini/client.ts +5 -1
  159. package/src/providers/minimax/client.ts +9 -0
  160. package/src/providers/model-intents.ts +2 -2
  161. package/src/providers/openai/chat-completions-provider.ts +2 -1
  162. package/src/providers/openai/responses-provider.ts +5 -1
  163. package/src/providers/retry.ts +8 -0
  164. package/src/providers/types.ts +11 -0
  165. package/src/runtime/AGENTS.md +6 -0
  166. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  167. package/src/runtime/agent-wake.ts +5 -5
  168. package/src/runtime/background-job-runner.ts +2 -2
  169. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  170. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  171. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  172. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  173. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  174. package/src/runtime/pre-first-message-gate.ts +1 -1
  175. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  176. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  177. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  178. package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
  179. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  180. package/src/runtime/routes/btw-routes.ts +0 -14
  181. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  182. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  183. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  184. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  185. package/src/runtime/routes/home-feed-routes.ts +10 -0
  186. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  187. package/src/runtime/routes/identity-routes.ts +76 -20
  188. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  189. package/src/runtime/routes/plugins-routes.ts +21 -0
  190. package/src/runtime/routes/schedule-routes.ts +19 -2
  191. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  192. package/src/schedule/recurrence-engine.ts +34 -0
  193. package/src/schedule/scheduler.ts +1 -0
  194. package/src/skills/validate-input.ts +41 -1
  195. package/src/subagent/types.ts +26 -1
  196. package/src/telemetry/types.ts +15 -1
  197. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  198. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  199. package/src/tools/apps/executors.ts +1 -1
  200. package/src/tools/skills/skill-tool-factory.ts +19 -8
  201. package/src/usage/types.ts +8 -1
  202. package/src/util/platform.ts +16 -0
  203. package/src/watcher/engine.ts +1 -0
  204. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  205. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  206. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  207. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  210. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  211. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  212. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  213. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  214. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  215. package/src/config/schemas/updates.ts +0 -14
  216. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  217. package/src/memory/conversation-starters-cadence.ts +0 -78
  218. package/src/prompts/update-bulletin-job.ts +0 -180
  219. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -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",
@@ -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(),
@@ -48,10 +48,6 @@ import {
48
48
  updateMessageContent,
49
49
  updateMessageMetadata,
50
50
  } from "../../memory/conversation-crud.js";
51
- import {
52
- clearPendingVerificationReply,
53
- getPendingVerificationReply,
54
- } from "../../memory/delivery-channels.js";
55
51
  import {
56
52
  clearPayload,
57
53
  findMessageBySourceId,
@@ -629,38 +625,6 @@ export async function handleChannelInbound({
629
625
 
630
626
  const replyCallbackUrl = body.replyCallbackUrl;
631
627
 
632
- // ── Retry pending verification reply on duplicate ──
633
- // If a previous verification delivery failed and stored a pending reply,
634
- // gateway retries (duplicates) re-attempt delivery here. On success the
635
- // pending marker is cleared so further duplicates short-circuit normally.
636
- if (result.duplicate && replyCallbackUrl) {
637
- const pendingReply = getPendingVerificationReply(result.eventId);
638
- if (pendingReply) {
639
- try {
640
- await deliverChannelReply(replyCallbackUrl, {
641
- chatId: pendingReply.chatId,
642
- text: pendingReply.text,
643
- assistantId: pendingReply.assistantId,
644
- });
645
- clearPendingVerificationReply(result.eventId);
646
- log.info(
647
- { eventId: result.eventId },
648
- "Retried pending verification reply: delivered",
649
- );
650
- } catch (retryErr) {
651
- log.error(
652
- { err: retryErr, eventId: result.eventId },
653
- "Retry of pending verification reply failed; will retry on next duplicate",
654
- );
655
- }
656
- return {
657
- accepted: true,
658
- duplicate: true,
659
- eventId: result.eventId,
660
- };
661
- }
662
- }
663
-
664
628
  // external_conversation_bindings is assistant-agnostic. Restrict writes to
665
629
  // self so assistant-scoped legacy routes do not overwrite each other's
666
630
  // channel binding metadata for the same chat.
@@ -191,6 +191,27 @@ const pluginDetailsResponseSchema = z.object({
191
191
  ref: z
192
192
  .string()
193
193
  .describe("Git ref the catalog metadata / README were resolved at."),
194
+ artifact: z
195
+ .object({
196
+ url: z
197
+ .string()
198
+ .describe("HTTPS URL the prebuilt client artifact is downloaded from."),
199
+ sha256: z
200
+ .string()
201
+ .describe(
202
+ "Lowercase 64-char hex SHA-256 the download is verified against.",
203
+ ),
204
+ label: z
205
+ .string()
206
+ .optional()
207
+ .describe(
208
+ 'Optional human label for the download (e.g. "Download for macOS"); absent when the plugin doesn\'t name it.',
209
+ ),
210
+ })
211
+ .nullable()
212
+ .describe(
213
+ "Prebuilt client artifact from `package.json` `vellum.artifact`, or null when the plugin ships none or its descriptor is incomplete (e.g. a placeholder sha256).",
214
+ ),
194
215
  });
195
216
 
196
217
  const pluginInstallRequestSchema = z.object({
@@ -12,6 +12,10 @@ import { INTERNAL_GUARDIAN_TRUST_CONTEXT } from "../../daemon/trust-context.js";
12
12
  import { bootstrapConversation } from "../../memory/conversation-bootstrap.js";
13
13
  import { getConversation } from "../../memory/conversation-crud.js";
14
14
  import { getUsageCostForConversationWindow } from "../../memory/llm-usage-store.js";
15
+ import {
16
+ describeRRuleExpression,
17
+ isSingleFireRRule,
18
+ } from "../../schedule/recurrence-engine.js";
15
19
  import { normalizeScheduleSyntax } from "../../schedule/recurrence-types.js";
16
20
  import {
17
21
  runScript,
@@ -136,7 +140,20 @@ function getCadenceDescription(
136
140
  if (job.syntax === "cron") {
137
141
  return describeCronExpression(job.cronExpression);
138
142
  }
139
- return job.expression ?? "";
143
+ return describeRRuleExpression(job.cronExpression);
144
+ }
145
+
146
+ /**
147
+ * Presentation-layer one-shot flag. A COUNT=1 rrule fires exactly once and
148
+ * should read as one-time in clients, even though the scheduler internally
149
+ * treats expression-backed jobs as recurring (retry policy, conversation
150
+ * reuse). Do not feed this back into scheduler logic.
151
+ */
152
+ function isOneShotForDisplay(
153
+ job: Pick<ScheduleJob, "syntax" | "cronExpression">,
154
+ ): boolean {
155
+ if (job.cronExpression == null) return true;
156
+ return job.syntax === "rrule" && isSingleFireRRule(job.cronExpression);
140
157
  }
141
158
 
142
159
  function handleListSchedules(queryParams: Record<string, string>) {
@@ -182,7 +199,7 @@ function handleListSchedules(queryParams: Record<string, string>) {
182
199
  routingIntent: j.routingIntent,
183
200
  reuseConversation: j.reuseConversation,
184
201
  wakeConversationId: j.wakeConversationId,
185
- isOneShot: j.cronExpression == null,
202
+ isOneShot: isOneShotForDisplay(j),
186
203
  };
187
204
  }),
188
205
  };