@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
@@ -40,14 +40,13 @@ mock.module("../../ipc/gateway-client.js", () => ({
40
40
  }));
41
41
 
42
42
 
43
- // Local-mirror primitive.
43
+ // Local resolver primitive — resolves the native contact/channel by id; the
44
+ // gateway owns the ACL downgrade, so it takes no reason and mutates nothing.
44
45
  const revokeMemberResult: ContactWriteResult = {
45
46
  contact: { id: "c1" } as ContactWriteResult["contact"],
46
- channel: { id: "ch1", status: "revoked" } as ContactWriteResult["channel"],
47
+ channel: { id: "ch1" } as ContactWriteResult["channel"],
47
48
  };
48
- const revokeMemberMock = mock((_memberId: string, _reason?: string) =>
49
- revokeMemberResult,
50
- );
49
+ const revokeMemberMock = mock((_memberId: string) => revokeMemberResult);
51
50
  const actualContactsWrite = await import("../contacts-write.js");
52
51
  mock.module("../contacts-write.js", () => ({
53
52
  ...actualContactsWrite,
@@ -75,7 +74,7 @@ describe("revokeMemberChannel gateway-first relay", () => {
75
74
  },
76
75
  ]);
77
76
  expect(revokeMemberMock).toHaveBeenCalledTimes(1);
78
- expect(revokeMemberMock).toHaveBeenCalledWith("ch1", "removed");
77
+ expect(revokeMemberMock).toHaveBeenCalledWith("ch1");
79
78
  expect(result).toBe(revokeMemberResult);
80
79
  });
81
80
 
@@ -83,8 +82,8 @@ describe("revokeMemberChannel gateway-first relay", () => {
83
82
  await revokeMemberChannel("c1:ch1");
84
83
 
85
84
  expect(ipcCalls[0]?.params?.contactChannelId).toBe("ch1");
86
- // The local mirror still receives the original composite id it accepts.
87
- expect(revokeMemberMock).toHaveBeenCalledWith("c1:ch1", undefined);
85
+ // The local resolver still receives the original composite id it accepts.
86
+ expect(revokeMemberMock).toHaveBeenCalledWith("c1:ch1");
88
87
  });
89
88
 
