@vellumai/assistant 0.9.0-dev.202606181735.ae6e1d8 → 0.9.0-dev.202606181935.328e5f4

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 (95) hide show
  1. package/docs/activation-funnel-telemetry.md +24 -18
  2. package/openapi.yaml +240 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +6 -8
  5. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  6. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  7. package/src/__tests__/channel-inbound-disk-pressure.test.ts +0 -2
  8. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  9. package/src/__tests__/contacts-tools.test.ts +29 -0
  10. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  11. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  12. package/src/__tests__/credential-prompt-route.test.ts +1 -0
  13. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  14. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  15. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  16. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  17. package/src/__tests__/invite-redemption-service.test.ts +0 -3
  18. package/src/__tests__/mcp-health-check.test.ts +6 -7
  19. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  20. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  21. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  22. package/src/__tests__/trusted-contact-verification.test.ts +2 -4
  23. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  24. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  25. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  26. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  27. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  28. package/src/config/feature-flag-registry.json +8 -8
  29. package/src/config/schema.ts +6 -6
  30. package/src/config/schemas/channels.ts +11 -0
  31. package/src/contacts/contact-store.ts +2 -10
  32. package/src/contacts/contacts-write.ts +1 -2
  33. package/src/contacts/types.ts +0 -1
  34. package/src/context/token-estimator.ts +1 -1
  35. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  36. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  37. package/src/daemon/handlers/config-a2a.ts +0 -2
  38. package/src/daemon/handlers/config-channels.ts +5 -10
  39. package/src/daemon/handlers/config-slack-channel.ts +20 -0
  40. package/src/daemon/lifecycle.ts +23 -12
  41. package/src/daemon/message-types/contacts.ts +0 -1
  42. package/src/events/tool-audit-listener.ts +2 -2
  43. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  44. package/src/memory/auth-fallback-events-store.ts +2 -2
  45. package/src/memory/conversation-key-store.ts +17 -2
  46. package/src/memory/db-init.ts +2 -0
  47. package/src/memory/lifecycle-events-store.ts +2 -2
  48. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  49. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  50. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  51. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
  52. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  53. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  54. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  55. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
  56. package/src/memory/migrations/index.ts +1 -0
  57. package/src/memory/onboarding-events-store.ts +3 -3
  58. package/src/memory/schema/contacts.ts +0 -1
  59. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  60. package/src/memory/skill-loaded-events-store.ts +2 -2
  61. package/src/memory/tool-executed-events-store.test.ts +7 -7
  62. package/src/notifications/destination-resolver.ts +4 -6
  63. package/src/permissions/checker.ts +22 -7
  64. package/src/permissions/ipc-risk-types.ts +10 -0
  65. package/src/platform/client.test.ts +96 -0
  66. package/src/platform/client.ts +52 -0
  67. package/src/platform/consent-cache.test.ts +159 -0
  68. package/src/platform/consent-cache.ts +107 -0
  69. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +4 -3
  70. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +6 -3
  71. package/src/plugins/defaults/advisor/config.ts +3 -7
  72. package/src/plugins/defaults/advisor/consult.ts +4 -6
  73. package/src/plugins/defaults/advisor/steering.ts +21 -5
  74. package/src/plugins/defaults/advisor/tools/advisor.ts +7 -4
  75. package/src/prompts/persona-resolver.ts +2 -2
  76. package/src/runtime/access-request-helper.ts +2 -3
  77. package/src/runtime/actor-trust-resolver.ts +1 -2
  78. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  79. package/src/runtime/channel-verification-service.ts +1 -2
  80. package/src/runtime/invite-redemption-service.ts +6 -22
  81. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  82. package/src/runtime/routes/contact-routes.ts +31 -8
  83. package/src/runtime/routes/conversation-management-routes.ts +22 -1
  84. package/src/runtime/routes/conversation-routes.ts +12 -0
  85. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  86. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  87. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  88. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  89. package/src/telemetry/usage-telemetry-reporter.test.ts +40 -25
  90. package/src/telemetry/usage-telemetry-reporter.ts +24 -23
  91. package/src/tools/registry.ts +20 -0
  92. package/src/util/platform.ts +8 -1
  93. package/src/watcher/telemetry.ts +2 -2
  94. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  95. package/src/workspace/migrations/registry.ts +2 -0
@@ -50,8 +50,9 @@ Flow, end to end:
50
50
  `assistant/src/telemetry/activation-funnel.ts`;
