@vellumai/assistant 0.10.3 → 0.10.4-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 (239) hide show
  1. package/openapi.yaml +73 -56
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  4. package/src/__tests__/assistant-stream-state.test.ts +3 -76
  5. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
  6. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  7. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  8. package/src/__tests__/channel-guardian.test.ts +82 -32
  9. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  10. package/src/__tests__/channel-reply-delivery.test.ts +6 -2
  11. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +148 -0
  13. package/src/__tests__/consult-deadline.test.ts +60 -0
  14. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  15. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  16. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  17. package/src/__tests__/contacts-write.test.ts +0 -2
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +98 -7
  20. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  21. package/src/__tests__/conversation-error.test.ts +18 -0
  22. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  23. package/src/__tests__/conversation-title-service.test.ts +222 -201
  24. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  25. package/src/__tests__/delete-propagation.test.ts +5 -3
  26. package/src/__tests__/dm-backfill.test.ts +6 -4
  27. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  28. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  29. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  30. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  31. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  32. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  33. package/src/__tests__/helpers/mock-logger.ts +1 -0
  34. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  35. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  36. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  37. package/src/__tests__/invite-routes-http.test.ts +34 -0
  38. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  39. package/src/__tests__/list-messages-page-latest.test.ts +173 -4
  40. package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
  41. package/src/__tests__/non-member-access-request.test.ts +15 -13
  42. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  43. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  44. package/src/__tests__/persona-resolver.test.ts +75 -45
  45. package/src/__tests__/plugin-bootstrap.test.ts +13 -5
  46. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  47. package/src/__tests__/provider-usage-tracking.test.ts +1 -1
  48. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  49. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  50. package/src/__tests__/reaction-persistence.test.ts +51 -4
  51. package/src/__tests__/relay-server.test.ts +88 -31
  52. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  53. package/src/__tests__/settings-routes.test.ts +32 -0
  54. package/src/__tests__/slack-block-formatting.test.ts +1 -38
  55. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  56. package/src/__tests__/stt-hints.test.ts +6 -3
  57. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  58. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  59. package/src/__tests__/subagent-role-registry.test.ts +17 -4
  60. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  61. package/src/__tests__/subagent-tools.test.ts +398 -3
  62. package/src/__tests__/thread-backfill.test.ts +3 -3
  63. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
  64. package/src/__tests__/tool-start-timestamp.test.ts +4 -3
  65. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  66. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  67. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  68. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  69. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  70. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  71. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  72. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  73. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
  74. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  75. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  76. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  77. package/src/agent/loop-exclusive-tool.test.ts +19 -15
  78. package/src/agent/loop-native-web-search.test.ts +200 -0
  79. package/src/agent/loop.ts +108 -1
  80. package/src/api/responses/conversation-message.ts +9 -0
  81. package/src/approvals/guardian-request-resolvers.ts +16 -4
  82. package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
  83. package/src/calls/guardian-dispatch.ts +14 -11
  84. package/src/calls/inbound-trust-reader.ts +7 -1
  85. package/src/calls/relay-access-wait.ts +6 -6
  86. package/src/calls/relay-server.ts +22 -2
  87. package/src/calls/relay-setup-router.ts +10 -10
  88. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  89. package/src/cli/commands/contacts.ts +10 -7
  90. package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
  91. package/src/cli/commands/memory/worker.ts +97 -30
  92. package/src/cli/commands/plugins.ts +3 -146
  93. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
  94. package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
  95. package/src/cli/lib/publish-plugin.ts +231 -1
  96. package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
  97. package/src/config/bundled-skills/subagent/SKILL.md +16 -1
  98. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  99. package/src/config/call-site-defaults.ts +0 -6
  100. package/src/config/llm-resolver.ts +0 -3
  101. package/src/config/schemas/call-site-catalog.ts +0 -7
  102. package/src/config/schemas/heartbeat.ts +2 -5
  103. package/src/config/schemas/llm.ts +3 -12
  104. package/src/config/schemas/memory-lifecycle.ts +1 -1
  105. package/src/config/seed-inference-profiles.ts +76 -35
  106. package/src/config/sync-gated-profiles.ts +0 -3
  107. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  108. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  109. package/src/contacts/contact-store.ts +27 -237
  110. package/src/contacts/contacts-write.ts +18 -58
  111. package/src/contacts/gateway-channel-read.ts +51 -0
  112. package/src/contacts/member-write-relay.ts +25 -31
  113. package/src/contacts/types.ts +3 -15
  114. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  115. package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
  116. package/src/daemon/conversation-agent-loop.ts +68 -61
  117. package/src/daemon/conversation-error.ts +7 -10
  118. package/src/daemon/conversation-tool-setup.ts +0 -10
  119. package/src/daemon/conversation.ts +10 -0
  120. package/src/daemon/external-plugins-bootstrap.ts +8 -2
  121. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  122. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  123. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  124. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  125. package/src/daemon/handlers/config-channels.ts +14 -29
  126. package/src/daemon/lifecycle.ts +16 -4
  127. package/src/daemon/message-types/surfaces.ts +2 -0
  128. package/src/heartbeat/heartbeat-service.ts +5 -0
  129. package/src/home/relationship-state-writer.ts +5 -0
  130. package/src/memory/__tests__/embedding-cache.test.ts +136 -0
  131. package/src/memory/compaction-ledger-store.ts +107 -0
  132. package/src/memory/conversation-crud.ts +136 -61
  133. package/src/memory/conversation-title-service.ts +173 -24
  134. package/src/memory/embedding-backend.ts +8 -1
  135. package/src/memory/embedding-cache.ts +139 -0
  136. package/src/memory/jobs-worker.ts +75 -29
  137. package/src/memory/memory-retrospective-job.ts +5 -0
  138. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
  139. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  140. package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
  141. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
  142. package/src/memory/schema/contacts.ts +6 -2
  143. package/src/memory/schema/conversations.ts +39 -0
  144. package/src/memory/steps.ts +1090 -367
  145. package/src/memory/worker-control.ts +104 -18
  146. package/src/memory/worker-process.ts +17 -0
  147. package/src/messaging/channel-binding-metadata.ts +31 -0
  148. package/src/messaging/channel-binding-schema.ts +51 -0
  149. package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
  150. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
  151. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
  152. package/src/messaging/providers/a2a/deliver.ts +5 -1
  153. package/src/messaging/providers/a2a/transport.ts +10 -0
  154. package/src/messaging/providers/callback-routing.ts +48 -0
  155. package/src/messaging/providers/channel-transport.ts +55 -0
  156. package/src/messaging/providers/index.ts +65 -241
  157. package/src/messaging/providers/slack/binding-metadata.ts +62 -0
  158. package/src/messaging/providers/slack/transport.ts +92 -0
  159. package/src/messaging/providers/telegram-bot/transport.ts +51 -0
  160. package/src/messaging/providers/whatsapp/transport.ts +38 -0
  161. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  162. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  163. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  164. package/src/notifications/destination-resolver.ts +7 -23
  165. package/src/notifications/emit-signal.ts +5 -11
  166. package/src/plugins/defaults/index.ts +0 -35
  167. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
  168. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
  169. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
  170. package/src/plugins/disabled-state.ts +31 -0
  171. package/src/plugins/registry.ts +55 -12
  172. package/src/prompts/persona-resolver.ts +43 -11
  173. package/src/providers/call-site-routing.ts +41 -0
  174. package/src/providers/provider-send-message.ts +6 -0
  175. package/src/providers/ratelimit.ts +6 -0
  176. package/src/providers/registry.ts +1 -1
  177. package/src/providers/retry.ts +6 -0
  178. package/src/providers/types.ts +13 -0
  179. package/src/providers/usage-tracking.ts +6 -0
  180. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  181. package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
  182. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  183. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
  184. package/src/runtime/access-request-helper.ts +1 -2
  185. package/src/runtime/actor-trust-resolver.ts +44 -17
  186. package/src/runtime/anchored-guardian.test.ts +7 -54
  187. package/src/runtime/anchored-guardian.ts +4 -53
  188. package/src/runtime/assistant-stream-state.ts +12 -74
  189. package/src/runtime/channel-reply-delivery.ts +3 -8
  190. package/src/runtime/guardian-vellum-migration.ts +18 -16
  191. package/src/runtime/invite-redemption-service.ts +25 -10
  192. package/src/runtime/local-actor-identity.test.ts +108 -0
  193. package/src/runtime/local-actor-identity.ts +27 -20
  194. package/src/runtime/member-verdict-cache.ts +0 -0
  195. package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
  196. package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
  197. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  198. package/src/runtime/routes/contact-routes.ts +40 -25
  199. package/src/runtime/routes/conversation-list-routes.ts +1 -29
  200. package/src/runtime/routes/conversation-routes.ts +27 -7
  201. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  202. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  203. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  204. package/src/runtime/routes/settings-routes.ts +8 -3
  205. package/src/runtime/services/conversation-serializer.ts +6 -49
  206. package/src/runtime/slack-block-formatting.ts +0 -15
  207. package/src/runtime/trust-verdict-consumer.ts +36 -41
  208. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  209. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  210. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
  211. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  212. package/src/subagent/index.ts +1 -1
  213. package/src/subagent/manager.ts +245 -33
  214. package/src/subagent/types.ts +8 -1
  215. package/src/tools/registry.ts +10 -3
  216. package/src/tools/subagent/consult-deadline.ts +49 -0
  217. package/src/tools/subagent/spawn.ts +234 -5
  218. package/src/util/logger.ts +9 -0
  219. package/src/util/platform.ts +14 -0
  220. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  221. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  222. package/src/workspace/migrations/registry.ts +2 -0
  223. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  224. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  225. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  226. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
  227. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  228. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  229. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  230. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  231. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  232. package/src/plugins/defaults/advisor/config.ts +0 -21
  233. package/src/plugins/defaults/advisor/consult.ts +0 -197
  234. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  235. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  236. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  237. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  238. package/src/plugins/defaults/advisor/package.json +0 -14
  239. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
