@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321

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 (209) hide show
  1. package/Dockerfile +7 -0
  2. package/README.md +12 -12
  3. package/docker/mousepad/90-vellum.gschema.override +4 -0
  4. package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
  5. package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
  6. package/openapi.yaml +97 -4
  7. package/package.json +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +4 -2
  9. package/src/__tests__/call-routes-http.test.ts +4 -2
  10. package/src/__tests__/channel-readiness-discord.test.ts +8 -0
  11. package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +1 -0
  13. package/src/__tests__/conversation-pairing.test.ts +10 -0
  14. package/src/__tests__/conversation-resurface-done.test.ts +289 -0
  15. package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
  16. package/src/__tests__/gemini-provider.test.ts +31 -0
  17. package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
  18. package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
  19. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  20. package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
  21. package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
  22. package/src/__tests__/platform-callback-registration.test.ts +46 -1
  23. package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
  24. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  25. package/src/__tests__/pricing.test.ts +33 -0
  26. package/src/__tests__/provider-error-scenarios.test.ts +46 -0
  27. package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
  28. package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
  29. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  30. package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
  31. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
  32. package/src/__tests__/send-endpoint-busy.test.ts +4 -2
  33. package/src/__tests__/skill-load-inline-command.test.ts +35 -0
  34. package/src/__tests__/skill-load-tool.test.ts +42 -11
  35. package/src/__tests__/skill-tool-factory.test.ts +50 -2
  36. package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
  37. package/src/__tests__/validate-input.test.ts +73 -0
  38. package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
  39. package/src/api/ask-question-tool.ts +50 -0
  40. package/src/api/events/tool-result.ts +11 -0
  41. package/src/api/index.ts +7 -0
  42. package/src/api/requests/dictation.ts +2 -2
  43. package/src/api/responses/home.ts +27 -0
  44. package/src/avatar/notification-avatar.test.ts +42 -34
  45. package/src/avatar/notification-avatar.ts +1 -0
  46. package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
  47. package/src/calls/voice-escalation-judge.ts +6 -5
  48. package/src/calls/voice-screen-annotation.ts +44 -0
  49. package/src/calls/voice-session-bridge.ts +22 -0
  50. package/src/cli/commands/__tests__/plugins.test.ts +29 -1
  51. package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
  52. package/src/cli/commands/platform/index.help.ts +2 -1
  53. package/src/cli/commands/platform/index.ts +8 -2
  54. package/src/cli/commands/plugins.ts +16 -0
  55. package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
  56. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
  57. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
  58. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
  59. package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
  60. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
  61. package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
  62. package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
  63. package/src/cli/lib/bundled-marketplace.json +4 -4
  64. package/src/cli/lib/inspect-plugin.ts +5 -0
  65. package/src/cli/lib/plugin-catalog-cache.ts +41 -4
  66. package/src/cli/lib/plugin-catalog-local.ts +9 -0
  67. package/src/cli/lib/plugin-catalog-platform.ts +48 -9
  68. package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
  69. package/src/cli/lib/plugin-details.ts +10 -7
  70. package/src/cli/lib/search-plugins.ts +2 -0
  71. package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
  72. package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
  73. package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
  74. package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
  75. package/src/config/call-site-defaults.ts +17 -11
  76. package/src/config/default-profile-catalog.ts +118 -19
  77. package/src/config/default-profile-names.ts +69 -0
  78. package/src/config/feature-flag-registry.json +25 -9
  79. package/src/config/inference-profile-validation.ts +7 -6
  80. package/src/config/llm-resolver.ts +16 -0
  81. package/src/config/profile-text-generation.test.ts +9 -0
  82. package/src/config/schemas/call-site-catalog.ts +8 -1
  83. package/src/config/schemas/llm.ts +70 -9
  84. package/src/config/sidebar-done-gate.ts +27 -0
  85. package/src/config/sync-gated-profiles.ts +96 -44
  86. package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
  87. package/src/daemon/auto-profile-router.ts +220 -0
  88. package/src/daemon/conversation-agent-loop.ts +77 -4
  89. package/src/daemon/conversation-slash.ts +2 -2
  90. package/src/daemon/conversation-turn-finalize.ts +12 -0
  91. package/src/daemon/dictation-text-processing.ts +4 -4
  92. package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
  93. package/src/daemon/handlers/config-channels.ts +3 -1
  94. package/src/daemon/handlers/conversation-history.ts +11 -1
  95. package/src/daemon/handlers/conversations.ts +118 -10
  96. package/src/daemon/pkb-reminder-builder.test.ts +10 -3
  97. package/src/daemon/pkb-reminder-builder.ts +1 -1
  98. package/src/desktop/desktop-browser-endpoint.ts +1 -0
  99. package/src/home/feed-types.ts +2 -0
  100. package/src/inbound/platform-callback-registration.ts +19 -6
  101. package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
  102. package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
  103. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
  104. package/src/live-voice/__tests__/protocol.test.ts +22 -0
  105. package/src/live-voice/continuation-judge.ts +1 -1
  106. package/src/live-voice/live-voice-session.ts +7 -6
  107. package/src/live-voice/protocol.ts +16 -5
  108. package/src/messaging/providers/gmail/client.ts +4 -0
  109. package/src/notifications/README.md +11 -3
  110. package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
  111. package/src/notifications/conversation-pairing.ts +23 -8
  112. package/src/notifications/home-feed-mirror.ts +39 -0
  113. package/src/notifications/home-feed-side-effect.ts +46 -19
  114. package/src/oauth/AGENTS.md +7 -1
  115. package/src/permissions/question-prompter.ts +14 -4
  116. package/src/persistence/conversation-crud.ts +114 -3
  117. package/src/persistence/conversation-queries.ts +48 -3
  118. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
  119. package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
  120. package/src/persistence/embeddings/embedding-backend.ts +23 -41
  121. package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
  122. package/src/persistence/embeddings/embedding-gemini.ts +3 -1
  123. package/src/persistence/embeddings/embedding-local.ts +72 -17
  124. package/src/persistence/embeddings/embedding-types.ts +0 -19
  125. package/src/persistence/jobs-store.ts +229 -0
  126. package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
  127. package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
  128. package/src/persistence/steps.ts +12 -0
  129. package/src/platform/sync-avatar.test.ts +29 -0
  130. package/src/plugin-api/model-profiles.ts +5 -3
  131. package/src/plugins/defaults/memory/AGENTS.md +5 -3
  132. package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
  133. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
  134. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
  135. package/src/plugins/defaults/memory/graph/tools.ts +9 -5
  136. package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
  137. package/src/plugins/defaults/memory/job-handlers.ts +8 -2
  138. package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
  139. package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
  140. package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
  141. package/src/plugins/defaults/memory/tools.test.ts +17 -0
  142. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
  143. package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
  144. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  145. package/src/providers/anthropic/client.ts +2 -2
  146. package/src/providers/gemini/client.ts +17 -1
  147. package/src/providers/inference/auth.ts +25 -0
  148. package/src/providers/jev/ask.test.ts +66 -0
  149. package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
  150. package/src/providers/malformed-tool-call.ts +55 -0
  151. package/src/providers/model-catalog.ts +257 -4
  152. package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
  153. package/src/providers/openai/chat-completions-provider.ts +30 -0
  154. package/src/providers/openai/codex-models.ts +2 -0
  155. package/src/providers/platform-proxy/constants.ts +5 -0
  156. package/src/providers/retry.ts +68 -36
  157. package/src/providers/search-provider-catalog.ts +10 -0
  158. package/src/providers/vellum-model-routing.test.ts +1 -0
  159. package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
  160. package/src/runtime/__tests__/question-resolution.test.ts +32 -0
  161. package/src/runtime/agent-wake.ts +9 -2
  162. package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
  163. package/src/runtime/auth/middleware.ts +15 -11
  164. package/src/runtime/middleware/auth.ts +7 -1
  165. package/src/runtime/pending-interactions.ts +8 -8
  166. package/src/runtime/question-resolution.ts +54 -8
  167. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
  168. package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
  169. package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
  170. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
  171. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
  172. package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
  173. package/src/runtime/routes/conversation-list-routes.ts +16 -14
  174. package/src/runtime/routes/conversation-query-routes.ts +4 -0
  175. package/src/runtime/routes/conversation-routes.ts +4 -2
  176. package/src/runtime/routes/diagnostics-routes.ts +66 -229
  177. package/src/runtime/routes/global-search-routes.ts +22 -7
  178. package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
  179. package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
  180. package/src/runtime/routes/platform-routes.ts +6 -1
  181. package/src/runtime/routes/plugins-routes.ts +5 -2
  182. package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
  183. package/src/runtime/sync/resource-sync-events.ts +11 -0
  184. package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
  185. package/src/runtime/sync/worker-daemon-notify.ts +44 -0
  186. package/src/runtime/verification-outbound-actions.ts +2 -2
  187. package/src/skills/validate-input.ts +75 -11
  188. package/src/telegram/__tests__/webhook-health.test.ts +8 -0
  189. package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
  190. package/src/tools/ask-question/ask-question-tool.ts +12 -1
  191. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
  192. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
  193. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
  194. package/src/tools/network/__tests__/web-search.test.ts +96 -0
  195. package/src/tools/network/web-fetch.ts +30 -9
  196. package/src/tools/network/web-search.ts +235 -4
  197. package/src/tools/skills/execute.ts +1 -1
  198. package/src/tools/skills/format-input-schema.ts +15 -0
  199. package/src/tools/skills/load.ts +15 -43
  200. package/src/tools/skills/scaffold-managed.ts +86 -71
  201. package/src/tools/skills/skill-tool-factory.ts +2 -1
  202. package/src/tools/workflows/manage-workflows.ts +4 -2
  203. package/src/util/worker-pipe.test.ts +146 -0
  204. package/src/util/worker-pipe.ts +49 -0
  205. package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
  206. package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
  207. package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