51
51
  - emission is best-effort (wrapped in try/catch) and never blocks or alters
52
52
  the surface-action flow;
53
- - `recordActivationEvent` respects the `getConfig().collectUsageData` opt-out
54
- gate (returns `null` / no row when disabled).
53
+ - `recordActivationEvent` respects the platform `share_analytics` consent gate
54
+ via `getCachedShareAnalytics()` (returns `null` / no row when the platform
55
+ user has not consented; default-off when there is no platform session).
55
56
  2. **Reporter flushes** every ~5 min: `usage-telemetry-reporter.ts`
56
57
  (`REPORT_INTERVAL_MS = 5 * 60 * 1000`, with a one-time
57
58
  `INITIAL_FLUSH_DELAY_MS = 30_000` after startup) POSTs unreported onboarding
@@ -172,22 +173,27 @@ When the flag is ON, the web prechat context selects
172
173
  `BOOTSTRAP-ACTIVATION-RAIL.md` as the bootstrap template, and the daemon marks
173
174
  the conversation in `activation_sessions` on first build of the system prompt.
174
175
 
175
- ### 5.2 Confirm usage-data collection is enabled — and do NOT run in dev mode
176
-
177
- Two gates must both be satisfied or no rows reach BigQuery:
178
-
179
- 1. `recordActivationEvent` no-ops when `config.collectUsageData` is false, so the
180
- dev build/config must have usage-data collection enabled, or no rows are
181
- written to SQLite.
182
- 2. **Dev mode disables the flush entirely.** `assistant/src/daemon/lifecycle.ts`
183
- computes the effective setting as `collectUsageData = !isDevMode &&
184
- config.collectUsageData`, so when the daemon runs in dev mode (`VELLUM_DEV=1`)
185
- the `UsageTelemetryReporter` is never started — rows accumulate in SQLite but
186
- are never POSTed, and the "wait/restart for flush" steps below will never
187
- reach BigQuery. For an end-to-end smoke test, run the daemon **outside dev
188
- mode** (so the reporter starts), or explicitly invoke the reporter's
189
- `flush()` via a dev hook. The SQLite rows can still be inspected directly in
190
- dev mode, but the BigQuery verification (§6) requires a real flush.
176
+ ### 5.2 Confirm share_analytics consent is on — and pick the right daemon mode
177
+
178
+ Two distinct concerns: whether record-time rows reach SQLite (consent gate), and
179
+ whether those rows reach BigQuery (flush gate, dev-disabled).
180
+
181
+ 1. `recordActivationEvent` no-ops when the platform `share_analytics` consent is
182
+ off (it reads `getCachedShareAnalytics()`), and the consent is **default-off
183
+ when there is no platform session**. So the dev session must be signed in to
184
+ the platform with `share_analytics` consent enabled, or no rows are written to
185
+ SQLite. The consent cache is refreshed by `startConsentRefresh()` in
186
+ `assistant/src/daemon/lifecycle.ts`, which runs **regardless of dev mode** — so
187
+ a dev session signed in with `share_analytics` consent enabled writes
188
+ record-time rows to SQLite, where they can be inspected directly.
189
+ 2. **Dev mode disables the flush entirely.** When the daemon runs in dev mode
190
+ (`VELLUM_DEV=1`), `assistant/src/daemon/lifecycle.ts` never starts the
191
+ `UsageTelemetryReporter` — rows accumulate in SQLite (consent permitting) but
192
+ are never POSTed, and the "wait/restart for flush" steps below will never reach
193
+ BigQuery. For an end-to-end smoke test, run the daemon **outside dev mode** (so
194
+ the reporter starts), or explicitly invoke the reporter's `flush()` via a dev
195
+ hook. The SQLite rows can be inspected directly in dev mode, but the BigQuery
196
+ verification (§6) requires a real flush.
191
197
 
192
198
  ### 5.3 Start a fresh activation conversation and capture its id
193
199
 
package/openapi.yaml CHANGED
@@ -6152,6 +6152,12 @@ paths:
6152
6152
  description: Only standard conversations are created by this endpoint
6153
6153
  type: string
6154
6154
  const: standard
6155
+ title:
6156
+ description:
6157
+ Explicit title for the conversation. When provided on creation, it is persisted as a user-set title (never
6158
+ overwritten by the auto-titler). Used by flows that mint a conversation up-front and don't want an
6159
+ auto-generated title.
6160
+ type: string
6155
6161
  responses:
6156
6162
  "200":
6157
6163
  description: Successful response
@@ -13839,6 +13845,11 @@ paths:
13839
13845
  type: string
13840
13846
  botUsername:
13841
13847
  type: string
13848
+ threadMode:
13849
+ type: string
13850
+ enum:
13851
+ - mention_only
13852
+ - mention_then_thread
13842
13853
  error:
13843
13854
  type: string
13844
13855
  warning:
@@ -13849,6 +13860,7 @@ paths:
13849
13860
  - hasAppToken
13850
13861
  - hasUserToken
13851
13862
  - connected
13863
+ - threadMode
13852
13864
  additionalProperties: false
13853
13865
  get:
13854
13866
  operationId: integrations_slack_channel_config_get
@@ -13884,6 +13896,11 @@ paths:
13884
13896
  type: string
13885
13897
  botUsername:
13886
13898
  type: string
13899
+ threadMode:
13900
+ type: string
13901
+ enum:
13902
+ - mention_only
13903
+ - mention_then_thread
13887
13904
  error:
13888
13905
  type: string
13889
13906
  warning:
@@ -13894,6 +13911,71 @@ paths:
13894
13911
  - hasAppToken
13895
13912
  - hasUserToken
13896
13913
  - connected
13914
+ - threadMode
13915
+ additionalProperties: false
13916
+ patch:
13917
+ operationId: integrations_slack_channel_config_patch
13918
+ summary: Update Slack channel settings
13919
+ description: Update Slack channel behavior settings (e.g. thread mode).
13920
+ tags:
13921
+ - integrations
13922
+ requestBody:
13923
+ required: true
13924
+ content:
13925
+ application/json:
13926
+ schema:
13927
+ type: object
13928
+ properties:
13929
+ threadMode:
13930
+ type: string
13931
+ enum:
13932
+ - mention_only
13933
+ - mention_then_thread
13934
+ description: Controls whether the bot follows threads after an initial @mention
13935
+ responses:
13936
+ "200":
13937
+ description: Successful response
13938
+ content:
13939
+ application/json:
13940
+ schema:
13941
+ type: object
13942
+ properties:
13943
+ success:
13944
+ type: boolean
13945
+ hasBotToken:
13946
+ type: boolean
13947
+ hasAppToken:
13948
+ type: boolean
13949
+ hasUserToken:
13950
+ type: boolean
13951
+ connected:
13952
+ type: boolean
13953
+ teamId:
13954
+ type: string
13955
+ teamName:
13956
+ type: string
13957
+ teamUrl:
13958
+ type: string
13959
+ botUserId:
13960
+ type: string
13961
+ botUsername:
13962
+ type: string
13963
+ threadMode:
13964
+ type: string
13965
+ enum:
13966
+ - mention_only
13967
+ - mention_then_thread
13968
+ error:
13969
+ type: string
13970
+ warning:
13971
+ type: string
13972
+ required:
13973
+ - success
13974
+ - hasBotToken
13975
+ - hasAppToken
13976
+ - hasUserToken
13977
+ - connected
13978
+ - threadMode
13897
13979
  additionalProperties: false
13898
13980
  post:
13899
13981
  operationId: integrations_slack_channel_config_post
@@ -13945,6 +14027,11 @@ paths:
13945
14027
  type: string
13946
14028
  botUsername:
13947
14029
  type: string
14030
+ threadMode:
14031
+ type: string
14032
+ enum:
14033
+ - mention_only
14034
+ - mention_then_thread
13948
14035
  error:
13949
14036
  type: string
13950
14037
  warning:
@@ -13955,6 +14042,7 @@ paths:
13955
14042
  - hasAppToken
13956
14043
  - hasUserToken
13957
14044
  - connected
14045
+ - threadMode
13958
14046
  additionalProperties: false
13959
14047
  /v1/integrations/telegram/commands:
13960
14048
  post:
@@ -14569,6 +14657,54 @@ paths:
14569
14657
  responses:
14570
14658
  "200":
14571
14659
  description: Successful response