@@ -33,13 +33,12 @@ function makeContact(
33
33
  id: "ct_1",
34
34
  displayName: "Alice",
35
35
  notes: null,
36
+ role: "contact",
36
37
  lastInteraction: null,
37
38
  interactionCount: 0,
38
39
  createdAt: 1,
39
40
  updatedAt: 1,
40
- role: "contact",
41
41
  contactType: "human",
42
- principalId: null,
43
42
  userFile: null,
44
43
  channels: [],
45
44
  ...overrides,
@@ -66,8 +66,9 @@ mock.module("../../../contacts/guardian-delivery-reader.js", () => ({
66
66
  ) => list.find((g) => g.channelType === channelType && g.status === "active"),
67
67
  }));
68
68
 
69
+ // Member ACL rides on memberRecord via the member-verdict cache; no local
70
+ // contact here.
69
71
  mock.module("../../../contacts/contact-store.js", () => ({
70
- findGuardianForChannel: (_channelType: string) => null,
71
72
  findContactByAddress: () => null,
72
73
  }));
73
74
 
@@ -78,15 +78,24 @@ function withChannelCompat<T extends { channels: { address: string }[] }>(
78
78
  };
79
79
  }
80
80
 
81
- /** Compose both response transforms (guardian display name + channel compat). */
81
+ /** Compose both response transforms (guardian display name + channel compat).
82
+ * Also coerces nullable gateway-sourced fields to their DB defaults so the
83
+ * response satisfies the strict enum schema even in degraded mode (assistant
84
+ * DB unreachable → gateway soft-fail join produces nulls).
85
+ */
82
86
  function prepareContactResponse<
