@vellumai/assistant 0.4.30 → 0.4.32

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 (194) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +14 -8
  3. package/README.md +2 -2
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +1 -4
  6. package/package.json +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  8. package/src/__tests__/anthropic-provider.test.ts +86 -1
  9. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  10. package/src/__tests__/checker.test.ts +37 -98
  11. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
  12. package/src/__tests__/config-schema.test.ts +6 -14
  13. package/src/__tests__/conflict-policy.test.ts +76 -0
  14. package/src/__tests__/conflict-store.test.ts +14 -20
  15. package/src/__tests__/contacts-tools.test.ts +8 -61
  16. package/src/__tests__/contradiction-checker.test.ts +5 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  19. package/src/__tests__/followup-tools.test.ts +0 -30
  20. package/src/__tests__/gemini-provider.test.ts +79 -1
  21. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  22. package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
  23. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  24. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  25. package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
  26. package/src/__tests__/memory-regressions.test.ts +6 -6
  27. package/src/__tests__/openai-provider.test.ts +82 -0
  28. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  29. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  30. package/src/__tests__/recurrence-types.test.ts +0 -15
  31. package/src/__tests__/registry.test.ts +0 -10
  32. package/src/__tests__/schedule-tools.test.ts +28 -44
  33. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  34. package/src/__tests__/session-agent-loop.test.ts +0 -2
  35. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  36. package/src/__tests__/session-profile-injection.test.ts +0 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  38. package/src/__tests__/session-skill-tools.test.ts +0 -49
  39. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  40. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  41. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  42. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  43. package/src/__tests__/task-management-tools.test.ts +111 -0
  44. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  45. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  46. package/src/__tests__/twilio-config.test.ts +0 -3
  47. package/src/amazon/session.ts +30 -91
  48. package/src/approvals/guardian-decision-primitive.ts +11 -7
  49. package/src/approvals/guardian-request-resolvers.ts +5 -3
  50. package/src/calls/call-controller.ts +423 -571
  51. package/src/calls/finalize-call.ts +20 -0
  52. package/src/calls/relay-access-wait.ts +340 -0
  53. package/src/calls/relay-server.ts +269 -899
  54. package/src/calls/relay-setup-router.ts +307 -0
  55. package/src/calls/relay-verification.ts +280 -0
  56. package/src/calls/twilio-config.ts +1 -8
  57. package/src/calls/voice-control-protocol.ts +184 -0
  58. package/src/calls/voice-session-bridge.ts +1 -8
  59. package/src/config/agent-schema.ts +1 -1
  60. package/src/config/bundled-skills/contacts/SKILL.md +7 -18
  61. package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
  62. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
  63. package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
  64. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
  65. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  66. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  67. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  68. package/src/config/bundled-tool-registry.ts +0 -5
  69. package/src/config/core-schema.ts +1 -1
  70. package/src/config/env.ts +0 -10
  71. package/src/config/feature-flag-registry.json +1 -1
  72. package/src/config/loader.ts +19 -0
  73. package/src/config/memory-schema.ts +0 -10
  74. package/src/config/schema.ts +2 -2
  75. package/src/config/system-prompt.ts +6 -0
  76. package/src/contacts/contact-store.ts +36 -62
  77. package/src/contacts/contacts-write.ts +14 -3
  78. package/src/contacts/types.ts +9 -4
  79. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  80. package/src/daemon/handlers/contacts.ts +2 -2
  81. package/src/daemon/handlers/guardian-actions.ts +1 -1
  82. package/src/daemon/handlers/session-history.ts +398 -0
  83. package/src/daemon/handlers/session-user-message.ts +982 -0
  84. package/src/daemon/handlers/sessions.ts +9 -1337
  85. package/src/daemon/ipc-contract/contacts.ts +2 -2
  86. package/src/daemon/ipc-contract/sessions.ts +0 -6
  87. package/src/daemon/ipc-contract-inventory.json +0 -1
  88. package/src/daemon/lifecycle.ts +0 -29
  89. package/src/daemon/session-agent-loop.ts +1 -45
  90. package/src/daemon/session-conflict-gate.ts +21 -82
  91. package/src/daemon/session-memory.ts +7 -52
  92. package/src/daemon/session-process.ts +3 -1
  93. package/src/daemon/session-runtime-assembly.ts +18 -35
  94. package/src/heartbeat/heartbeat-service.ts +5 -1
  95. package/src/home-base/app-link-store.ts +0 -7
  96. package/src/memory/conflict-intent.ts +3 -6
  97. package/src/memory/conflict-policy.ts +34 -0
  98. package/src/memory/conflict-store.ts +10 -18
  99. package/src/memory/contradiction-checker.ts +2 -2
  100. package/src/memory/conversation-attention-store.ts +1 -1
  101. package/src/memory/conversation-store.ts +0 -51
  102. package/src/memory/db-init.ts +8 -0
  103. package/src/memory/job-handlers/conflict.ts +24 -7
  104. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  105. package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
  106. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  107. package/src/memory/migrations/index.ts +2 -0
  108. package/src/memory/migrations/registry.ts +6 -0
  109. package/src/memory/recall-cache.ts +0 -5
  110. package/src/memory/schema/calls.ts +274 -0
  111. package/src/memory/schema/contacts.ts +125 -0
  112. package/src/memory/schema/conversations.ts +129 -0
  113. package/src/memory/schema/guardian.ts +172 -0
  114. package/src/memory/schema/index.ts +8 -0
  115. package/src/memory/schema/infrastructure.ts +205 -0
  116. package/src/memory/schema/memory-core.ts +196 -0
  117. package/src/memory/schema/notifications.ts +191 -0
  118. package/src/memory/schema/tasks.ts +78 -0
  119. package/src/memory/schema.ts +1 -1402
  120. package/src/memory/slack-thread-store.ts +0 -69
  121. package/src/messaging/index.ts +0 -1
  122. package/src/messaging/types.ts +0 -38
  123. package/src/notifications/decisions-store.ts +2 -105
  124. package/src/notifications/deliveries-store.ts +0 -11
  125. package/src/notifications/preferences-store.ts +1 -58
  126. package/src/permissions/checker.ts +6 -17
  127. package/src/providers/anthropic/client.ts +6 -2
  128. package/src/providers/gemini/client.ts +13 -2
  129. package/src/providers/managed-proxy/constants.ts +55 -0
  130. package/src/providers/managed-proxy/context.ts +77 -0
  131. package/src/providers/registry.ts +112 -0
  132. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  133. package/src/runtime/guardian-action-service.ts +3 -2
  134. package/src/runtime/guardian-outbound-actions.ts +3 -3
  135. package/src/runtime/guardian-reply-router.ts +4 -4
  136. package/src/runtime/http-server.ts +83 -710
  137. package/src/runtime/http-types.ts +0 -16
  138. package/src/runtime/middleware/auth.ts +0 -12
  139. package/src/runtime/routes/app-routes.ts +33 -0
  140. package/src/runtime/routes/approval-routes.ts +32 -0
  141. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  142. package/src/runtime/routes/attachment-routes.ts +32 -0
  143. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  144. package/src/runtime/routes/call-routes.ts +41 -0
  145. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  146. package/src/runtime/routes/channel-routes.ts +70 -0
  147. package/src/runtime/routes/contact-routes.ts +371 -29
  148. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  149. package/src/runtime/routes/conversation-routes.ts +192 -194
  150. package/src/runtime/routes/debug-routes.ts +15 -0
  151. package/src/runtime/routes/events-routes.ts +16 -0
  152. package/src/runtime/routes/global-search-routes.ts +17 -2
  153. package/src/runtime/routes/guardian-action-routes.ts +23 -1
  154. package/src/runtime/routes/guardian-approval-interception.ts +2 -1
  155. package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
  156. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  157. package/src/runtime/routes/identity-routes.ts +20 -0
  158. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  159. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
  160. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  161. package/src/runtime/routes/integration-routes.ts +83 -0
  162. package/src/runtime/routes/invite-routes.ts +31 -0
  163. package/src/runtime/routes/migration-routes.ts +47 -17
  164. package/src/runtime/routes/pairing-routes.ts +18 -0
  165. package/src/runtime/routes/secret-routes.ts +20 -0
  166. package/src/runtime/routes/surface-action-routes.ts +26 -0
  167. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  168. package/src/runtime/routes/twilio-routes.ts +79 -0
  169. package/src/schedule/recurrence-types.ts +1 -11
  170. package/src/tools/followups/followup_create.ts +9 -3
  171. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  172. package/src/tools/memory/definitions.ts +0 -6
  173. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  174. package/src/tools/schedule/create.ts +1 -3
  175. package/src/tools/schedule/update.ts +9 -6
  176. package/src/twitter/session.ts +29 -77
  177. package/src/util/cookie-session.ts +114 -0
  178. package/src/workspace/git-service.ts +6 -4
  179. package/src/__tests__/conversation-routes.test.ts +0 -99
  180. package/src/__tests__/get-weather.test.ts +0 -393
  181. package/src/__tests__/task-tools.test.ts +0 -685
  182. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  183. package/src/autonomy/autonomy-resolver.ts +0 -62
  184. package/src/autonomy/autonomy-store.ts +0 -138
  185. package/src/autonomy/disposition-mapper.ts +0 -31
  186. package/src/autonomy/index.ts +0 -11
  187. package/src/autonomy/types.ts +0 -43
  188. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  189. package/src/config/bundled-skills/weather/TOOLS.json +0 -36
  190. package/src/config/bundled-skills/weather/icon.svg +0 -24
  191. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  192. package/src/contacts/startup-migration.ts +0 -21
  193. package/src/messaging/triage-engine.ts +0 -344
  194. package/src/tools/weather/service.ts +0 -712
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Voice call control marker constants, regexes, and stripping utilities.
3
+ *
4
+ * Centralizes all marker definitions so call-controller.ts and
5
+ * voice-session-bridge.ts share a single source of truth.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // String constants
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export const CALL_OPENING_MARKER = "[CALL_OPENING]";
13
+ export const CALL_OPENING_ACK_MARKER = "[CALL_OPENING_ACK]";
14
+ export const END_CALL_MARKER = "[END_CALL]";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Regexes
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export const ASK_GUARDIAN_CAPTURE_REGEX = /\[ASK_GUARDIAN:\s*(.+?)\]/;
21
+ export const ASK_GUARDIAN_MARKER_REGEX = /\[ASK_GUARDIAN:\s*.+?\]/g;
22
+
23
+ // Flexible prefix for ASK_GUARDIAN_APPROVAL — tolerates variable whitespace
24
+ // after the colon so the marker is recognized even if the model omits the
25
+ // space or inserts a newline.
26
+ export const ASK_GUARDIAN_APPROVAL_PREFIX_RE = /\[ASK_GUARDIAN_APPROVAL:\s*/;
27
+
28
+ export const USER_ANSWERED_MARKER_REGEX = /\[USER_ANSWERED:\s*.+?\]/g;
29
+ export const USER_INSTRUCTION_MARKER_REGEX = /\[USER_INSTRUCTION:\s*.+?\]/g;
30
+ export const CALL_OPENING_MARKER_REGEX = /\[CALL_OPENING\]/g;
31
+ export const CALL_OPENING_ACK_MARKER_REGEX = /\[CALL_OPENING_ACK\]/g;
32
+ export const END_CALL_MARKER_REGEX = /\[END_CALL\]/g;
33
+ export const GUARDIAN_TIMEOUT_MARKER_REGEX = /\[GUARDIAN_TIMEOUT\]/g;
34
+ export const GUARDIAN_UNAVAILABLE_MARKER_REGEX = /\[GUARDIAN_UNAVAILABLE\]/g;
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Balanced JSON extraction (used by stripGuardianApprovalMarkers)
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Extract a balanced JSON object from text that starts with an
42
+ * ASK_GUARDIAN_APPROVAL prefix. Uses brace counting with string-literal
43
+ * awareness so that `}` or `}]` inside JSON string values does not
44
+ * terminate the match prematurely.
45
+ *
46
+ * Returns the extracted JSON string, the full marker text
47
+ * (prefix + JSON + "]"), and the start index — or null when:
48
+ * - no prefix is found,
49
+ * - braces are unbalanced (still streaming), or
50
+ * - the closing `]` has not yet arrived (prevents stripping
51
+ * the marker body while the bracket leaks into TTS in a later delta).
52
+ */
53
+ export function extractBalancedJson(
54
+ text: string,
55
+ ): { json: string; fullMatch: string; startIndex: number } | null {
56
+ const prefixMatch = ASK_GUARDIAN_APPROVAL_PREFIX_RE.exec(text);
57
+ if (!prefixMatch) return null;
58
+
59
+ const prefixIdx = prefixMatch.index;
60
+ const jsonStart = prefixIdx + prefixMatch[0].length;
61
+ if (jsonStart >= text.length || text[jsonStart] !== "{") return null;
62
+
63
+ let depth = 0;
64
+ let inString = false;
65
+ let escape = false;
66
+
67
+ for (let i = jsonStart; i < text.length; i++) {
68
+ const ch = text[i];
69
+
70
+ if (escape) {
71
+ escape = false;
72
+ continue;
73
+ }
74
+
75
+ if (ch === "\\" && inString) {
76
+ escape = true;
77
+ continue;
78
+ }
79
+
80
+ if (ch === '"') {
81
+ inString = !inString;
82
+ continue;
83
+ }
84
+
85
+ if (inString) continue;
86
+
87
+ if (ch === "{") {
88
+ depth++;
89
+ } else if (ch === "}") {
90
+ depth--;
91
+ if (depth === 0) {
92
+ const jsonEnd = i + 1;
93
+ const json = text.slice(jsonStart, jsonEnd);
94
+ // Skip any whitespace between the closing '}' and the expected ']'.
95
+ // Models sometimes emit formatted markers with spaces or newlines
96
+ // before the bracket (e.g. `{ ... }\n]` or `{ ... } ]`).
97
+ let bracketIdx = jsonEnd;
98
+ while (bracketIdx < text.length && /\s/.test(text[bracketIdx])) {
99
+ bracketIdx++;
100
+ }
101
+ // Require the closing ']' to be present before considering this
102
+ // a complete match. If it hasn't arrived yet (streaming), return
103
+ // null so the caller keeps buffering.
104
+ if (bracketIdx >= text.length || text[bracketIdx] !== "]") {
105
+ return null;
106
+ }
107
+ const fullMatchEnd = bracketIdx + 1;
108
+ const fullMatch = text.slice(prefixIdx, fullMatchEnd);
109
+ return { json, fullMatch, startIndex: prefixIdx };
110
+ }
111
+ }
112
+ }
113
+
114
+ return null; // Unbalanced braces — still streaming
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Marker stripping
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Strip all balanced ASK_GUARDIAN_APPROVAL markers from text, handling
123
+ * nested braces, string literals, and flexible whitespace correctly.
124
+ * Only strips complete markers (prefix + balanced JSON + closing `]`).
125
+ */
126
+ export function stripGuardianApprovalMarkers(text: string): string {
127
+ let result = text;
128
+ for (;;) {
129
+ const match = extractBalancedJson(result);
130
+ if (!match) break;
131
+ result =
132
+ result.slice(0, match.startIndex) +
133
+ result.slice(match.startIndex + match.fullMatch.length);
134
+ }
135
+ return result;
136
+ }
137
+
138
+ export function stripInternalSpeechMarkers(text: string): string {
139
+ let result = stripGuardianApprovalMarkers(text);
140
+ result = result
141
+ .replace(ASK_GUARDIAN_MARKER_REGEX, "")
142
+ .replace(USER_ANSWERED_MARKER_REGEX, "")
143
+ .replace(USER_INSTRUCTION_MARKER_REGEX, "")
144
+ .replace(CALL_OPENING_MARKER_REGEX, "")
145
+ .replace(CALL_OPENING_ACK_MARKER_REGEX, "")
146
+ .replace(END_CALL_MARKER_REGEX, "")
147
+ .replace(GUARDIAN_TIMEOUT_MARKER_REGEX, "")
148
+ .replace(GUARDIAN_UNAVAILABLE_MARKER_REGEX, "");
149
+ return result;
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Control marker detection
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /**
157
+ * All known control marker prefixes. Used by couldBeControlMarker to detect
158
+ * whether a buffer that starts with `[` might be the beginning of a control
159
+ * marker (and should therefore be held rather than flushed to TTS).
160
+ */
161
+ const CONTROL_MARKER_STRINGS = [
162
+ "[ASK_GUARDIAN_APPROVAL:",
163
+ "[ASK_GUARDIAN:",
164
+ "[USER_ANSWERED:",
165
+ "[USER_INSTRUCTION:",
166
+ "[CALL_OPENING]",
167
+ "[CALL_OPENING_ACK]",
168
+ "[END_CALL]",
169
+ "[GUARDIAN_TIMEOUT]",
170
+ "[GUARDIAN_UNAVAILABLE]",
171
+ ];
172
+
173
+ /**
174
+ * Check whether `text` could be a partial or complete control marker.
175
+ *
176
+ * Returns true if any known marker string is a prefix of `text`
177
+ * (text starts with the marker) or `text` is a prefix of a marker
178
+ * (the marker starts with text — i.e. text is still being streamed).
179
+ */
180
+ export function couldBeControlMarker(text: string): boolean {
181
+ return CONTROL_MARKER_STRINGS.some(
182
+ (marker) => marker.startsWith(text) || text.startsWith(marker),
183
+ );
184
+ }
@@ -24,14 +24,7 @@ import { checkIngressForSecrets } from "../security/secret-ingress.js";
24
24
  import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
25
25
  import { IngressBlockedError } from "../util/errors.js";
26
26
  import { getLogger } from "../util/logger.js";
27
-
28
- /**
29
- * Matches the exact `[CALL_OPENING]` marker that call-controller sends for
30
- * the initial greeting turn. We replace it with a benign content string before
31
- * persisting so the marker never appears in session history where it could
32
- * retrigger opener behavior after a barge-in interruption.
33
- */
34
- const CALL_OPENING_MARKER = "[CALL_OPENING]";
27
+ import { CALL_OPENING_MARKER } from "./voice-control-protocol.js";
35
28
 
36
29
  const log = getLogger("voice-session-bridge");
37
30
 
@@ -82,7 +82,7 @@ export const SwarmConfigSchema = z.object({
82
82
  coder: z.number().int().positive().optional(),
83
83
  reviewer: z.number().int().positive().optional(),
84
84
  })
85
- .default({} as any),
85
+ .default({}),
86
86
  plannerModelIntent: z
87
87
  .enum(["latency-optimized", "quality-optimized", "vision-optimized"], {
88
88
  error: "swarm.plannerModelIntent must be a valid model intent",
@@ -25,10 +25,7 @@ curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts" \
25
25
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
26
26
  -d '{
27
27
  "displayName": "<name>",
28
- "relationship": "<relationship>",
29
- "importance": 0.5,
30
- "responseExpectation": "<speed>",
31
- "preferredTone": "<tone>",
28
+ "notes": "<free-text notes about this contact>",
32
29
  "channels": [
33
30
  {
34
31
  "type": "<channel_type>",
@@ -48,15 +45,12 @@ Required fields:
48
45
  Optional fields:
49
46
 
50
47
  - `id` -- contact ID to update (omit to create new, or auto-match by channel address)
51
- - `relationship` -- e.g. colleague, friend, manager, client, family
52
- - `importance` -- score from 0 to 1 (default 0.5), higher means more important
53
- - `responseExpectation` -- expected response speed: immediate, within_hours, within_day, casual
54
- - `preferredTone` -- communication tone: formal, casual, friendly, professional
48
+ - `notes` -- free-text notes about this contact (e.g. relationship, communication preferences, response expectations)
55
49
  - `channels` -- list of communication channels
56
50
 
57
51
  ### Search contacts
58
52
 
59
- Search for contacts by name, channel address, relationship type, or other criteria using the gateway API.
53
+ Search for contacts by name, channel address, or other criteria using the gateway API.
60
54
 
61
55
  ```bash
62
56
  curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/contacts?query=<search_term>" \
@@ -68,7 +62,6 @@ Optional query parameters:
68
62
  - `query` -- search by display name (partial match)
69
63
  - `channelAddress` -- search by channel address (email, phone, handle)
70
64
  - `channelType` -- filter by channel type when searching by address
71
- - `relationship` -- filter by relationship type (exact match)
72
65
  - `limit` -- maximum results to return (default 50, max 100)
73
66
 
74
67
  ### Merge contacts
@@ -76,7 +69,7 @@ Optional query parameters:
76
69
  When you discover two contacts are the same person (e.g. same person on email and Slack), merge them to consolidate. Merging:
77
70
 
78
71
  - Combines all channels from both contacts
79
- - Keeps the higher importance score
72
+ - Merges notes from both contacts
80
73
  - Sums interaction counts
81
74
  - Deletes the donor contact
82
75
 
@@ -404,10 +397,7 @@ Replace `<invite_id>` with the invite's `id` from the list response. The same re
404
397
  ## Contact Fields
405
398
 
406
399
  - **displayName** -- the contact's name (required)
407
- - **relationship** -- e.g. colleague, friend, manager, client, family
408
- - **importance** -- score from 0 to 1 (default 0.5), higher means more important
409
- - **responseExpectation** -- expected response speed: immediate, within_hours, within_day, casual
410
- - **preferredTone** -- communication tone: formal, casual, friendly, professional
400
+ - **notes** -- free-text notes about this contact (e.g. relationship, communication preferences, response expectations)
411
401
  - **channels** -- list of communication channels (email, slack, whatsapp, phone, telegram, discord, other)
412
402
 
413
403
  ### Channel Types
@@ -446,9 +436,8 @@ Each channel has:
446
436
  ## Tips
447
437
 
448
438
  - Use contact search with `channelAddress` to find contacts by their email, phone, or handle.
449
- - When creating follow-ups, provide a `contact_id` to link the follow-up to a specific contact for grace period calculations.
450
- - Contacts with higher importance scores get shorter default response deadlines.
451
- - When merging contacts, the surviving contact keeps the higher importance score and gains all channels from the donor.
439
+ - When creating follow-ups, provide a `contact_id` to link the follow-up to a specific contact.
440
+ - When merging contacts, the surviving contact gains all channels and merged notes from the donor.
452
441
 
453
442
  ## Typical Workflows
454
443
 
@@ -17,21 +17,9 @@
17
17
  "type": "string",
18
18
  "description": "Display name for the contact"
19
19
  },
20
- "relationship": {
20
+ "notes": {
21
21
  "type": "string",
22
- "description": "Relationship type (e.g. colleague, friend, manager, client, family)"
23
- },
24
- "importance": {
25
- "type": "number",
26
- "description": "Importance score 0-1 (default 0.5). Higher = more important."
27
- },
28
- "response_expectation": {
29
- "type": "string",
30
- "description": "Expected response speed (e.g. immediate, within_hours, within_day, casual)"
31
- },
32
- "preferred_tone": {
33
- "type": "string",
34
- "description": "Preferred communication tone (e.g. formal, casual, friendly, professional)"
22
+ "description": "Free-text notes about this contact (e.g. relationship, communication preferences, response expectations)"
35
23
  },
36
24
  "channels": {
37
25
  "type": "array",
@@ -76,7 +64,7 @@
76
64
  },
77
65
  {
78
66
  "name": "contact_search",
79
- "description": "Search for contacts by name, channel address, relationship type, or other criteria. Returns matching contacts with their channel information.",
67
+ "description": "Search for contacts by name, channel address, or other criteria. Returns matching contacts with their channel information.",
80
68
  "category": "contacts",
81
69
  "risk": "low",
82
70
  "input_schema": {
@@ -94,10 +82,6 @@
94
82
  "type": "string",
95
83
  "description": "Filter by channel type when searching by address (email, slack, whatsapp, phone, etc.)"
96
84
  },
97
- "relationship": {
98
- "type": "string",
99
- "description": "Filter by relationship type (exact match)"
100
- },
101
85
  "limit": {
102
86
  "type": "number",
103
87
  "description": "Maximum results to return (default 20, max 100)"
@@ -114,7 +98,7 @@
114
98
  },
115
99
  {
116
100
  "name": "contact_merge",
117
- "description": "Merge two contacts when you discover they are the same person (e.g. same person on email and Slack). Combines channels, keeps the higher importance, and deletes the donor contact.",
101
+ "description": "Merge two contacts when you discover they are the same person (e.g. same person on email and Slack). Combines channels, merges notes, and deletes the donor contact.",
118
102
  "category": "contacts",
119
103
  "risk": "medium",
120
104
  "input_schema": {
@@ -17,8 +17,7 @@ interface ContactChannel {
17
17
  interface ContactResponse {
18
18
  id: string;
19
19
  displayName: string;
20
- relationship: string | null;
21
- importance: number;
20
+ notes: string | null;
22
21
  interactionCount: number;
23
22
  channels: ContactChannel[];
24
23
  }
@@ -96,9 +95,8 @@ export async function executeContactMerge(
96
95
  ``,
97
96
  `Surviving contact (${merged.id}):`,
98
97
  ` Name: ${merged.displayName}`,
99
- ` Importance: ${merged.importance.toFixed(2)}`,
100
98
  ` Interactions: ${merged.interactionCount}`,
101
- merged.relationship ? ` Relationship: ${merged.relationship}` : null,
99
+ merged.notes ? ` Notes: ${merged.notes}` : null,
102
100
  merged.channels.length > 0 ? ` Channels:\n${channelList}` : null,
103
101
  ``,
104
102
  `Deleted contact: ${mergeContact.displayName} (${mergeId})`,
@@ -16,20 +16,16 @@ interface ContactChannel {
16
16
  interface ContactResponse {
17
17
  id: string;
18
18
  displayName: string;
19
- relationship: string | null;
20
- importance: number;
19
+ notes: string | null;
21
20
  interactionCount: number;
22
21
  channels: ContactChannel[];
23
22
  }
24
23
 
25
24
  function formatContactSummary(c: ContactResponse): string {
26
25
  const parts = [`- **${c.displayName}** (ID: ${c.id})`];
27
- if (c.relationship) parts.push(` Relationship: ${c.relationship}`);
28
- parts.push(
29
- ` Importance: ${c.importance.toFixed(2)} | Interactions: ${
30
- c.interactionCount
31
- }`,
32
- );
26
+ if (c.notes) parts.push(` Notes: ${c.notes}`);
27
+ if (c.interactionCount > 0)
28
+ parts.push(` Interactions: ${c.interactionCount}`);
33
29
  if (c.channels.length > 0) {
34
30
  const channelList = c.channels
35
31
  .map((ch) => `${ch.type}:${ch.address}${ch.isPrimary ? "*" : ""}`)
@@ -46,13 +42,12 @@ export async function executeContactSearch(
46
42
  const query = input.query as string | undefined;
47
43
  const channelAddress = input.channel_address as string | undefined;
48
44
  const channelType = input.channel_type as string | undefined;
49
- const relationship = input.relationship as string | undefined;
50
45
  const limit = input.limit as number | undefined;
51
46
 
52
- if (!query && !channelAddress && !relationship) {
47
+ if (!query && !channelAddress) {
53
48
  return {
54
49
  content:
55
- "Error: At least one search criterion is required (query, channel_address, or relationship)",
50
+ "Error: At least one search criterion is required (query or channel_address)",
56
51
  isError: true,
57
52
  };
58
53
  }
@@ -62,7 +57,6 @@ export async function executeContactSearch(
62
57
  if (query) params.set("query", query);
63
58
  if (channelAddress) params.set("channelAddress", channelAddress);
64
59
  if (channelType) params.set("channelType", channelType);
65
- if (relationship) params.set("relationship", relationship);
66
60
  if (limit !== undefined) params.set("limit", String(limit));
67
61
 
68
62
  const qs = params.toString();
@@ -16,21 +16,14 @@ interface ContactChannel {
16
16
  interface ContactResponse {
17
17
  id: string;
18
18
  displayName: string;
19
- relationship: string | null;
20
- importance: number;
21
- responseExpectation: string | null;
22
- preferredTone: string | null;
19
+ notes: string | null;
23
20
  interactionCount: number;
24
21
  channels: ContactChannel[];
25
22
  }
26
23
 
27
24
  function formatContact(c: ContactResponse): string {
28
25
  const lines = [`Contact ${c.id}`, ` Name: ${c.displayName}`];
29
- if (c.relationship) lines.push(` Relationship: ${c.relationship}`);
30
- lines.push(` Importance: ${c.importance.toFixed(2)}`);
31
- if (c.responseExpectation)
32
- lines.push(` Response expectation: ${c.responseExpectation}`);
33
- if (c.preferredTone) lines.push(` Preferred tone: ${c.preferredTone}`);
26
+ if (c.notes) lines.push(` Notes: ${c.notes}`);
34
27
  if (c.interactionCount > 0)
35
28
  lines.push(` Interactions: ${c.interactionCount}`);
36
29
  if (c.channels.length > 0) {
@@ -59,17 +52,6 @@ export async function executeContactUpsert(
59
52
  };
60
53
  }
61
54
 
62
- const importance = input.importance as number | undefined;
63
- if (
64
- importance !== undefined &&
65
- (typeof importance !== "number" || importance < 0 || importance > 1)
66
- ) {
67
- return {
68
- content: "Error: importance must be a number between 0 and 1",
69
- isError: true,
70
- };
71
- }
72
-
73
55
  const rawChannels = input.channels as
74
56
  | Array<{ type: string; address: string; is_primary?: boolean }>
75
57
  | undefined;
@@ -86,10 +68,7 @@ export async function executeContactUpsert(
86
68
  }>("/v1/contacts", {
87
69
  id: input.id as string | undefined,
88
70
  displayName: displayName.trim(),
89
- relationship: input.relationship as string | undefined,
90
- importance,
91
- responseExpectation: input.response_expectation as string | undefined,
92
- preferredTone: input.preferred_tone as string | undefined,
71
+ notes: input.notes as string | undefined,
93
72
  channels,
94
73
  });
95
74
 
@@ -29,10 +29,6 @@
29
29
  "type": "string",
30
30
  "description": "Optional recurrence schedule ID to fire a reminder when overdue"
31
31
  },
32
- "reminder_cron_id": {
33
- "type": "string",
34
- "description": "Deprecated alias for reminder_schedule_id. Use reminder_schedule_id instead."
35
- },
36
32
  "reason": {
37
33
  "type": "string",
38
34
  "description": "Brief non-technical explanation of why this tool is being called"
@@ -63,7 +63,7 @@ Exclusions (EXDATE, EXRULE) always take precedence over inclusions (RRULE, RDATE
63
63
 
64
64
  ## Tool Input
65
65
 
66
- Use `syntax` + `expression` to specify the schedule type explicitly, or just `expression` to auto-detect. The legacy `cron_expression` field is still accepted as a cron alias.
66
+ Use `syntax` + `expression` to specify the schedule type explicitly, or just `expression` to auto-detect.
67
67
 
68
68
  ## Lifecycle
69
69
 
@@ -20,11 +20,7 @@
20
20
  },
21
21
  "expression": {
22
22
  "type": "string",
23
- "description": "Canonical schedule expression (cron or RRULE). Preferred over cron_expression."
24
- },
25
- "cron_expression": {
26
- "type": "string",
27
- "description": "Deprecated alias for expression with cron syntax. Use expression + syntax instead."
23
+ "description": "Schedule expression (cron or RRULE)"
28
24
  },
29
25
  "timezone": {
30
26
  "type": "string",
@@ -97,11 +93,7 @@
97
93
  },
98
94
  "expression": {
99
95
  "type": "string",
100
- "description": "New schedule expression (cron or RRULE). Preferred over cron_expression."
101
- },
102
- "cron_expression": {
103
- "type": "string",
104
- "description": "Deprecated alias for expression with cron syntax. Use expression + syntax instead."
96
+ "description": "New schedule expression (cron or RRULE)"
105
97
  },
106
98
  "timezone": {
107
99
  "type": "string",
@@ -175,8 +175,6 @@ import * as watcherDelete from "./bundled-skills/watcher/tools/watcher-delete.js
175
175
  import * as watcherDigest from "./bundled-skills/watcher/tools/watcher-digest.js";
176
176
  import * as watcherList from "./bundled-skills/watcher/tools/watcher-list.js";
177
177
  import * as watcherUpdate from "./bundled-skills/watcher/tools/watcher-update.js";
178
- // ── weather ──────────────────────────────────────────────────────────────────
179
- import * as getWeather from "./bundled-skills/weather/tools/get-weather.js";
180
178
 
181
179
  // ─── Registry ────────────────────────────────────────────────────────────────
182
180
 
@@ -375,7 +373,4 @@ export const bundledToolRegistry = new Map<string, SkillToolScript>([
375
373
  ["watcher:tools/watcher-update.ts", watcherUpdate],
376
374
  ["watcher:tools/watcher-delete.ts", watcherDelete],
377
375
  ["watcher:tools/watcher-digest.ts", watcherDigest],
378
-
379
- // weather
380
- ["weather:tools/get-weather.ts", getWeather],
381
376
  ]);
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  const VALID_SECRET_ACTIONS = ["redact", "warn", "block", "prompt"] as const;
4
- const VALID_PERMISSIONS_MODES = ["legacy", "strict", "workspace"] as const;
4
+ const VALID_PERMISSIONS_MODES = ["strict", "workspace"] as const;
5
5
  const VALID_SMS_PROVIDERS = ["twilio"] as const;
6
6
 
7
7
  export const TimeoutConfigSchema = z.object({
package/src/config/env.ts CHANGED
@@ -131,10 +131,6 @@ export function getTwilioUserPhoneNumber(): string | undefined {
131
131
  return str("TWILIO_USER_PHONE_NUMBER");
132
132
  }
133
133
 
134
- export function getTwilioWssBaseUrl(): string | undefined {
135
- return str("TWILIO_WSS_BASE_URL");
136
- }
137
-
138
134
  export function isTwilioWebhookValidationDisabled(): boolean {
139
135
  // Intentionally strict: only exact "true" disables validation (not "1").
140
136
  // This is a security-sensitive bypass — we don't want environments that
@@ -212,12 +208,6 @@ export function validateEnv(): void {
212
208
  throw new Error(`Invalid RUNTIME_HTTP_PORT: ${httpPort} (must be 1-65535)`);
213
209
  }
214
210
 
215
- if (getTwilioWssBaseUrl()) {
216
- log.warn(
217
- "TWILIO_WSS_BASE_URL env var is deprecated. Relay URL is now derived from ingress.publicBaseUrl.",
218
- );
219
- }
220
-
221
211
  for (const warning of checkUnrecognizedEnvVars()) {
222
212
  log.warn(warning);
223
213
  }
@@ -31,7 +31,7 @@
31
31
  "key": "feature_flags.browser.enabled",
32
32
  "label": "Browser",
33
33
  "description": "Enable browser skill prerequisites section in the system prompt",
34
- "defaultEnabled": false
34
+ "defaultEnabled": true
35
35
  },
36
36
  {
37
37
  "id": "twitter",
@@ -55,6 +55,21 @@ function ensureMigratedDataDir(): void {
55
55
  ensureDataDir();
56
56
  }
57
57
 
58
+ /**
59
+ * Migrate deprecated raw config values before Zod validation.
60
+ * This prevents `validateWithSchema` from silently dropping fields whose
61
+ * values were valid in older releases but have since been removed from the
62
+ * schema enum, which would cause a fallback to the default and silently
63
+ * change behavior on upgrade.
64
+ */
65
+ function migrateRawConfig(raw: Record<string, unknown>): void {
66
+ const permissions = raw.permissions as Record<string, unknown> | undefined;
67
+ if (permissions?.mode === "legacy") {
68
+ permissions.mode = "workspace";
69
+ log.info('Migrated permissions.mode from "legacy" to "workspace".');
70
+ }
71
+ }
72
+
58
73
  /**
59
74
  * Zod 4's .default({}) returns {} as output without running inner-schema
60
75
  * parsing, so nested object defaults are never applied. Re-parse the config
@@ -227,6 +242,10 @@ export function loadConfig(): AssistantConfig {
227
242
  }
228
243
  }
229
244
 
245
+ // Migrate removed config values before validation so existing configs
246
+ // don't silently change behavior when Zod drops unknown enum values.
247
+ migrateRawConfig(fileConfig);
248
+
230
249
  // Validate and apply defaults via Zod schema
231
250
  const config = validateWithSchema(fileConfig);
232
251
 
@@ -509,11 +509,6 @@ export const MemoryConflictsConfigSchema = z.object({
509
509
  gateMode: z
510
510
  .enum(["soft"], { error: 'memory.conflicts.gateMode must be "soft"' })
511
511
  .default("soft"),
512
- reaskCooldownTurns: z
513
- .number({ error: "memory.conflicts.reaskCooldownTurns must be a number" })
514
- .int("memory.conflicts.reaskCooldownTurns must be an integer")
515
- .positive("memory.conflicts.reaskCooldownTurns must be a positive integer")
516
- .default(3),
517
512
  resolverLlmTimeoutMs: z
518
513
  .number({ error: "memory.conflicts.resolverLlmTimeoutMs must be a number" })
519
514
  .int("memory.conflicts.resolverLlmTimeoutMs must be an integer")
@@ -526,11 +521,6 @@ export const MemoryConflictsConfigSchema = z.object({
526
521
  .min(0, "memory.conflicts.relevanceThreshold must be >= 0")
527
522
  .max(1, "memory.conflicts.relevanceThreshold must be <= 1")
528
523
  .default(0.3),
529
- askOnIrrelevantTurns: z
530
- .boolean({
531
- error: "memory.conflicts.askOnIrrelevantTurns must be a boolean",
532
- })
533
- .default(false),
534
524
  conflictableKinds: z
535
525
  .array(
536
526
  z.enum(VALID_MEMORY_ITEM_KINDS, {
@@ -201,7 +201,7 @@ export const AssistantConfigSchema = z
201
201
  z.string(),
202
202
  z.string({ error: "Each apiKeys value must be a string" }),
203
203
  )
204
- .default({} as any),
204
+ .default({} as Record<string, string>),
205
205
  webSearchProvider: z
206
206
  .enum(VALID_WEB_SEARCH_PROVIDERS, {
207
207
  error: `webSearchProvider must be one of: ${VALID_WEB_SEARCH_PROVIDERS.join(
@@ -273,7 +273,7 @@ export const AssistantConfigSchema = z
273
273
  z.string(),
274
274
  z.boolean({ error: "featureFlags values must be booleans" }),
275
275
  )
276
- .default({} as any),
276
+ .default({} as Record<string, boolean>),
277
277
  assistantFeatureFlagValues: z
278
278
  .record(
279
279
  z.string(),
@@ -944,5 +944,11 @@ function formatSkillsCatalog(skills: SkillSummary[]): string {
944
944
  "When a credential is missing, check if any skill declares `credential-setup-for` matching that service — if so, load that skill.",
945
945
  "",
946
946
  lines.join("\n"),
947
+ "",
948
+ "### Installing additional skills",
949
+ "If `skill_load` fails because a skill is not found, additional first-party skills may be available in the Vellum catalog.",
950
+ "Use `bash` to discover and install them:",
951
+ "- `vellum skills list` — list all available catalog skills",
952
+ "- `vellum skills install <skill-id>` — install a skill, then retry `skill_load`",
947
953
  ].join("\n");
948
954
  }