14660
+ content:
14661
+ application/json:
14662
+ schema:
14663
+ type: object
14664
+ properties:
14665
+ servers:
14666
+ type: array
14667
+ items:
14668
+ type: object
14669
+ properties:
14670
+ id:
14671
+ type: string
14672
+ status:
14673
+ type: string
14674
+ transport:
14675
+ type: object
14676
+ properties:
14677
+ type:
14678
+ type: string
14679
+ enum:
14680
+ - stdio
14681
+ - sse
14682
+ - streamable-http
14683
+ required:
14684
+ - type
14685
+ additionalProperties: {}
14686
+ enabled:
14687
+ type: boolean
14688
+ defaultRiskLevel:
14689
+ type: string
14690
+ allowedTools:
14691
+ type: array
14692
+ items:
14693
+ type: string
14694
+ blockedTools:
14695
+ type: array
14696
+ items:
14697
+ type: string
14698
+ required:
14699
+ - id
14700
+ - status
14701
+ - transport
14702
+ - enabled
14703
+ - defaultRiskLevel
14704
+ additionalProperties: false
14705
+ required:
14706
+ - servers
14707
+ additionalProperties: false
14572
14708
  /v1/internal/mcp/reload:
14573
14709
  post:
14574
14710
  operationId: internal_mcp_reload_post
@@ -14600,6 +14736,104 @@ paths:
14600
14736
  responses:
14601
14737
  "200":
14602
14738
  description: Successful response
14739
+ /v1/internal/mcp/tools-summary:
14740
+ get:
14741
+ operationId: internal_mcp_toolssummary_get
14742
+ summary: Per-server MCP tool counts and token estimates
14743
+ description:
14744
+ Returns registered tool counts, individual tool details, and estimated token overhead for each connected
14745
+ MCP server.
14746
+ tags:
14747
+ - internal
14748
+ responses:
14749
+ "200":
14750
+ description: Successful response
14751
+ content:
14752
+ application/json:
14753
+ schema:
14754
+ type: object
14755
+ properties:
14756
+ servers:
14757
+ type: array
14758
+ items:
14759
+ type: object
14760
+ properties:
14761
+ serverId:
14762
+ type: string
14763
+ toolCount:
14764
+ type: number
14765
+ estimatedTokens:
14766
+ type: number
14767
+ tools:
14768
+ type: array
14769
+ items:
14770
+ type: object
14771
+ properties:
14772
+ name:
14773
+ type: string
14774
+ description:
14775
+ type: string
14776
+ estimatedTokens:
14777
+ type: number
14778
+ required:
14779
+ - name
14780
+ - description
14781
+ - estimatedTokens
14782
+ additionalProperties: false
14783
+ required:
14784
+ - serverId
14785
+ - toolCount
14786
+ - estimatedTokens
14787
+ - tools
14788
+ additionalProperties: false
14789
+ totalToolCount:
14790
+ type: number
14791
+ totalEstimatedTokens:
14792
+ type: number
14793
+ required:
14794
+ - servers
14795
+ - totalToolCount
14796
+ - totalEstimatedTokens
14797
+ additionalProperties: false
14798
+ /v1/internal/mcp/update:
14799
+ post:
14800
+ operationId: internal_mcp_update_post
14801
+ summary: Update an MCP server configuration
14802
+ description: Updates fields on an existing MCP server config entry and triggers a reload.
14803
+ tags:
14804
+ - internal
14805
+ requestBody:
14806
+ required: true
14807
+ content:
14808
+ application/json:
14809
+ schema:
14810
+ type: object
14811
+ properties:
14812
+ name:
14813
+ type: string
14814
+ enabled:
14815
+ type: boolean
14816
+ defaultRiskLevel:
14817
+ type: string
14818
+ maxTools:
14819
+ type: number
14820
+ allowedTools:
14821
+ anyOf:
14822
+ - type: array
14823
+ items:
14824
+ type: string
14825
+ - type: "null"
14826
+ blockedTools:
14827
+ anyOf:
14828
+ - type: array
14829
+ items:
14830
+ type: string
14831
+ - type: "null"
14832
+ required:
14833
+ - name
14834
+ responses:
14835
+ "200":
14836
+ description: Successful response
14603
14837
  /v1/internal/oauth/callback:
14604
14838
  post:
14605
14839
  operationId: internal_oauth_callback_post
@@ -16854,6 +17088,12 @@ paths:
16854
17088
  type: array
16855
17089
  items:
16856
17090
  type: string
17091
+ title:
17092
+ description:
17093
+ Explicit title for the conversation minted on this first message. Persisted as a user-set title (never
17094
+ overwritten by the auto-titler). Used by onboarding flows that mint a conversation behind the
17095
+ scenes.
17096
+ type: string
16857
17097
  required:
16858
17098
  - tools
16859
17099
  - tasks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.9.0-dev.202606181735.ae6e1d8",
3
+ "version": "0.9.0-dev.202606181935.328e5f4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -43,7 +43,6 @@ const PHONE = "+15559871234";
43
43
  function makeContact(
44
44
  role: "guardian" | "contact" = "contact",
45
45
  status: "unverified" | "active" = "unverified",
46
- externalUserId: string | null = null,
47
46
  ): ContactWithChannels {
48
47
  const channelId = "ch-test";
49
48
  return {
@@ -64,7 +63,6 @@ function makeContact(
64
63
  contactId: "contact-test",
65
64
  type: "phone",
66
65
  address: PHONE,
67
- externalUserId,
68
66
  externalChatId: null,
69
67
  isPrimary: true,
70
68
  status,
@@ -94,7 +92,7 @@ describe("resolveActorTrust — address fallback", () => {
94
92
 
95
93
  test("finds unverified channel via address when externalUserId is null", () => {
96
94
  // Simulate a contact registered by name-capture: address set, externalUserId null.
97
- _byAddress = makeContact("contact", "unverified", null);
95
+ _byAddress = makeContact("contact", "unverified");
98
96
 
99
97
  const result = resolveActorTrust({
100
98
  assistantId: "asst-1",
@@ -112,7 +110,7 @@ describe("resolveActorTrust — address fallback", () => {
112
110
  });
113
111
 
114
112
  test("address lookup is the sole member resolution path", () => {
115
- _byAddress = makeContact("contact", "active", null);
113
+ _byAddress = makeContact("contact", "active");
116
114
 
117
115
  const result = resolveActorTrust({
118
116
  assistantId: "asst-1",
@@ -142,7 +140,7 @@ describe("resolveActorTrust — address fallback", () => {
142
140
  test("address-found active channel elevates trust to trusted_contact", () => {
143
141
  // An active channel found via address (e.g. after manual verify without externalUserId set)
144
142
  // should still yield trusted_contact trust class.
145
- _byAddress = makeContact("contact", "active", null);
143
+ _byAddress = makeContact("contact", "active");
146
144
 
147
145
  const result = resolveActorTrust({
148
146
  assistantId: "asst-1",
@@ -159,7 +157,7 @@ describe("resolveActorTrust — address fallback", () => {
159
157
  test("pending-status member is classified as unverified_contact", () => {
160
158
  // Mirrors the unverified branch but for `pending` status (e.g. a phone
161
159
  // contact registered by name-capture awaiting the DTMF challenge).
162
- const contact = makeContact("contact", "unverified", PHONE);
160
+ const contact = makeContact("contact", "unverified");
163
161
  // Override status to "pending" — makeContact only accepts unverified/active
164
162
  contact.channels[0]!.status = "pending";
165
163
  _byAddress = contact;
@@ -178,7 +176,7 @@ describe("resolveActorTrust — address fallback", () => {
178
176
  test("blocked-status member is classified as unknown (not unverified_contact)", () => {
179
177
  // Hard-deny statuses (blocked, revoked) stay `unknown` — admission-layer
180
178
  // re-checks channel.status and emits the hard-deny reasons.
181
- const contact = makeContact("contact", "unverified", PHONE);
179
+ const contact = makeContact("contact", "unverified");
182
180
  contact.channels[0]!.status = "blocked";
183
181
  _byAddress = contact;
184
182
 
@@ -194,7 +192,7 @@ describe("resolveActorTrust — address fallback", () => {
194
192
  });
195
193
 
196
194
  test("revoked-status member is classified as unknown", () => {
197
- const contact = makeContact("contact", "unverified", PHONE);
195
+ const contact = makeContact("contact", "unverified");
198
196
  contact.channels[0]!.status = "revoked";
199
197
  _byAddress = contact;
200
198
 
@@ -7,18 +7,10 @@ mock.module("../util/logger.js", () => ({
7
7
  }),
8
8
  }));
9
9
 
10
- let collectUsageData = true;
10
+ let shareAnalytics = true;
11
11
 
12
- mock.module("../config/loader.js", () => ({
13
- getConfig: () => ({
14
- ui: {},
15
- model: "test",
16
- provider: "test",
17
- memory: { enabled: false },
18
- rateLimit: { maxRequestsPerMinute: 0 },
19
- secretDetection: { enabled: false },
20
- collectUsageData,
21
- }),
12
+ mock.module("../platform/consent-cache.js", () => ({
13
+ getCachedShareAnalytics: () => shareAnalytics,
22
14
  }));
23
15
 
24
16
  import {
@@ -53,7 +45,7 @@ const SAMPLE: AuthFallbackCount[] = [
53
45
 
54
46
  describe("auth-fallback-events-store", () => {
55
47
  beforeEach(() => {
56
- collectUsageData = true;
48
+ shareAnalytics = true;
57
49
  resetTable();
58
50
  });
59
51
 
@@ -78,8 +70,8 @@ describe("auth-fallback-events-store", () => {
78
70
  });
79
71
  });
80
72
 
81
- test("honors the collectUsageData opt-out (records nothing)", () => {
82
- collectUsageData = false;
73
+ test("honors the share_analytics opt-out (records nothing)", () => {
74
+ shareAnalytics = false;
83
75
  const recorded = recordAuthFallbackCounts(1000, 2000, SAMPLE);
84
76
  expect(recorded).toBe(0);
85
77
  expect(queryUnreportedAuthFallbackEvents(0, undefined, 100).length).toBe(0);
@@ -224,14 +224,12 @@ function ensureTestContact(): void {
224
224
  {
225
225
  type: "telegram",
226
226
  address: "telegram-user-default",
227
- externalUserId: "telegram-user-default",
228
227
  status: "active",
229
228
  policy: "allow",
230
229
  },
231
230
  {
232
231
  type: "slack",
233
232
  address: "slack-user-default",
234
- externalUserId: "slack-user-default",
235
233
  status: "active",
236
234
  policy: "allow",
237
235
  },
@@ -2054,7 +2052,6 @@ describe("requester cancel of guardian-gated pending request", () => {
2054
2052
  {
2055
2053
  type: "telegram",
2056
2054
  address: "requester-cancel-user",
2057
- externalUserId: "requester-cancel-user",
2058
2055
  status: "active",
2059
2056
  policy: "allow",
2060
2057
  },
@@ -2981,7 +2978,6 @@ describe("trusted-contact self-approval blocked before guardian approval row exi
2981
2978
  {
2982
2979
  type: "telegram",
2983
2980
  address: "tc-selfapproval-user",
2984
- externalUserId: "tc-selfapproval-user",
2985
2981
  status: "active",
2986
2982
  policy: "allow",
2987
2983
  },
@@ -97,7 +97,6 @@ function seedTrustedContact(policy: "allow" | "escalate" = "allow"): void {
97
97
  {
98
98
  type: "telegram",
99
99
  address: "telegram-user-1",
100
- externalUserId: "telegram-user-1",
101
100
  status: "active",
102
101
  policy,
103
102
  },
@@ -244,7 +243,6 @@ describe("channel inbound disk pressure gate", () => {
244
243
  {
245
244
  type: "slack",
246
245
  address: "slack-user-1",
247
- externalUserId: "slack-user-1",
248
246
  status: "active",
249
247
  policy: "allow",
250
248
  },
@@ -75,7 +75,6 @@ describe("upsertContact user_file selection", () => {
75
75
  {
76
76
  type: "vellum",
77
77
  address: "vellum-principal-abc",
78
- externalUserId: "vellum-principal-abc",
79
78
  },
80
79
  ],
81
80
  });
@@ -91,7 +90,6 @@ describe("upsertContact user_file selection", () => {
91
90
  {
92
91
  type: "slack",
93
92
  address: "u123456",
94
- externalUserId: "U123456",
95
93
  externalChatId: "D987654",
96
94
  },
97
95
  ],
@@ -109,7 +107,6 @@ describe("upsertContact user_file selection", () => {
109
107
  {
110
108
  type: "slack",
111
109
  address: "ualice",
112
- externalUserId: "UALICE",
113
110
  externalChatId: "DALICE",
114
111
  },
115
112
  ],
@@ -125,7 +122,6 @@ describe("upsertContact user_file selection", () => {
125
122
  {
126
123
  type: "slack",
127
124
  address: "ubob1",
128
- externalUserId: "UBOB1",
129
125
  externalChatId: "DBOB1",
130
126
  },
131
127
  ],
@@ -137,7 +133,6 @@ describe("upsertContact user_file selection", () => {
137
133
  {
138
134
  type: "slack",
139
135
  address: "ubob2",
140
- externalUserId: "UBOB2",
141
136
  externalChatId: "DBOB2",
142
137
  },
143
138
  ],
@@ -164,7 +159,6 @@ describe("upsertContact user_file selection", () => {
164
159
  {
165
160
  type: "phone",
166
161
  address: "+15550000",
167
- externalUserId: "+15550000",
168
162
  externalChatId: "+15550000",
169
163
  },
170
164
  ],
@@ -240,6 +240,35 @@ describe("contact_search tool", () => {
240
240
  });
241
241
  });
242
242
 
243
+ // ── search_contacts route (HTTP/IPC compat shim) ─────────────────────
244
+
245
+ describe("search_contacts route", () => {
246
+ beforeEach(clearContacts);
247
+
248
+ test("includes externalUserId (= address) on channels for older clients", () => {
249
+ const seeded = upsertFixture({
250
+ display_name: "Dana",
251
+ channels: [{ type: "slack", address: "U12345ABC" }],
252
+ });
253
+ const seededAddress = seeded.channels[0]!.address;
254
+
255
+ const searchRoute = ROUTES.find(
256
+ (r) => r.operationId === "search_contacts",
257
+ )!;
258
+ const contacts = searchRoute.handler({
259
+ body: { channelAddress: seededAddress },
260
+ }) as unknown as Array<{
261
+ channels: Array<{ address: string; externalUserId?: string }>;
262
+ }>;
263
+
264
+ expect(contacts.length).toBeGreaterThanOrEqual(1);
265
+ const channel = contacts[0]!.channels[0]!;
266
+ // The route re-derives the compat field from address, so SDK/macOS
267
+ // clients that read externalUserId keep working.
268
+ expect(channel.externalUserId).toBe(channel.address);
269
+ });
270
+ });
271
+
243
272
  // ── contact_merge ───────────────────────────────────────────────────
244
273
 
245
274
  describe("contact_merge tool", () => {
@@ -76,7 +76,6 @@ function ensureTestContact(): void {
76
76
  {
77
77
  type: "telegram",
78
78
  address: "telegram-user-default",
79
- externalUserId: "telegram-user-default",
80
79
  status: "active",
81
80
  policy: "allow",
82
81
  },
@@ -10,9 +10,11 @@ mock.module("../util/logger.js", () => ({
10
10
  }),
11
11
  }));
12
12
 
13
- // Usage-data collection is enabled so recordActivationEvent writes rows.
14
- mock.module("../config/loader.js", () => ({
15
- getConfig: () => ({ collectUsageData: true }),
13
+ // Analytics consent is granted so recordActivationEvent writes rows.
14
+ let shareAnalytics = true;
15
+
16
+ mock.module("../platform/consent-cache.js", () => ({
17
+ getCachedShareAnalytics: () => shareAnalytics,
16
18
  }));
17
19
 
18
20
  let broadcastedMessages: ServerMessage[] = [];
@@ -117,6 +119,7 @@ async function showTaggedChoice(
117
119
 
118
120
  describe("activation moment emission from ui_show surface commits", () => {
119
121
  beforeEach(() => {
122
+ shareAnalytics = true;
120
123
  broadcastedMessages = [];
121
124
  resetTables();
122
125
  });
@@ -110,6 +110,7 @@ function slackResult(
110
110
  hasAppToken: false,
111
111
  hasUserToken: false,
112
112
  connected: false,
113
+ threadMode: "mention_only",
113
114
  ...overrides,
114
115
  };
115
116
  }
@@ -44,7 +44,7 @@ describe("healGuardianBindingDrift", () => {
44
44
  const guardian = findGuardianForChannel("vellum");
45
45
  expect(guardian).not.toBeNull();
46
46
  expect(guardian!.contact.principalId).toBe("vellum-principal-old-uuid");
47
- expect(guardian!.channel.externalUserId).toBe("vellum-principal-old-uuid");
47
+ expect(guardian!.channel.address).toBe("vellum-principal-old-uuid");
48
48
  });
49
49
 
50
50
  test("no-op when principals already match", () => {
@@ -162,7 +162,6 @@ describe("inbound-message-handler trusted-contact interactivity", () => {
162
162
  {
163
163
  type: "telegram",
164
164
  address: "telegram-user-default",
165
- externalUserId: "telegram-user-default",
166
165
  status: "active",
167
166
  policy: "allow",
168
167
  },