@@ -32,6 +32,7 @@ import type { ChannelId, InterfaceId } from "../channels/types.js";
32
32
  import { parseChannelId, parseInterfaceId } from "../channels/types.js";
33
33
  import { CHANNEL_IDS, isChannelId } from "../channels/types.js";
34
34
  import { getConfig } from "../config/loader.js";
35
+ import { isSidebarDoneEnabled } from "../config/sidebar-done-gate.js";
35
36
  import { findDisplayTurnEndIndex } from "../conversations/message-consolidation.js";
36
37
  import { findConversation } from "../daemon/conversation-registry.js";
37
38
  import { conversationMetadataSyncTag } from "../daemon/message-types/sync.js";
@@ -48,6 +49,7 @@ import { indexMessageNow } from "../plugins/defaults/memory/indexer.js";
48
49
  import { runHook } from "../plugins/pipeline.js";
49
50
  import type { ContentBlock } from "../providers/types.js";
50
51
  import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
52
+ import { publishConversationListAndMetadataChanged } from "../runtime/sync/resource-sync-events.js";
51
53
  import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
52
54
  import { UserError } from "../util/errors.js";
53
55
  import { getLogger } from "../util/logger.js";
@@ -100,6 +102,7 @@ import {
100
102
  computerUseScreenshotAttachmentIdsFromMetadata,
101
103
  type ConversationCreateType,
102
104
  type ConversationOrigin,
105
+ isEchoSuppressedUserMessage,
103
106
  isHiddenMessageMetadata,
104
107
  isNoResponseMetadata,
105
108
  isReactionMessageMetadata,
@@ -942,6 +945,11 @@ interface InsertMessageCoreParams {
942
945
  /** Answered synchronously at the top of every insert attempt. See
943
946
  * {@link AddMessageOptions.insertPrecondition}. */
944
947
  insertPrecondition?: () => boolean;
948
+ /** The row is a reservation booked ahead of the content that will fill it
949
+ * ({@link reserveMessage}), so it carries nothing to read yet. */
950
+ reserved?: boolean;
951
+ /** See {@link AddMessageOptions.skipResurface}. */
952
+ skipResurface?: boolean;
945
953
  }
946
954
 
947
955
  /**
@@ -1029,6 +1037,8 @@ async function insertMessageCore(
1029
1037
  clientMessageId,
1030
1038
  id,
1031
1039
  insertPrecondition,
1040
+ reserved,
1041
+ skipResurface,
1032
1042
  } = params;
1033
1043
  warnOnModelInvisibleContent(content, conversationId);
1034
1044
  const db = getDb();
@@ -1054,7 +1064,7 @@ async function insertMessageCore(
1054
1064
 
1055
1065
  // The timestamp is recomputed each attempt so a late retry doesn't persist a
1056
1066
  // stale `updatedAt`.
1057
- return withSqliteRetry(
1067
+ const inserted = await withSqliteRetry(
1058
1068
  (): InsertedMessage => {
1059
1069
  // Asked at the top of EVERY attempt, and synchronously, because that is
1060
1070
  // the scope the answer holds for. Contention retries this function after
@@ -1179,6 +1189,32 @@ async function insertMessageCore(
1179
1189
  },
1180
1190
  { op: "insertMessageCore", context: { conversationId } },
1181
1191
  );
1192
+
1193
+ // A message the user reads brings a Done conversation back to the list.
1194
+ // Four kinds of insert are not that: the echo-suppressed set, which never
1195
+ // renders in the transcript; the empty row `reserveMessage` books for an LLM
1196
+ // call that has not run yet, whose content arrives at the finalize seam and
1197
+ // resurfaces from there; a row whose producer declared it bookkeeping ABOUT
1198
+ // the conversation rather than activity in it (`skipResurface`); and a
1199
+ // deduplicated insert, which wrote no row at all. Best-effort: a failure
1200
+ // here must not escalate into a failed persist.
1201
+ if (
1202
+ !inserted.deduplicated &&
1203
+ !reserved &&
1204
+ !skipResurface &&
1205
+ !isEchoSuppressedUserMessage(metadata)
1206
+ ) {
1207
+ try {
1208
+ resurfaceArchivedConversation(conversationId, inserted.createdAt);
1209
+ } catch (err) {
1210
+ log.warn(
1211
+ { err, conversationId, messageId: inserted.id },
1212
+ "Failed to resurface Done conversation after message insert",
1213
+ );
1214
+ }
1215
+ }
1216
+
1217
+ return inserted;
1182
1218
  }
1183
1219
 
1184
1220
  /**
@@ -2653,6 +2689,17 @@ export interface AddMessageOptions {
2653
2689
  * during that sleep.
2654
2690
  */
2655
2691
  insertPrecondition?: () => boolean;
2692
+ /**
2693
+ * The row is the assistant's bookkeeping ABOUT this conversation rather than
2694
+ * activity in it, so it must not bring the conversation back from Done (see
2695
+ * {@link resurfaceArchivedConversation}). The producer declares this, because
2696
+ * only the producer knows which of the two a row is.
2697
+ *
2698
+ * The retrospective skill card is the case: a pass runs once a conversation
2699
+ * has gone idle, which is exactly when the user has just marked it done, so
2700
+ * a card that resurfaced would bounce fresh done chats back into the sidebar.
2701
+ */
2702
+ skipResurface?: boolean;
2656
2703
  }