83
87
  T extends {
84
88
  role: string;
85
89
  displayName: string;
90
+ contactType?: string | null;
86
91
  channels: { address: string }[];
87
92
  },
88
93
  >(contact: T): T {
89
- return withChannelCompat(withGuardianNameOverride(contact));
94
+ const coerced =
95
+ contact.contactType == null
96
+ ? { ...contact, contactType: "human" as T["contactType"] }
97
+ : contact;
98
+ return withChannelCompat(withGuardianNameOverride(coerced));
90
99
  }
91
100
 
92
101
  const VALID_CONTACT_TYPES: readonly ContactType[] = ["human", "assistant"];
@@ -99,6 +108,13 @@ function isContactType(value: string): value is ContactType {
99
108
  // Response schemas (drive OpenAPI spec → codegen → typed SDK)
100
109
  // ---------------------------------------------------------------------------
101
110
 
111
+ // Channel ACL fields (status/policy/verifiedAt/verifiedVia/revokedReason/
112
+ // blockedReason) are gateway-owned and present ONLY on gateway-relayed reads
113
+ // (`contacts_list_rich`/`contacts_get_rich`). Daemon-native filtered reads
114
+ // (search / contactType) omit them, so they are `.optional()`. Contact-level
115
+ // `role` is stored locally (NOT NULL, default "contact") and returned on all
116
+ // paths. INFO telemetry (lastSeenAt/interactionCount/lastInteraction) is
117
+ // locally hydrated on every read path and stays required.
102
118
  const contactChannelSchema = z.object({
103
119
  id: z.string(),
104
120
  contactId: z.string(),
@@ -107,23 +123,23 @@ const contactChannelSchema = z.object({
107
123
  isPrimary: z.boolean(),
108
124
  /** @deprecated Echoes `address` for backwards compatibility with older macOS clients. */
109
125
  externalUserId: z.string().nullable(),
110
- status: z.string(),
111
- policy: z.string(),
112
- verifiedAt: z.number().nullable(),
113
- verifiedVia: z.string().nullable(),
126
+ status: z.string().optional(),
127
+ policy: z.string().optional(),
128
+ verifiedAt: z.number().nullable().optional(),
129
+ verifiedVia: z.string().nullable().optional(),
114
130
  lastSeenAt: z.number().nullable(),
115
131
  interactionCount: z.number(),
116
132
  lastInteraction: z.number().nullable(),
117
- revokedReason: z.string().nullable(),
118
- blockedReason: z.string().nullable(),
133
+ revokedReason: z.string().nullable().optional(),
134
+ blockedReason: z.string().nullable().optional(),
119
135
  });
120
136
 
121
137
  const contactSchema = z.object({
122
138
  id: z.string(),
123
139
  displayName: z.string(),
124
- role: z.string(),
140
+ role: z.enum(["guardian", "contact"]),
125
141
  notes: z.string().nullable().optional(),
126
- contactType: z.string().nullable().optional(),
142
+ contactType: z.enum(["human", "assistant"]),
127
143
  lastInteraction: z.number().nullable().optional(),
128
144
  interactionCount: z.number(),
129
145
  createdAt: z.number(),
@@ -142,10 +158,7 @@ const contactSchema = z.object({
142
158
  * a relay failure surfaces as an error rather than reading ACL from the
143
159
  * assistant DB.
144
160
  */
145
- async function relayListContacts(
146
- limit: number,
147
- role: ContactRole | undefined,
148
- ) {
161
+ async function relayListContacts(limit: number, role: ContactRole | undefined) {
149
162
  try {
150
163
  const result = await ipcCallPersistent("contacts_list_rich", {
151
164
  limit,
@@ -188,7 +201,6 @@ export async function handleListContacts(queryParams: Record<string, string>) {
188
201
  query,
189
202
  channelAddress,
190
203
  channelType,
191
- role,
192
204
  contactType,
193
205
  limit,
194
206
  });
@@ -206,7 +218,7 @@ export async function handleListContacts(queryParams: Record<string, string>) {
206
218
  log.debug(
207
219
  "handleListContacts: contactType-filtered read served daemon-native (gateway-native contactType filtering is design-blocked, pending ACL classification)",
208
220
  );
209
- const contacts = listContacts(limit, role, contactType);
221
+ const contacts = listContacts(limit, contactType);
210
222
  return {
211
223
  ok: true,
212
224
  contacts: contacts.map(prepareContactResponse),
@@ -254,7 +266,9 @@ export async function handleGetContact(contactId: string) {
254
266
  // ~5s fallback plus IPC overhead on both nested hops. (List/revoke keep the default.)
255
267
  const INVITE_CREATE_RELAY_TIMEOUT_MS = 30_000;
256
268
 
257
- export async function handleListInvites({ queryParams = {} }: RouteHandlerArgs) {
269
+ export async function handleListInvites({
270
+ queryParams = {},
271
+ }: RouteHandlerArgs) {
258
272
  try {
259
273
  const result = (await ipcCallPersistent("invites_list", {
260
274
  ...(queryParams.sourceChannel
@@ -294,7 +308,9 @@ export async function handleCreateInvite({ body = {} }: RouteHandlerArgs) {
294
308
  }
295
309
  }
296
310
 
297
- export async function handleRevokeInvite({ pathParams = {} }: RouteHandlerArgs) {
311
+ export async function handleRevokeInvite({
312
+ pathParams = {},
313
+ }: RouteHandlerArgs) {
298
314
  try {
299
315
  const result = (await ipcCallPersistent("invites_revoke", {
300
316
  id: pathParams.id,
@@ -312,9 +328,7 @@ export async function handleRevokeInvite({ pathParams = {} }: RouteHandlerArgs)
312
328
  * `invites_redeem_voice` method. Wraps the identity-bound
313
329
  * `redeemVoiceInviteCode` path.
314
330
  */
315
- export async function handleRedeemVoiceInvite({
316
- body = {},
317
- }: RouteHandlerArgs) {
331
+ export async function handleRedeemVoiceInvite({ body = {} }: RouteHandlerArgs) {
318
332
  const callerExternalUserId = body.callerExternalUserId as string | undefined;
319
333
  const code = body.code as string | undefined;
320
334
 
@@ -348,9 +362,7 @@ export async function handleRedeemVoiceInvite({
348
362
  * `invites_redeem_token` method. Wraps the generic `redeemIngressInvite`
349
363
  * token path.
350
364
  */
351
- export async function handleRedeemTokenInvite({
352
- body = {},
353
- }: RouteHandlerArgs) {
365
+ export async function handleRedeemTokenInvite({ body = {} }: RouteHandlerArgs) {
354
366
  const result = await redeemIngressInvite({
355
367
  token: body.token as string | undefined,
356
368
  externalUserId: body.externalUserId as string | undefined,
@@ -641,7 +653,10 @@ export const ROUTES: RouteDefinition[] = [
641
653
  // No-filter "search" is a list read — relay to the gateway so it returns
642
654
  // the same source-of-truth data as `contacts list`.
643
655
  if (!hasFilter) {
644
- const { contacts } = await relayListContacts(parsed.limit ?? 50, undefined);
656
+ const { contacts } = await relayListContacts(
657
+ parsed.limit ?? 50,
658
+ undefined,
659
+ );
645
660
  return contacts;
646
661
  }
647
662
 
@@ -31,6 +31,7 @@ import {
31
31
  import type { ConversationType } from "../../memory/conversation-types.js";
32
32
  import { getBindingsForConversations } from "../../memory/external-conversation-store.js";
33
33
  import { listGroups } from "../../memory/group-crud.js";
34
+ import { channelBindingSchema } from "../../messaging/channel-binding-schema.js";
34
35
  import { UserError } from "../../util/errors.js";
35
36
  import { getLogger } from "../../util/logger.js";
36
37
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
@@ -86,35 +87,6 @@ const assistantAttentionSchema = z.object({
86
87
  .optional(),
87
88
  });
88
89
 
89
- const slackThreadSchema = z.object({
90
- channelId: z.string(),
91
- threadTs: z.string(),
92
- link: z
93
- .object({
94
- appUrl: z.string().optional(),
95
- webUrl: z.string().optional(),
96
- })
97
- .optional(),
98
- });
99
-
100
- const slackChannelSchema = z.object({
101
- channelId: z.string(),
102
- name: z.string().optional(),
103
- link: z.object({ webUrl: z.string() }).optional(),
104
- });
105
-
106
- const channelBindingSchema = z.object({
107
- sourceChannel: z.string(),
108
- externalChatId: z.string(),
109
- externalChatName: z.string().optional(),
110
- externalThreadId: z.string().optional(),
111
- externalUserId: z.string().nullable(),
112
- displayName: z.string().nullable(),
113
- username: z.string().nullable(),
114
- slackThread: slackThreadSchema.optional(),
115
- slackChannel: slackChannelSchema.optional(),
116
- });
117
-
118
90
  const forkParentSchema = z.object({
119
91
  conversationId: z.string(),
120
92
  messageId: z.string(),
@@ -90,9 +90,11 @@ import {
90
90
  addMessage,
91
91
  extractImageSourcePaths,
92
92
  getConversation,
93
+ getConversationPersistedSeq,
93
94
  getMessages,
94
95
  getMessagesPaginated,
95
96
  hasMessages,
97
+ isConversationProcessing,
96
98
  type MessageRow,
97
99
  provenanceFromTrustContext,
98
100
  setConversationInferenceProfile,
@@ -122,7 +124,6 @@ import {
122
124
  } from "../../util/platform.js";
123
125
  import { silentlyWithLog } from "../../util/silently.js";
124
126
  import { assistantEventHub, broadcastMessage } from "../assistant-event-hub.js";
125
- import { getPersistedSeq } from "../assistant-stream-state.js";
126
127
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
127
128
  import {
128
129
  type GuardianPendingScope,
@@ -393,6 +394,8 @@ function buildSlackHistoryMessage(
393
394
  : {}),
394
395
  ...(messageLink ? { messageLink } : {}),
395
396
  ...(threadLink ? { threadLink } : {}),
397
+ ...(slackMeta.eventKind ? { eventKind: slackMeta.eventKind } : {}),
398
+ ...(slackMeta.reaction ? { reaction: slackMeta.reaction } : {}),
396
399
  };
397
400
  }
398
401
 
@@ -657,6 +660,7 @@ export function handleListMessages({
657
660
  oldestTimestamp: null,
658
661
  oldestMessageId: null,
659
662
  seq: null,
663
+ processing: false,
660
664
  };
661
665
  }
662
666
  return { messages: [] };
@@ -1006,11 +1010,19 @@ export function handleListMessages({
1006
1010
  });
1007
1011
 
1008
1012
  // Snapshot↔stream alignment token: the `seq` of the last event whose
1009
- // content is durably persisted for this conversation in the current
1010
- // daemon process. Returned on every resolved-conversation response so a
1011
- // client can apply only stream events with a higher `seq`. Null when
1012
- // nothing has been persisted in-process (cold/aged-out/post-restart).
1013
- const persistedSeq = getPersistedSeq(resolvedConversationId);
1013
+ // content is durably persisted for this conversation, read from the
1014
+ // `conversations.seq` column. Returned on every resolved-conversation
1015
+ // response so a client can apply only stream events with a higher `seq`.
1016
+ // Null when nothing has been persisted (the conversation was created before
1017
+ // any stream activity, or predates the column) -- the client cold-starts.
1018
+ const persistedSeq = getConversationPersistedSeq(resolvedConversationId);
1019
+
1020
+ // Authoritative "is the agent mid-turn?" signal, sourced from the
1021
+ // `processing_started_at` column (persisted, survives daemon restarts).
1022
+ // Clients use this to distinguish a live turn still in flight from a
1023
+ // turn that silently died — without it, a dropped SSE stream leaves the
1024
+ // UI spinning forever with no way to learn the server is actually idle.
1025
+ const processing = isConversationProcessing(resolvedConversationId);
1014
1026
 
1015
1027
  if (isPaginated) {
1016
1028
  // Prefer the page's oldest visible row (the documented cursor semantic).
@@ -1034,6 +1046,7 @@ export function handleListMessages({
1034
1046
  oldestTimestamp: oldestTimestamp ?? null,
1035
1047
  oldestMessageId: oldestMessageId ?? null,
1036
1048
  seq: persistedSeq,
1049
+ processing,
1037
1050
  };
1038
1051
  }
1039
1052
 
@@ -1043,10 +1056,11 @@ export function handleListMessages({
1043
1056
  ...(oldestTimestamp != null ? { oldestTimestamp } : {}),
1044
1057
  ...(oldestMessageId != null ? { oldestMessageId } : {}),
1045
1058
  seq: persistedSeq,
1059
+ processing,
1046
1060
  };
1047
1061
  }
1048
1062
 
1049
- return { messages, seq: persistedSeq };
1063
+ return { messages, seq: persistedSeq, processing };
1050
1064
  }
1051
1065
 
1052
1066
  /**
@@ -2688,6 +2702,12 @@ export const ROUTES: RouteDefinition[] = [
2688
2702
  .describe(
2689
2703
  "Global SSE `seq` of the last event whose content is durably persisted for this conversation in the current daemon process. A client can align this snapshot with the `/events` stream by applying only events with `seq` greater than this value. Null when no events have been persisted in this process (cold conversation, after a daemon restart, or when the conversation has aged out of the in-memory map) — clients should cold-start in that case. Absent on older daemons that predate this field.",
2690
2704
  ),
2705
+ processing: z
2706
+ .boolean()
2707
+ .optional()
2708
+ .describe(
2709
+ "Whether the agent is currently mid-turn for this conversation, sourced authoritatively from the persisted `processing_started_at` column. `true` means a turn is in flight; `false` means the conversation is idle. Clients use this to recover from a dropped SSE stream: if a turn appears to be running locally but the server reports `processing: false`, the turn has ended (or died) and the UI should stop waiting rather than spin indefinitely. Absent on older daemons that predate this field.",
2710
+ ),
2691
2711
  }),
2692
2712
  handler: (args) => handleListMessages(args),
2693
2713
  },
@@ -9,10 +9,6 @@ import { isInviteCodeRedemptionEnabled } from "../../../channels/config.js";
9
9
  import type { ChannelId } from "../../../channels/types.js";
10
10
  import { getGuardianDelivery } from "../../../contacts/guardian-delivery-reader.js";
11
11
  import { channelStatusToMemberStatus } from "../../../contacts/member-status.js";
12
- import type {
13
- ContactChannel,
14
- ContactWithChannels,
15
- } from "../../../contacts/types.js";
16
12
  import { deleteInbound, recordInbound } from "../../../memory/delivery-crud.js";
17
13
  import { markProcessed } from "../../../memory/delivery-status.js";
18
14
  import {
@@ -96,12 +92,6 @@ export interface AclEnforcementParams {
96
92
  effectiveAdmissionPolicy?: AdmissionPolicy;
97
93
  }
98
94
 
99
- /** Resolved contact + channel pair from ACL enforcement. */
100
- export type ResolvedMember = {
101
- contact: ContactWithChannels;
102
- channel: ContactChannel;
103
- };
104
-
105
95
  export interface AclResult {
106
96
  resolvedMember: VerdictMember | null;
107
97
  /** When set, the caller must return this response immediately. */
@@ -8,7 +8,6 @@
8
8
  * focused on orchestration.
9
9
  */
10
10
  import type { ChannelId, InterfaceId } from "../../../channels/types.js";
11
- import { findGuardianForChannel } from "../../../contacts/contact-store.js";
12
11
  import {
13
12
  getGuardianDelivery,
14
13
  guardianForChannel,
@@ -964,17 +963,14 @@ function startTrustedContactApprovalNotifier(params: {
964
963
 
965
964
  if (info && !globalNotifiedApprovalRequestIds.has(info.requestId)) {
966
965
  globalNotifiedApprovalRequestIds.set(info.requestId, conversationId);
967
- // Gateway-resolved guardian display name with the transitional
968
- // local fallback on null/no-match (display-only). Removed in Combo 11.
966
+ // Gateway-resolved guardian display name (display-only).
969
967
  const guardians = await getGuardianDelivery({
970
968
  channelTypes: [sourceChannel],
971
969
  });
972
- const gatewayDisplayName = guardians
973
- ? guardianForChannel(guardians, sourceChannel)?.displayName
970
+ const displayName = guardians
971
+ ? (guardianForChannel(guardians, sourceChannel)?.displayName ??
972
+ undefined)
974
973
  : undefined;
975
- const displayName =
976
- gatewayDisplayName ??
977
- findGuardianForChannel(sourceChannel)?.contact.displayName;
978
974
  const guardianName = resolveGuardianName(displayName);
979
975
  const waitingText = `Waiting for ${guardianName}'s approval...`;
980
976
  try {
@@ -18,6 +18,7 @@
18
18
  import type { SourceMetadata } from "@vellumai/gateway-client";
19
19
 
20
20
  import type { ChannelId, InterfaceId } from "../../../channels/types.js";
21
+ import { getGuardianDeliveryFresh } from "../../../contacts/guardian-delivery-reader.js";
21
22
  import { getDiskPressureStatus } from "../../../daemon/disk-pressure-guard.js";
22
23
  import { classifyDiskPressureTurnPolicy } from "../../../daemon/disk-pressure-policy.js";
23
24
  import { addMessage } from "../../../memory/conversation-crud.js";
@@ -35,6 +36,7 @@ import {
35
36
  import { getLogger } from "../../../util/logger.js";
36
37
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../assistant-scope.js";
37
38
  import type { ApprovalConversationGenerator } from "../../http-types.js";
39
+ import { setMemberVerdict } from "../../member-verdict-cache.js";
38
40
  import { resolveTrustContext } from "../../trust-context-resolver.js";
39
41
  import { handleGuardianReplyIntercept } from "./guardian-reply-intercept.js";
40
42
 
@@ -126,6 +128,23 @@ export async function handleSlackReactionIntercept(
126
128
  approvalConversationGenerator,
127
129
  } = params;
128
130
 
131
+ // Warm the channel-specific guardian-delivery cache before the SYNC trust
132
+ // resolve below. The sync resolver reads the IO-free cache snapshot; on a
133
+ // cold process only `vellum` is warmed at startup, so a Slack guardian
134
+ // reaction would otherwise misclassify as `unknown` and drop. Read fresh:
135
+ // gateway-side binding writes don't invalidate the daemon cache, so a stale
136
+ // empty snapshot would otherwise survive the TTL. This await runs in the
137
+ // already-async intercept, off the sync resolver's hot path.
138
+ await getGuardianDeliveryFresh({ channelTypes: [sourceChannel] });
139
+
140
+ // Reactions carry the gateway-stamped verdict but skip getInboundTrustVerdict,
141
+ // which warms the member-verdict cache the sync resolver reads. Seed it from
142
+ // the stamped verdict so an active non-guardian contact's reaction resolves
143
+ // instead of failing closed to `unknown` on a cold cache.
144
+ if (sourceMetadata?.trustVerdict) {
145
+ setMemberVerdict(sourceChannel, rawSenderId, sourceMetadata.trustVerdict);
146
+ }
147
+
129
148
  // Classify the reactor. No timezone enrichment — reactions never drive an
130
149
  // agent turn, so only the trust class / guardian principal matter.
131
150
  const trustCtx = resolveTrustContext({
@@ -15,6 +15,7 @@ import {
15
15
  } from "../../config/env.js";
16
16
  import { loadRawConfig, saveRawConfig } from "../../config/loader.js";
17
17
  import { loadSkillCatalog } from "../../config/skills.js";
18
+ import { getGuardianDelivery } from "../../contacts/guardian-delivery-reader.js";
18
19
  import { findConversation } from "../../daemon/conversation-registry.js";
19
20
  import {
20
21
  computeGatewayTarget,
@@ -303,8 +304,12 @@ async function handleOAuthConnectStart({ body = {} }: RouteHandlerArgs) {
303
304
  // Workspace files (list/read)
304
305
  // ---------------------------------------------------------------------------
305
306
 
306
- function getWorkspaceFiles(): string[] {
307
+ async function getWorkspaceFiles(): Promise<string[]> {
307
308
  const files = ["IDENTITY.md", "SOUL.md", "skills/"];
309
+ // Warm the vellum guardian-delivery cache so the sync persona resolution
310
+ // below hits a fresh key instead of falling back to default.md on a cold or
311
+ // TTL-expired cache.
312
+ await getGuardianDelivery({ channelTypes: ["vellum"] });
308
313
  const guardianPath = resolveGuardianPersonaPath();
309
314
  if (guardianPath) {
310
315
  files.push(`users/${basename(guardianPath)}`);
@@ -312,9 +317,9 @@ function getWorkspaceFiles(): string[] {
312
317
  return files;
313
318
  }
314
319
 
315
- function handleWorkspaceFilesList() {
320
+ async function handleWorkspaceFilesList() {
316
321
  const base = getWorkspaceDir();
317
- const files = getWorkspaceFiles().map((name) => ({
322
+ const files = (await getWorkspaceFiles()).map((name) => ({
318
323
  path: name,
319
324
  name,
320
325
  exists: pathExists(join(base, name)),
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  import { parseChannelId } from "../../channels/types.js";
11
- import { getConfig } from "../../config/loader.js";
12
11
  import { normalizeConversationType } from "../../daemon/message-types/shared.js";
13
12
  import {
14
13
  type AttentionState,
@@ -24,10 +23,7 @@ import {
24
23
  } from "../../memory/conversation-crud.js";
25
24
  import type { ExternalConversationBinding } from "../../memory/external-conversation-store.js";
26
25
  import { getBindingsForConversations } from "../../memory/external-conversation-store.js";
27
- import {
28
- buildSlackMessageDeepLinks,
29
- buildSlackWebChannelUrl,
30
- } from "../../messaging/providers/slack/deep-link.js";
26
+ import { buildChannelBindingMetadata } from "../../messaging/channel-binding-metadata.js";
31
27
 
32
28
  // ---------------------------------------------------------------------------
33
29
  // Helpers
@@ -126,48 +122,7 @@ function resolveSerializedGroupId(
126
122
  }
127
123
 
128
124
  function buildChannelBinding(binding: ExternalConversationBinding) {
129
- const externalChatName =
130
- binding.externalChatName?.trim() ||
131
- (binding.sourceChannel === "slack" ? binding.externalChatId : undefined);
132
- const slackConfig =
133
- binding.sourceChannel === "slack" ? getConfig().slack : undefined;
134
- const slackThreadLink =
135
- slackConfig && binding.externalThreadId
136
- ? buildSlackMessageDeepLinks({
137
- teamId: slackConfig.teamId,
138
- teamUrl: slackConfig.teamUrl,
139
- channelId: binding.externalChatId,
140
- messageTs: binding.externalThreadId,
141
- })
142
- : undefined;
143
- const slackThread =
144
- binding.sourceChannel === "slack" && binding.externalThreadId
145
- ? {
146
- channelId: binding.externalChatId,
147
- threadTs: binding.externalThreadId,
148
- ...(slackThreadLink ? { link: slackThreadLink } : {}),
149
- }
150
- : undefined;
151
- const slackChannelWebUrl = slackConfig
152
- ? buildSlackWebChannelUrl({
153
- teamUrl: slackConfig.teamUrl,
154
- channelId: binding.externalChatId,
155
- })
156
- : undefined;
157
- const slackChannel =
158
- binding.sourceChannel === "slack"
159
- ? {
160
- channelId: binding.externalChatId,
161
- name: externalChatName,
162
- ...(slackChannelWebUrl
163
- ? {
164
- link: {
165
- webUrl: slackChannelWebUrl,
166
- },
167
- }
168
- : {}),
169
- }
170
- : undefined;
125
+ const externalChatName = binding.externalChatName?.trim() || undefined;
171
126
 
172
127
  return {
173
128
  sourceChannel: binding.sourceChannel,
@@ -179,8 +134,10 @@ function buildChannelBinding(binding: ExternalConversationBinding) {
179
134
  externalUserId: binding.externalUserId,
180
135
  displayName: binding.displayName,
181
136
  username: binding.username,
182
- ...(slackThread ? { slackThread } : {}),
183
- ...(slackChannel ? { slackChannel } : {}),
137
+ // Channel-specific enrichment (e.g. Slack deep links) is contributed by
138
+ // the source channel's binding-metadata builder, keeping this serializer
139
+ // channel-agnostic.
140
+ ...buildChannelBindingMetadata(binding),
184
141
  };
185
142
  }
186
143
 
@@ -118,21 +118,6 @@ export function textToSlackBlocks(text: string): KnownBlock[] | undefined {
118
118
  return blocks.length > 0 ? blocks : undefined;
119
119
  }
120
120
 
121
- /**
122
- * Detect whether a callback URL points to the gateway's Slack delivery endpoint.
123
- */
124
- export function isSlackCallbackUrl(callbackUrl: string): boolean {
125
- try {
126
- const url = new URL(callbackUrl);
127
- return (
128
- url.pathname === "/deliver/slack" ||
129
- url.pathname.startsWith("/deliver/slack?")
130
- );
131
- } catch {
132
- return false;
133
- }
134
- }
135
-
136
121
  // ---------------------------------------------------------------------------
137
122
  // Internals
138
123
  // ---------------------------------------------------------------------------