90
89
  test("always relays — never skips based on local mirror status", async () => {
@@ -48,7 +48,7 @@ mock.module("../../ipc/gateway-client.js", () => ({
48
48
  // Local-mirror primitive.
49
49
  const localResult: ContactWriteResult = {
50
50
  contact: { id: "c1" } as ContactWriteResult["contact"],
51
- channel: { id: "ch1", status: "active" } as ContactWriteResult["channel"],
51
+ channel: { id: "ch1" } as ContactWriteResult["channel"],
52
52
  };
53
53
  let mirrorCallOrder = -1;
54
54
  const upsertContactChannelMock = mock(
@@ -108,6 +108,32 @@ describe("activateMemberChannel gateway-first relay", () => {
108
108
  // The local mirror ran AFTER the gateway relay.
109
109
  expect(mirrorCallOrder).toBe(1);
110
110
  expect(upsertContactChannelMock).toHaveBeenCalledTimes(1);
111
+
112
+ // The local mirror persists identity/INFO only — no ACL columns. The
113
+ // gateway owns status/policy/verification.
114
+ const mirrorArgs = upsertContactChannelMock.mock.calls[0]![0] as Record<
115
+ string,
116
+ unknown
117
+ >;
118
+ expect(mirrorArgs).toEqual({
119
+ sourceChannel: "telegram",
120
+ externalUserId: "user-1",
121
+ externalChatId: "chat-1",
122
+ displayName: "Mom",
123
+ username: undefined,
124
+ inviteId: "inv-1",
125
+ contactId: "target-mom",
126
+ });
127
+ for (const aclKey of [
128
+ "status",
129
+ "policy",
130
+ "role",
131
+ "verifiedAt",
132
+ "verifiedVia",
133
+ ]) {
134
+ expect(aclKey in mirrorArgs).toBe(false);
135
+ }
136
+
111
137
  expect(result).toEqual({
112
138
  status: "activated",
113
139
  memberId: "ch1",
@@ -115,7 +141,7 @@ describe("activateMemberChannel gateway-first relay", () => {
115
141
  });
116
142
  });
117
143
 
118
- test("fails open and still mirrors locally when the gateway relay throws", async () => {
144
+ test("fails closed and skips the local mirror when the gateway relay throws", async () => {
119
145
  ipcThrows = true;
120
146
 
121
147
  const result = await activateMemberChannel({
@@ -126,12 +152,10 @@ describe("activateMemberChannel gateway-first relay", () => {
126
152
  });
127
153
 
128
154
  expect(ipcCalls).toHaveLength(1);
129
- expect(upsertContactChannelMock).toHaveBeenCalledTimes(1);
130
- expect(result).toEqual({
131
- status: "activated",
132
- memberId: "ch1",
133
- member: localResult,
134
- });
155
+ // Identity-only mirror would land at the schema-default unverified status, so
156
+ // a failed gateway write must not report success off it.
157
+ expect(upsertContactChannelMock).not.toHaveBeenCalled();
158
+ expect(result).toEqual({ status: "refused" });
135
159
  });
136
160
 
137
161
  test("returns the gateway channel id when the gateway verifies but the local mirror throws", async () => {
@@ -155,7 +179,7 @@ describe("activateMemberChannel gateway-first relay", () => {
155
179
  });
156
180
  });
157
181
 
158
- test("refuses when the gateway throws AND the local mirror yields no row", async () => {
182
+ test("refuses when the gateway throws even if the local mirror would have thrown", async () => {
159
183
  ipcThrows = true;
160
184
  upsertContactChannelMock.mockImplementation(() => {
161
185
  throw new Error("local mirror exploded");
@@ -167,8 +191,8 @@ describe("activateMemberChannel gateway-first relay", () => {
167
191
  externalChatId: "chat-1",
168
192
  });
169
193
 
170
- // No gateway channel (relay threw) and no local mirror: no stable id, so the
171
- // caller maps this to a non-redeemed outcome rather than crashing.
194
+ // Fail-closed: a thrown gateway write refuses before the mirror is touched.
195
+ expect(upsertContactChannelMock).not.toHaveBeenCalled();
172
196
  expect(result).toEqual({ status: "refused" });
173
197
  });
174
198
 
@@ -1,4 +1,4 @@
1
- import { and, asc, desc, eq, isNotNull, like, sql } from "drizzle-orm";
1
+ import { and, asc, desc, eq, like, sql } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
4
  import type { ChannelId } from "../channels/types.js";
@@ -96,13 +96,12 @@ function parseContact(row: typeof contacts.$inferSelect): Contact {
96
96
  id: row.id,
97
97
  displayName: row.displayName,
98
98
  notes: row.notes,
99
+ role: row.role,
99
100
  lastInteraction: null,
100
101
  interactionCount: 0,
101
102
  createdAt: row.createdAt,
102
103
  updatedAt: row.updatedAt,
103
- role: row.role as Contact["role"],
104
- contactType: (row.contactType as Contact["contactType"]) ?? "human",
105
- principalId: row.principalId,
104
+ contactType: row.contactType,
106
105
  userFile: row.userFile ?? null,
107
106
  };
108
107
  }
@@ -117,13 +116,7 @@ function parseChannel(
117
116
  address: row.address,
118
117
  isPrimary: row.isPrimary,
119
118
  externalChatId: row.externalChatId,
120
- status: row.status as ContactChannel["status"],
121
- policy: row.policy as ContactChannel["policy"],
122
- verifiedAt: row.verifiedAt,
123
- verifiedVia: row.verifiedVia,
124
119
  inviteId: row.inviteId,
125
- revokedReason: row.revokedReason,
126
- blockedReason: row.blockedReason,
127
120
  lastSeenAt: row.lastSeenAt,
128
121
  interactionCount: row.interactionCount,
129
122
  lastInteraction: row.lastInteraction,
@@ -145,6 +138,8 @@ function getChannelsForContact(contactId: string): ContactChannel[] {
145
138
 
146
139
  function withChannels(contact: Contact): ContactWithChannels {
147
140
  const channels = getChannelsForContact(contact.id);
141
+ // INFO telemetry aggregated from channel rows (not ACL): sum interaction
142
+ // counts, take the most recent interaction across channels.
148
143
  const interactionCount = channels.reduce(
149
144
  (sum, ch) => sum + ch.interactionCount,
150
145
  0,
@@ -225,7 +220,6 @@ export function upsertContact(params: {
225
220
  notes?: string | null;
226
221
  role?: ContactRole;
227
222
  contactType?: ContactType;
228
- principalId?: string | null;
229
223
  userFile?: string | null;
230
224
  channels?: SyncChannelData[];
231
225
  /** When true, conflicting channels on other contacts are reassigned to this
@@ -260,11 +254,8 @@ export function upsertContact(params: {
260
254
  updatedAt: now,
261
255
  };
262
256
  if (params.notes !== undefined) updateSet.notes = params.notes;
263
- if (params.role !== undefined) updateSet.role = params.role;
264
257
  if (params.contactType !== undefined)
265
258
  updateSet.contactType = params.contactType;
266
- if (params.principalId !== undefined)
267
- updateSet.principalId = params.principalId;
268
259
  if (params.userFile !== undefined) updateSet.userFile = params.userFile;
269
260
 
270
261
  db.update(contacts)
@@ -298,11 +289,8 @@ export function upsertContact(params: {
298
289
  updatedAt: now,
299
290
  };
300
291
  if (params.notes !== undefined) updateSet.notes = params.notes;
301
- if (params.role !== undefined) updateSet.role = params.role;
302
292
  if (params.contactType !== undefined)
303
293
  updateSet.contactType = params.contactType;
304
- if (params.principalId !== undefined)
305
- updateSet.principalId = params.principalId;
306
294
  if (params.userFile !== undefined) updateSet.userFile = params.userFile;
307
295
 
308
296
  db.update(contacts)
@@ -319,35 +307,16 @@ export function upsertContact(params: {
319
307
 
320
308
  // Create new contact
321
309
  contactId = contactId ?? uuid();
322
- // Sibling contacts sharing a principal_id must share a user_file so every
323
- // channel for one principal resolves to the same persona + journal slug.
324
- let resolvedUserFile: string | null;
325
- if (params.userFile !== undefined) {
326
- resolvedUserFile = params.userFile;
327
- } else if (params.principalId) {
328
- const sibling = db
329
- .select({ userFile: contacts.userFile })
330
- .from(contacts)
331
- .where(
332
- and(
333
- eq(contacts.principalId, params.principalId),
334
- isNotNull(contacts.userFile),
335
- ),
336
- )
337
- .get();
338
- resolvedUserFile =
339
- sibling?.userFile ?? generateUserFileSlug(params.displayName);
340
- } else {
341
- resolvedUserFile = generateUserFileSlug(params.displayName);
342
- }
310
+ const resolvedUserFile =
311
+ params.userFile !== undefined
312
+ ? params.userFile
313
+ : generateUserFileSlug(params.displayName);
343
314
  db.insert(contacts)
344
315
  .values({
345
316
  id: contactId,
346
317
  displayName: params.displayName,
347
318
  notes: params.notes ?? null,
348
- role: params.role ?? "contact",
349
319
  contactType: params.contactType ?? "human",
350
- principalId: params.principalId ?? null,
351
320
  userFile: resolvedUserFile,
352
321
  createdAt: now,
353
322
  updatedAt: now,
@@ -396,27 +365,12 @@ function syncChannels(
396
365
  .get();
397
366
 
398
367
  if (existing) {
399
- // Preserve guardian blocks: if the channel is blocked, do not overwrite
400
- // its status/policy — mirrors the guard in the cross-contact reassignment
401
- // path so a blocked channel cannot be unblocked via a same-contact sync.
402
- const isBlocked = existing.status === "blocked";
403
-
404
368
  const updateSet: Record<string, unknown> = {};
405
369
  // Self-heal legacy lowercased addresses to canonical form.
406
370
  if (existing.address !== ch.address) updateSet.address = ch.address;
407
371
  if (ch.isPrimary !== undefined) updateSet.isPrimary = ch.isPrimary;
408
372
  if (ch.externalChatId !== undefined)
409
373
  updateSet.externalChatId = ch.externalChatId;
410
- if (!isBlocked) {
411
- if (ch.status !== undefined) updateSet.status = ch.status;
412
- if (ch.policy !== undefined) updateSet.policy = ch.policy;
413
- if (ch.revokedReason !== undefined)
414
- updateSet.revokedReason = ch.revokedReason;
415
- if (ch.blockedReason !== undefined)
416
- updateSet.blockedReason = ch.blockedReason;
417
- }
418
- if (ch.verifiedAt !== undefined) updateSet.verifiedAt = ch.verifiedAt;
419
- if (ch.verifiedVia !== undefined) updateSet.verifiedVia = ch.verifiedVia;
420
374
  if (ch.inviteId !== undefined) updateSet.inviteId = ch.inviteId;
421
375
 
422
376
  if (Object.keys(updateSet).length > 0) {
@@ -434,11 +388,6 @@ function syncChannels(
434
388
 
435
389
  if (conflicting) {
436
390
  if (reassignConflicting) {
437
- // Preserve guardian blocks: if the existing channel is blocked, do not
438
- // overwrite its status/policy — a valid invite must not bypass an
439
- // explicit guardian block on a different contact.
440
- const isBlocked = conflicting.status === "blocked";
441
-
442
391
  // Reassign the channel to the target contact. Used by invite redemption
443
392
  // to bind a redeemer's existing channel identity to the invite's target.
444
393
  const reassignSet: Record<string, unknown> = {
@@ -447,17 +396,6 @@ function syncChannels(
447
396
  };
448
397
  if (ch.externalChatId !== undefined)
449
398
  reassignSet.externalChatId = ch.externalChatId;
450
- if (!isBlocked) {
451
- if (ch.status !== undefined) reassignSet.status = ch.status;
452
- if (ch.policy !== undefined) reassignSet.policy = ch.policy;
453
- if (ch.revokedReason !== undefined)
454
- reassignSet.revokedReason = ch.revokedReason;
455
- if (ch.blockedReason !== undefined)
456
- reassignSet.blockedReason = ch.blockedReason;
457
- }
458
- if (ch.verifiedAt !== undefined) reassignSet.verifiedAt = ch.verifiedAt;
459
- if (ch.verifiedVia !== undefined)
460
- reassignSet.verifiedVia = ch.verifiedVia;
461
399
  if (ch.inviteId !== undefined) reassignSet.inviteId = ch.inviteId;
462
400
 
463
401
  db.update(contactChannels)
@@ -478,10 +416,6 @@ function syncChannels(
478
416
  address: ch.address,
479
417
  isPrimary: ch.isPrimary ?? false,
480
418
  externalChatId: ch.externalChatId ?? null,
481
- status: ch.status ?? "unverified",
482
- policy: ch.policy ?? "allow",
483
- verifiedAt: ch.verifiedAt ?? null,
484
- verifiedVia: ch.verifiedVia ?? null,
485
419
  inviteId: ch.inviteId ?? null,
486
420
  createdAt: now,
487
421
  updatedAt: now,
@@ -494,7 +428,6 @@ export function searchContacts(params: {
494
428
  query?: string;
495
429
  channelAddress?: string;
496
430
  channelType?: string;
497
- role?: ContactRole;
498
431
  contactType?: ContactType;
499
432
  limit?: number;
500
433
  }): ContactWithChannels[] {
@@ -534,7 +467,6 @@ export function searchContacts(params: {
534
467
  const contact = getContactInternal(id);
535
468
  if (
536
469
  contact &&
537
- (!params.role || contact.role === params.role) &&
538
470
  (!params.contactType || contact.contactType === params.contactType) &&
539
471
  (!sanitizedQuery ||
540
472
  (contact.displayName &&
@@ -564,7 +496,6 @@ export function searchContacts(params: {
564
496
  const contact = getContactInternal(id);
565
497
  if (
566
498
  contact &&
567
- (!params.role || contact.role === params.role) &&
568
499
  (!params.contactType || contact.contactType === params.contactType)
569
500
  ) {
570
501
  results.push(contact);
@@ -577,14 +508,11 @@ export function searchContacts(params: {
577
508
  const conditions = [];
578
509
  if (params.query) {
579
510
  const sanitized = escapeLike(params.query);
580
- if (!sanitized && !params.role && !params.contactType) return [];
511
+ if (!sanitized && !params.contactType) return [];
581
512
  if (sanitized) {
582
513
  conditions.push(like(contacts.displayName, `%${sanitized}%`));
583
514
  }
584
515
  }
585
- if (params.role) {
586
- conditions.push(eq(contacts.role, params.role));
587
- }
588
516
  if (params.contactType) {
589
517
  conditions.push(eq(contacts.contactType, params.contactType));
590
518
  }
@@ -633,20 +561,16 @@ export function searchContacts(params: {
633
561
 
634
562
  export function listContacts(
635
563
  limit = 50,
636
- role?: ContactRole,
637
564
  contactType?: ContactType,
638
565
  opts?: { uncapped?: boolean },
639
566
  ): ContactWithChannels[] {
640
567
  const db = getDb();
641
568
  const effectiveLimit = opts?.uncapped ? limit : Math.min(limit, 200);
642
- const conditions = [];
643
- if (role) conditions.push(eq(contacts.role, role));
644
- if (contactType) conditions.push(eq(contacts.contactType, contactType));
645
569
  const rows = db
646
570
  .select()
647
571
  .from(contacts)
648
- .where(conditions.length === 1 ? conditions[0] : and(...conditions))
649
- .orderBy(sql`${contacts.role} = 'guardian' DESC`, desc(contacts.updatedAt))
572
+ .where(contactType ? eq(contacts.contactType, contactType) : undefined)
573
+ .orderBy(desc(contacts.updatedAt))
650
574
  .limit(effectiveLimit)
651
575
  .all();
652
576
  return rows.map((r) => withChannels(parseContact(r)));
@@ -758,7 +682,8 @@ export function findContactByAddress(
758
682
  /**
759
683
  * Find a contact by channel external chat ID. Fallback for callers that only
760
684
  * have a chat ID (no user-level address) — matches by (type, externalChatId).
761
- * No unique constraint exists on externalChatId, so ORDER BY is needed.
685
+ * No unique constraint exists on externalChatId, so ORDER BY is needed for a
686
+ * deterministic pick; channel ranking (status) is owned by the gateway now.
762
687
  */
763
688
  function findContactByChannelExternalChatId(
764
689
  channelType: string,
@@ -774,14 +699,7 @@ function findContactByChannelExternalChatId(
774
699
  eq(contactChannels.externalChatId, externalChatId),
775
700
  ),
776
701
  )
777
- .orderBy(
778
- sql`CASE ${contactChannels.status}
779
- WHEN 'active' THEN 0
780
- WHEN 'unverified' THEN 1
781
- ELSE 2
782
- END`,
783
- desc(contactChannels.updatedAt),
784
- )
702
+ .orderBy(desc(contactChannels.updatedAt), desc(contactChannels.createdAt))
785
703
  .get();
786
704
  if (!channel) return null;
787
705
  return getContactInternal(channel.contactId);
@@ -830,131 +748,10 @@ export function findContactChannel(params: {
830
748
  }
831
749
 
832
750
  /**
833
- * Find the guardian contact and their specific channel entry for a given channel type.
834
- * This is the contacts-based equivalent of getGuardianBinding(assistantId, channel).
835
- * Returns null if no guardian contact has a channel of the specified type.
836
- */
837
- export function findGuardianForChannel(
838
- channelType: string,
839
- ): { contact: Contact; channel: ContactChannel } | null {
840
- const db = getDb();
841
- const conditions = [
842
- eq(contacts.role, "guardian"),
843
- eq(contactChannels.type, channelType),
844
- eq(contactChannels.status, "active"),
845
- ];
846
- const rows = db
847
- .select({
848
- contact: contacts,
849
- channel: contactChannels,
850
- })
851
- .from(contacts)
852
- .innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
853
- .where(and(...conditions))
854
- .orderBy(desc(contactChannels.verifiedAt))
855
- .limit(1)
856
- .all();
857
-
858
- if (rows.length === 0) return null;
859
- const row = rows[0];
860
- return {
861
- contact: parseContact(row.contact),
862
- channel: parseChannel(row.channel),
863
- };
864
- }
865
-
866
- /**
867
- * List all active channels for guardian contacts.
868
- * This is the contacts-based equivalent of listActiveBindingsByAssistant(assistantId).
869
- * Joins contacts+channels with status='active' in a single query so we never
870
- * pick a guardian that has no active channels.
871
- * Returns channels ordered by most-recently-verified first.
872
- */
873
- export function listGuardianChannels(): {
874
- contact: Contact;
875
- channels: ContactChannel[];
876
- } | null {
877
- const db = getDb();
878
- const rows = db
879
- .select({
880
- contact: contacts,
881
- channel: contactChannels,
882
- })
883
- .from(contacts)
884
- .innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
885
- .where(
886
- and(eq(contacts.role, "guardian"), eq(contactChannels.status, "active")),
887
- )
888
- .orderBy(desc(contactChannels.verifiedAt))
889
- .all();
890
-
891
- if (rows.length === 0) return null;
892
-
893
- // Use the first row's contact (the guardian with the most-recently-verified
894
- // active channel) and collect all active channels for that contact.
895
- const guardian = parseContact(rows[0].contact);
896
- const channels = rows
897
- .filter((r) => r.contact.id === guardian.id)
898
- .map((r) => parseChannel(r.channel));
899
-
900
- return { contact: guardian, channels };
901
- }
902
-
903
- /**
904
- * Update a channel's access-control fields (status, policy, reasons).
905
- * Returns the updated channel, or null if the channel does not exist.
906
- */
907
- export function updateChannelStatus(
908
- channelId: string,
909
- params: {
910
- status?: ChannelStatus;
911
- policy?: ChannelPolicy;
912
- revokedReason?: string | null;
913
- blockedReason?: string | null;
914
- },
915
- ): ContactChannel | null {
916
- const db = getDb();
917
- const existing = db
918
- .select()
919
- .from(contactChannels)
920
- .where(eq(contactChannels.id, channelId))
921
- .get();
922
-
923
- if (!existing) return null;
924
-
925
- const updateSet: Record<string, unknown> = {};
926
- if (params.status !== undefined) updateSet.status = params.status;
927
- if (params.policy !== undefined) updateSet.policy = params.policy;
928
- if (params.revokedReason !== undefined)
929
- updateSet.revokedReason = params.revokedReason;
930
- if (params.blockedReason !== undefined)
931
- updateSet.blockedReason = params.blockedReason;
932
-
933
- if (Object.keys(updateSet).length > 0) {
934
- updateSet.updatedAt = Date.now();
935
- db.update(contactChannels)
936
- .set(updateSet)
937
- .where(eq(contactChannels.id, channelId))
938
- .run();
939
-
940
- const updated = db
941
- .select()
942
- .from(contactChannels)
943
- .where(eq(contactChannels.id, channelId))
944
- .get();
945
-
946
- const result = updated ? parseChannel(updated) : null;
947
- emitContactChange();
948
- return result;
949
- }
950
-
951
- return parseChannel(existing);
952
- }
953
-
954
- /**
955
- * Update a guardian contact's principalId and its channel's identity fields.
956
- * Used for healing guardian binding drift when the JWT principal no longer
957
- * matches the stored guardian binding after a DB reset.
751
+ * Heal a guardian channel's identity address when the JWT principal no longer
752
+ * matches the stored guardian binding after a DB reset. The principalId ACL
753
+ * column is gateway-owned and no longer written here; only the channel identity
754
+ * address is repaired.
958
755
  *
959
756
  * Returns false if the update would violate the unique (type, address)
960
757
  * constraint on contact_channels — e.g. when the incoming principal already
@@ -962,7 +759,7 @@ export function updateChannelStatus(
962
759
  * In that case the heal is skipped and trust stays `unknown`.
963
760
  */
964
761
  export function updateContactPrincipalAndChannel(
965
- contactId: string,
762
+ _contactId: string,
966
763
  channelId: string,
967
764
  newPrincipalId: string,
968
765
  ): boolean {
@@ -983,20 +780,13 @@ export function updateContactPrincipalAndChannel(
983
780
  return false;
984
781
  }
985
782
 
986
- db.transaction(() => {
987
- db.update(contacts)
988
- .set({ principalId: newPrincipalId, updatedAt: now })
989
- .where(eq(contacts.id, contactId))
990
- .run();
991
-
992
- db.update(contactChannels)
993
- .set({
994
- address: newPrincipalId,
995
- updatedAt: now,
996
- })
997
- .where(eq(contactChannels.id, channelId))
998
- .run();
999
- });
783
+ db.update(contactChannels)
784
+ .set({
785
+ address: newPrincipalId,
786
+ updatedAt: now,
787
+ })
788
+ .where(eq(contactChannels.id, channelId))
789
+ .run();
1000
790
 
1001
791
  emitContactChange();
1002
792
  return true;