2657
2704
 
2658
2705
  /**
@@ -2666,8 +2713,14 @@ export async function addMessage(
2666
2713
  content: string,
2667
2714
  options?: AddMessageOptions,
2668
2715
  ) {
2669
- const { metadata, skipIndexing, clientMessageId, id, insertPrecondition } =
2670
- options ?? {};
2716
+ const {
2717
+ metadata,
2718
+ skipIndexing,
2719
+ clientMessageId,
2720
+ id,
2721
+ insertPrecondition,
2722
+ skipResurface,
2723
+ } = options ?? {};
2671
2724
  const inserted = await insertMessageCore({
2672
2725
  conversationId,
2673
2726
  role,
@@ -2676,6 +2729,7 @@ export async function addMessage(
2676
2729
  clientMessageId,
2677
2730
  id,
2678
2731
  ...(insertPrecondition ? { insertPrecondition } : {}),
2732
+ ...(skipResurface ? { skipResurface } : {}),
2679
2733
  });
2680
2734
 
2681
2735
  if (inserted.deduplicated) {
@@ -3521,6 +3575,62 @@ export function unarchiveConversation(id: string): boolean {
3521
3575
  return true;
3522
3576
  }
3523
3577
 
3578
+ /**
3579
+ * Bring a Done conversation back to the list because a message the user reads
3580
+ * landed in it at `activityAt`.
3581
+ *
3582
+ * Under the `sidebar-done` gate "Done" is a completion mark, not a deletion:
3583
+ * the conversation keeps its schedules and its channel threads, so anything
3584
+ * the user would read arriving in it has to be reachable again. Two seams call
3585
+ * this, which between them cover every ingest path without a per-provider
3586
+ * hook: {@link insertMessageCore}, where each durable append lands (channel
3587
+ * inbound, web and CLI sends, wake tail messages, assistant-initiated
3588
+ * notification bodies), and the turn's finalize effect, where a streamed
3589
+ * assistant reply becomes readable content.
3590
+ *
3591
+ * Rows the user never reads resurface nothing. `insertMessageCore` excludes
3592
+ * the echo-suppressed set (hidden signals, subagent and ACP notifications, and
3593
+ * the `<background_event>` trigger each wake persists); the empty row an LLM
3594
+ * call reserves before it runs, so a call rejected before producing anything
3595
+ * leaves the conversation Done; and any row whose producer passed
3596
+ * {@link AddMessageOptions.skipResurface}. Retrospectives and memory
3597
+ * consolidation are excluded by construction: they append to a fork or to a
3598
+ * conversation of their own, never to the conversation under review.
3599
+ *
3600
+ * The clear is conditional on `archived_at <= activityAt` in one statement, so
3601
+ * a Done mark the user makes after this activity (while a turn's deferred tail
3602
+ * is still settling, or from the daemon while a worker runs the turn) wins
3603
+ * instead of being erased by it.
3604
+ *
3605
+ * No-ops when the gate is off, which is the shipped behavior: nothing but the
3606
+ * unarchive route clears `archived_at`.
3607
+ */
3608
+ export function resurfaceArchivedConversation(
3609
+ conversationId: string,
3610
+ activityAt: number,
3611
+ ): boolean {
3612
+ if (!isSidebarDoneEnabled(getConfig())) {
3613
+ return false;
3614
+ }
3615
+ const now = Date.now();
3616
+ const changed = rawRun(
3617
+ "conversation:resurface",
3618
+ "UPDATE conversations SET archived_at = NULL, updated_at = ? WHERE id = ? AND archived_at IS NOT NULL AND archived_at <= ?",
3619
+ now,
3620
+ conversationId,
3621
+ activityAt,
3622
+ );
3623
+ if (changed === 0) {
3624
+ return false;
3625
+ }
3626
+ publishConversationListAndMetadataChanged("reordered", conversationId);
3627
+ log.info(
3628
+ { conversationId },
3629
+ "Resurfaced Done conversation on user-visible activity",
3630
+ );
3631
+ return true;
3632
+ }
3633
+
3524
3634
  /**
3525
3635
  * Persist the processing-start timestamp for a conversation. Called by
3526
3636
  * `Conversation.setProcessing(true)` so out-of-process callers can detect
@@ -4530,6 +4640,7 @@ export async function reserveMessage(
4530
4640
  content: inflightRef ? JSON.stringify({ ref: inflightRef }) : "[]",
4531
4641
  ...(inflightRef ? { finalized: 0 as const } : {}),
4532
4642
  metadata,
4643
+ reserved: true,
4533
4644
  });
4534
4645
  }
4535
4646
 
@@ -116,6 +116,16 @@ export function hasLexicalTokens(text: string): boolean {
116
116
  */
117
117
  export type ArchiveStatusFilter = "active" | "archived" | "all";
118
118
 
119
+ /**
120
+ * Which bucket a listing covers.
121
+ *
122
+ * The persisted {@link ConversationType} values plus `"all"`, a read-side
123
+ * union that exists only here: no row carries it, and nothing writes it. It
124
+ * lets one paginated request span a user's whole history, which is what a
125
+ * full-history view needs and what three interleaved cursors cannot give it.
126
+ */
127
+ export type ConversationListTypeFilter = ConversationType | "all";
128
+
119
129
  function archiveStatusClause(status: ArchiveStatusFilter) {
120
130
  switch (status) {
121
131
  case "active":
@@ -196,6 +206,25 @@ function surfacedVisibilitySql(alias = "conversations"): string {
196
206
  );
197
207
  }
198
208
 
209
+ /**
210
+ * Raw SQL predicate for the background **umbrella**: background and scheduled
211
+ * rows together, matched on `conversation_type` or on the system group the
212
+ * producer filed them in, with subagent runs excluded so the sidebar never
213
+ * surfaces them.
214
+ *
215
+ * Shared by the `"background"` bucket of {@link conversationTypeClause} and by
216
+ * the `"all"` bucket, which is the union of that one with the standard
217
+ * listing, so the combined read can never admit a row either bucket alone
218
+ * would have withheld.
219
+ */
220
+ function backgroundUmbrellaSql(alias = "conversations"): string {
221
+ return (
222
+ `((${alias}.conversation_type IN ('background', 'scheduled')` +
223
+ ` OR COALESCE(${alias}.group_id, 'system:all') IN ('system:background', 'system:scheduled'))` +
224
+ ` AND (${alias}.source IS NULL OR ${alias}.source != 'subagent'))`
225
+ );
226
+ }
227
+
199
228
  /**
200
229
  * Raw SQL predicate for "not an automated background/scheduled row".
201
230
  *
@@ -289,6 +318,11 @@ function ungroupedSql(alias = "conversations"): string {
289
318
  * Background and Scheduled sidebar sections from one request.
290
319
  * - `"scheduled"` — scheduled rows only, so the Scheduled section can load
291
320
  * independently of the broader background backlog without over-fetching it.
321
+ * - `"all"`: the union of `"standard"` and `"background"`, so one paginated
322
+ * request covers a user's whole history instead of interleaving three. Not
323
+ * a bypass of the visibility rules: it is exactly those two predicates
324
+ * OR-ed, with the legacy `private` type excluded on top, so nothing reaches
325
+ * it that neither bucket would have returned.
292
326
  *
293
327
  * `group_id` is matched alongside `conversationType` so conversations routed to
294
328
  * `system:background` / `system:scheduled` (heartbeat, reminders, schedule-job
@@ -296,7 +330,7 @@ function ungroupedSql(alias = "conversations"): string {
296
330
  * correct bucket. Subagent runs are excluded from the background/scheduled
297
331
  * buckets so the sidebar never surfaces them.
298
332
  */
299
- function conversationTypeClause(type: ConversationType) {
333
+ function conversationTypeClause(type: ConversationListTypeFilter) {
300
334
  const notSubagent = sql`(${conversations.source} IS NULL OR ${conversations.source} != 'subagent')`;
301
335
  switch (type) {
302
336
  case "standard":
@@ -305,9 +339,20 @@ function conversationTypeClause(type: ConversationType) {
305
339
  // standardListingVisibilitySql for the full predicate semantics.
306
340
  return sql.raw(standardListingVisibilitySql());
307
341
  case "background":
308
- return sql`(${conversations.conversationType} IN ('background', 'scheduled') OR group_id IN ('system:background', 'system:scheduled')) AND ${notSubagent}`;
342
+ return sql.raw(backgroundUmbrellaSql());
309
343
  case "scheduled":
310
344
  return sql`(${conversations.conversationType} = 'scheduled' OR group_id = 'system:scheduled') AND ${notSubagent}`;
345
+ case "all":
346
+ /* `private` is excluded explicitly rather than left to the two arms.
347
+ The standard arm already drops it everywhere, but the umbrella
348
+ matches on the system group as well as on the type, so a legacy
349
+ private row filed in `system:background` would otherwise arrive
350
+ through it. Those rows exist transiently after an in-place snapshot
351
+ restore, before migration cleanup deletes them. */
352
+ return sql.raw(
353
+ `((${standardListingVisibilitySql()} OR ${backgroundUmbrellaSql()})` +
354
+ ` AND conversations.conversation_type != 'private')`,
355
+ );
311
356
  }
312
357
  }
313
358
 
@@ -321,7 +366,7 @@ function conversationTypeClause(type: ConversationType) {
321
366
  * transposition waiting to happen.
322
367
  */
323
368
  export interface ConversationListFilter {
324
- conversationType?: ConversationType;
369
+ conversationType?: ConversationListTypeFilter;
325
370
  archiveStatus?: ArchiveStatusFilter;
326
371
  originChannel?: string;
327
372
  /**