@vellumai/assistant 0.3.19 → 0.3.20

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 (189) hide show
  1. package/ARCHITECTURE.md +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/docs/architecture/integrations.md +7 -11
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  7. package/src/__tests__/approval-primitive.test.ts +540 -0
  8. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  9. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  11. package/src/__tests__/call-controller.test.ts +439 -108
  12. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  13. package/src/__tests__/cli.test.ts +42 -1
  14. package/src/__tests__/config-schema.test.ts +11 -127
  15. package/src/__tests__/config-watcher.test.ts +0 -8
  16. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  17. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  18. package/src/__tests__/diff.test.ts +22 -0
  19. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  20. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  21. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  22. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  23. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  24. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  25. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  26. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  27. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  28. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  29. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  30. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  31. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  32. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  33. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  34. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  35. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  36. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  37. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  38. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  39. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  40. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  41. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  42. package/src/__tests__/system-prompt.test.ts +1 -1
  43. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  44. package/src/__tests__/terminal-tools.test.ts +2 -93
  45. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  46. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  47. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  48. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  49. package/src/agent/loop.ts +36 -1
  50. package/src/approvals/approval-primitive.ts +381 -0
  51. package/src/approvals/guardian-decision-primitive.ts +191 -0
  52. package/src/calls/call-controller.ts +252 -209
  53. package/src/calls/call-domain.ts +44 -6
  54. package/src/calls/guardian-dispatch.ts +48 -0
  55. package/src/calls/types.ts +1 -1
  56. package/src/calls/voice-session-bridge.ts +46 -30
  57. package/src/cli/core-commands.ts +0 -4
  58. package/src/cli.ts +76 -34
  59. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  60. package/src/config/assistant-feature-flags.ts +162 -0
  61. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  62. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  63. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  64. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  65. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  66. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  67. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  68. package/src/config/core-schema.ts +1 -1
  69. package/src/config/env-registry.ts +10 -0
  70. package/src/config/feature-flag-registry.json +61 -0
  71. package/src/config/loader.ts +22 -1
  72. package/src/config/sandbox-schema.ts +0 -39
  73. package/src/config/schema.ts +6 -2
  74. package/src/config/skill-state.ts +34 -0
  75. package/src/config/skills-schema.ts +0 -1
  76. package/src/config/skills.ts +9 -0
  77. package/src/config/system-prompt.ts +110 -46
  78. package/src/config/templates/SOUL.md +1 -1
  79. package/src/config/types.ts +19 -1
  80. package/src/config/vellum-skills/catalog.json +1 -1
  81. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  82. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  83. package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
  84. package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
  85. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  86. package/src/daemon/config-watcher.ts +0 -1
  87. package/src/daemon/daemon-control.ts +1 -1
  88. package/src/daemon/guardian-invite-intent.ts +124 -0
  89. package/src/daemon/handlers/avatar.ts +68 -0
  90. package/src/daemon/handlers/browser.ts +2 -2
  91. package/src/daemon/handlers/guardian-actions.ts +120 -0
  92. package/src/daemon/handlers/index.ts +4 -0
  93. package/src/daemon/handlers/sessions.ts +19 -0
  94. package/src/daemon/handlers/shared.ts +3 -1
  95. package/src/daemon/install-cli-launchers.ts +58 -13
  96. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  97. package/src/daemon/ipc-contract/sessions.ts +8 -2
  98. package/src/daemon/ipc-contract/settings.ts +25 -2
  99. package/src/daemon/ipc-contract-inventory.json +10 -0
  100. package/src/daemon/ipc-contract.ts +4 -0
  101. package/src/daemon/lifecycle.ts +6 -2
  102. package/src/daemon/main.ts +1 -0
  103. package/src/daemon/server.ts +1 -0
  104. package/src/daemon/session-lifecycle.ts +52 -7
  105. package/src/daemon/session-memory.ts +45 -0
  106. package/src/daemon/session-process.ts +258 -432
  107. package/src/daemon/session-runtime-assembly.ts +12 -0
  108. package/src/daemon/session-skill-tools.ts +14 -1
  109. package/src/daemon/session-tool-setup.ts +5 -0
  110. package/src/daemon/session.ts +11 -0
  111. package/src/daemon/tool-side-effects.ts +35 -9
  112. package/src/index.ts +0 -2
  113. package/src/memory/conversation-display-order-migration.ts +44 -0
  114. package/src/memory/conversation-queries.ts +2 -0
  115. package/src/memory/conversation-store.ts +91 -0
  116. package/src/memory/db-init.ts +5 -1
  117. package/src/memory/embedding-local.ts +13 -8
  118. package/src/memory/guardian-action-store.ts +125 -2
  119. package/src/memory/ingress-invite-store.ts +95 -1
  120. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  121. package/src/memory/migrations/index.ts +2 -1
  122. package/src/memory/schema.ts +5 -1
  123. package/src/memory/scoped-approval-grants.ts +14 -5
  124. package/src/messaging/providers/slack/client.ts +12 -0
  125. package/src/messaging/providers/slack/types.ts +5 -0
  126. package/src/notifications/decision-engine.ts +49 -12
  127. package/src/notifications/emit-signal.ts +7 -0
  128. package/src/notifications/signal.ts +7 -0
  129. package/src/notifications/thread-seed-composer.ts +2 -1
  130. package/src/runtime/channel-approval-types.ts +16 -6
  131. package/src/runtime/channel-approvals.ts +19 -15
  132. package/src/runtime/channel-invite-transport.ts +85 -0
  133. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  134. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  135. package/src/runtime/guardian-action-message-composer.ts +30 -0
  136. package/src/runtime/guardian-decision-types.ts +91 -0
  137. package/src/runtime/http-server.ts +23 -1
  138. package/src/runtime/ingress-service.ts +22 -0
  139. package/src/runtime/invite-redemption-service.ts +181 -0
  140. package/src/runtime/invite-redemption-templates.ts +39 -0
  141. package/src/runtime/routes/call-routes.ts +2 -1
  142. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  143. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  144. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  145. package/src/runtime/routes/pairing-routes.ts +4 -0
  146. package/src/security/encrypted-store.ts +31 -17
  147. package/src/security/keychain.ts +176 -2
  148. package/src/security/secure-keys.ts +97 -0
  149. package/src/security/tool-approval-digest.ts +1 -1
  150. package/src/tools/browser/browser-execution.ts +2 -2
  151. package/src/tools/browser/browser-manager.ts +46 -32
  152. package/src/tools/browser/browser-screencast.ts +2 -2
  153. package/src/tools/calls/call-start.ts +1 -1
  154. package/src/tools/executor.ts +22 -17
  155. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  156. package/src/tools/skills/load.ts +22 -8
  157. package/src/tools/system/avatar-generator.ts +119 -0
  158. package/src/tools/system/navigate-settings.ts +65 -0
  159. package/src/tools/system/open-system-settings.ts +75 -0
  160. package/src/tools/system/voice-config.ts +121 -32
  161. package/src/tools/terminal/backends/native.ts +40 -19
  162. package/src/tools/terminal/backends/types.ts +3 -3
  163. package/src/tools/terminal/parser.ts +1 -1
  164. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  165. package/src/tools/terminal/sandbox.ts +1 -12
  166. package/src/tools/terminal/shell.ts +3 -31
  167. package/src/tools/tool-approval-handler.ts +141 -3
  168. package/src/tools/tool-manifest.ts +6 -0
  169. package/src/tools/types.ts +6 -0
  170. package/src/util/diff.ts +36 -13
  171. package/Dockerfile.sandbox +0 -5
  172. package/src/__tests__/doordash-client.test.ts +0 -187
  173. package/src/__tests__/doordash-session.test.ts +0 -154
  174. package/src/__tests__/signup-e2e.test.ts +0 -354
  175. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  176. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  177. package/src/cli/doordash.ts +0 -1057
  178. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  179. package/src/config/templates/LOOKS.md +0 -25
  180. package/src/doordash/cart-queries.ts +0 -787
  181. package/src/doordash/client.ts +0 -1016
  182. package/src/doordash/order-queries.ts +0 -85
  183. package/src/doordash/queries.ts +0 -13
  184. package/src/doordash/query-extractor.ts +0 -94
  185. package/src/doordash/search-queries.ts +0 -203
  186. package/src/doordash/session.ts +0 -84
  187. package/src/doordash/store-queries.ts +0 -246
  188. package/src/doordash/types.ts +0 -367
  189. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Canonical assistant feature-flag resolver.
3
+ *
4
+ * Loads default flag values from the unified registry at
5
+ * `meta/feature-flags/feature-flag-registry.json` and resolves the effective
6
+ * enabled/disabled state for each declared assistant-scope flag by consulting
7
+ * (in priority order):
8
+ * 1. `config.assistantFeatureFlagValues[key]` (explicit override)
9
+ * 2. defaults registry `defaultEnabled` (for declared keys)
10
+ * 3. `true` (for undeclared keys)
11
+ *
12
+ * Key format:
13
+ * Canonical: `feature_flags.<id>.enabled`
14
+ */
15
+
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { dirname, join } from 'node:path';
18
+
19
+ import type { AssistantConfig } from './schema.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface FeatureFlagDefault {
26
+ defaultEnabled: boolean;
27
+ description: string;
28
+ label: string;
29
+ }
30
+
31
+ export type FeatureFlagDefaultsRegistry = Record<string, FeatureFlagDefault>;
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Registry loading (singleton, loaded once)
35
+ // ---------------------------------------------------------------------------
36
+
37
+ let cachedDefaults: FeatureFlagDefaultsRegistry | undefined;
38
+
39
+ const REGISTRY_FILENAME = 'feature-flag-registry.json';
40
+
41
+ function loadDefaultsRegistry(): FeatureFlagDefaultsRegistry {
42
+ if (cachedDefaults) return cachedDefaults;
43
+
44
+ const thisDir = import.meta.dirname ?? __dirname;
45
+ const envPath = process.env.FEATURE_FLAG_DEFAULTS_PATH?.trim();
46
+ const candidates = [
47
+ // Explicit override (primarily for tests / controlled environments)
48
+ ...(envPath ? [envPath] : []),
49
+ // Bundled: co-located copy in the same directory as this source file.
50
+ // Works in Docker / packaged builds where the repo-root `meta/` dir
51
+ // is not available.
52
+ join(thisDir, REGISTRY_FILENAME),
53
+ // Packaged macOS app layout: the daemon binary lives at
54
+ // <App>.app/Contents/MacOS/vellum-daemon and the registry is copied
55
+ // to <App>.app/Contents/Resources/ by build.sh. In bun --compile
56
+ // binaries, import.meta.dirname resolves to /$bunfs/root (virtual),
57
+ // so we need to resolve relative to the real executable path.
58
+ join(dirname(process.execPath), '..', 'Resources', REGISTRY_FILENAME),
59
+ // Development: relative to this source file's directory, walking up
60
+ // to the repo root to reach `meta/feature-flags/`.
61
+ join(thisDir, '..', '..', '..', 'meta', 'feature-flags', REGISTRY_FILENAME),
62
+ // Alternate: from repo root via cwd
63
+ join(process.cwd(), 'meta', 'feature-flags', REGISTRY_FILENAME),
64
+ ];
65
+
66
+ for (const candidate of candidates) {
67
+ if (existsSync(candidate)) {
68
+ try {
69
+ const raw = readFileSync(candidate, 'utf-8');
70
+ const parsed = JSON.parse(raw);
71
+ cachedDefaults = parseRegistryToDefaults(parsed);
72
+ return cachedDefaults;
73
+ } catch {
74
+ // Malformed file — fall through to next candidate
75
+ }
76
+ }
77
+ }
78
+
79
+ cachedDefaults = {};
80
+ return cachedDefaults;
81
+ }
82
+
83
+ /**
84
+ * Parse the unified registry JSON into a flat key -> default map,
85
+ * filtering to assistant-scope flags only.
86
+ */
87
+ function parseRegistryToDefaults(parsed: unknown): FeatureFlagDefaultsRegistry {
88
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
89
+
90
+ const registry = parsed as { version?: number; flags?: unknown[] };
91
+ if (!Array.isArray(registry.flags)) return {};
92
+
93
+ const result: FeatureFlagDefaultsRegistry = {};
94
+ for (const flag of registry.flags) {
95
+ if (!flag || typeof flag !== 'object' || Array.isArray(flag)) continue;
96
+ const entry = flag as Record<string, unknown>;
97
+ if (entry.scope !== 'assistant') continue;
98
+ if (typeof entry.key !== 'string') continue;
99
+ if (typeof entry.defaultEnabled !== 'boolean') continue;
100
+
101
+ result[entry.key as string] = {
102
+ defaultEnabled: entry.defaultEnabled,
103
+ description: typeof entry.description === 'string' ? entry.description : '',
104
+ label: typeof entry.label === 'string' ? entry.label : '',
105
+ };
106
+ }
107
+ return result;
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Public API
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Resolve whether an assistant feature flag is enabled.
116
+ *
117
+ * Resolution order:
118
+ * 1. `config.assistantFeatureFlagValues[key]` (explicit override)
119
+ * 2. defaults registry `defaultEnabled` (for declared assistant-scope keys)
120
+ * 3. `true` (for undeclared keys with no override)
121
+ */
122
+ export function isAssistantFeatureFlagEnabled(key: string, config: AssistantConfig): boolean {
123
+ const defaults = loadDefaultsRegistry();
124
+ const declared = defaults[key];
125
+
126
+ // 1. Check canonical section
127
+ const newValues = (config as AssistantConfigWithFeatureFlags).assistantFeatureFlagValues;
128
+ if (newValues) {
129
+ const explicit = newValues[key];
130
+ if (typeof explicit === 'boolean') return explicit;
131
+ }
132
+
133
+ // 2. For declared keys, use the registry default
134
+ if (declared) {
135
+ return declared.defaultEnabled;
136
+ }
137
+
138
+ // 3. Undeclared keys with no persisted override default to enabled
139
+ return true;
140
+ }
141
+
142
+ /**
143
+ * Return the loaded defaults registry (for introspection/tooling).
144
+ */
145
+ export function getAssistantFeatureFlagDefaults(): FeatureFlagDefaultsRegistry {
146
+ return loadDefaultsRegistry();
147
+ }
148
+
149
+ /**
150
+ * Reset the cached defaults registry. Intended for tests only.
151
+ */
152
+ export function _resetDefaultsCache(): void {
153
+ cachedDefaults = undefined;
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Internal type augmentation for the new config field
158
+ // ---------------------------------------------------------------------------
159
+
160
+ interface AssistantConfigWithFeatureFlags extends AssistantConfig {
161
+ assistantFeatureFlagValues?: Record<string, boolean>;
162
+ }
@@ -0,0 +1,18 @@
1
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="16" height="16" fill="#0f172a"/>
3
+ <rect x="2" y="2" width="12" height="12" fill="#1e293b"/>
4
+ <rect x="3" y="3" width="10" height="10" fill="#0f172a" stroke="#3b82f6" stroke-width="1"/>
5
+ <rect x="4" y="4" width="2" height="2" fill="#3b82f6"/>
6
+ <rect x="10" y="4" width="2" height="2" fill="#3b82f6"/>
7
+ <rect x="4" y="10" width="2" height="2" fill="#3b82f6"/>
8
+ <rect x="10" y="10" width="2" height="2" fill="#3b82f6"/>
9
+ <line x1="5" y1="5" x2="11" y2="5" stroke="#10b981" stroke-width="1"/>
10
+ <line x1="5" y1="5" x2="5" y2="11" stroke="#10b981" stroke-width="1"/>
11
+ <line x1="11" y1="5" x2="11" y2="11" stroke="#10b981" stroke-width="1"/>
12
+ <line x1="5" y1="11" x2="11" y2="11" stroke="#10b981" stroke-width="1"/>
13
+ <rect x="7" y="7" width="2" height="2" fill="#f59e0b"/>
14
+ <line x1="5" y1="8" x2="7" y2="8" stroke="#60a5fa" stroke-width="1"/>
15
+ <line x1="9" y1="8" x2="11" y2="8" stroke="#60a5fa" stroke-width="1"/>
16
+ <line x1="8" y1="5" x2="8" y2="7" stroke="#60a5fa" stroke-width="1"/>
17
+ <line x1="8" y1="9" x2="8" y2="11" stroke="#60a5fa" stroke-width="1"/>
18
+ </svg>
@@ -256,6 +256,36 @@
256
256
  "executor": "tools/slack-add-reaction.ts",
257
257
  "execution_target": "host"
258
258
  },
259
+ {
260
+ "name": "slack_delete_message",
261
+ "description": "Delete a Slack message posted by the bot. Include a confidence score (0-1).",
262
+ "category": "messaging",
263
+ "risk": "high",
264
+ "input_schema": {
265
+ "type": "object",
266
+ "properties": {
267
+ "channel": {
268
+ "type": "string",
269
+ "description": "Slack channel ID"
270
+ },
271
+ "timestamp": {
272
+ "type": "string",
273
+ "description": "Message timestamp (ts) to delete"
274
+ },
275
+ "confidence": {
276
+ "type": "number",
277
+ "description": "Confidence score (0-1) for this action"
278
+ }
279
+ },
280
+ "required": [
281
+ "channel",
282
+ "timestamp",
283
+ "confidence"
284
+ ]
285
+ },
286
+ "executor": "tools/slack-delete-message.ts",
287
+ "execution_target": "host"
288
+ },
259
289
  {
260
290
  "name": "slack_leave_channel",
261
291
  "description": "Leave a Slack channel. Include a confidence score (0-1).",
@@ -0,0 +1,24 @@
1
+ import { deleteMessage } from '../../../../messaging/providers/slack/client.js';
2
+ import { getMessagingProvider } from '../../../../messaging/registry.js';
3
+ import { withValidToken } from '../../../../security/token-manager.js';
4
+ import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
5
+ import { err, ok } from './shared.js';
6
+
7
+ export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
8
+ const channel = input.channel as string;
9
+ const timestamp = input.timestamp as string;
10
+
11
+ if (!channel || !timestamp) {
12
+ return err('channel and timestamp are both required.');
13
+ }
14
+
15
+ try {
16
+ const provider = getMessagingProvider('slack');
17
+ return withValidToken(provider.credentialService, async (token) => {
18
+ await deleteMessage(token, channel, timestamp);
19
+ return ok(`Message deleted.`);
20
+ });
21
+ } catch (e) {
22
+ return err(e instanceof Error ? e.message : String(e));
23
+ }
24
+ }
@@ -14,7 +14,7 @@ Use `send_notification` for user-facing alerts and notifications. This tool rout
14
14
 
15
15
  ## Deduplication (`dedupe_key`)
16
16
 
17
- - `dedupe_key` suppresses duplicate signals. A second notification with the same key is **dropped entirely** within a **1-hour window**. After the window expires, the same key is accepted again.
17
+ - `dedupe_key` suppresses duplicate signals **permanently**. A second notification with the same key is **dropped entirely** for the lifetime of the assistant's event store. Once a key has been used, it cannot be reused — any future notification with the same key will be silently discarded.
18
18
  - Never reuse a `dedupe_key` across logically distinct notifications, even if they are related. The key means "this exact event already fired," not "these events are in the same category."
19
19
  - If you omit `dedupe_key`, the LLM decision engine may generate one automatically based on signal context. This means even keyless signals can be deduplicated if the engine considers them duplicates of a recent event.
20
20
 
@@ -15,12 +15,34 @@ Create, list, and cancel one-time reminders. Reminders fire at a specific future
15
15
 
16
16
  Control how the reminder is delivered at trigger time with `routing_intent`:
17
17
 
18
- - **single_channel** (default) — deliver to one best channel
18
+ - **single_channel** — deliver to one best channel
19
19
  - **multi_channel** — deliver to a subset of channels
20
- - **all_channels** — deliver to every available channel
20
+ - **all_channels** (default) — deliver to every available channel
21
21
 
22
22
  Optionally pass `routing_hints` (a JSON object) to influence routing decisions (e.g. preferred channels, exclusions). When omitted, defaults to `{}`.
23
23
 
24
+ ### Routing Defaults
25
+
26
+ Use the following heuristics to pick `routing_intent`:
27
+
28
+ - **Default to `all_channels`** for most reminders. Users setting reminders usually want to be notified wherever they are, and redundant notifications are less harmful than missed ones.
29
+ - **Use `single_channel`** only when the user explicitly specifies a single channel (e.g. "remind me on Telegram") or the reminder is low-stakes and noise reduction matters.
30
+ - **Check `user_message_channel`** from the turn context. If the user is currently active on a specific channel (e.g. `vellum`), always include that channel. Pass it as a routing hint:
31
+ ```
32
+ routing_hints: { preferred_channels: ["vellum"] }
33
+ routing_intent: "all_channels"
34
+ ```
35
+ - **Never use `single_channel` as a passive default.** If you haven't thought about which channel to use, use `all_channels`.
36
+
37
+ ### Examples
38
+
39
+ | Scenario | routing_intent | routing_hints |
40
+ |---|---|---|
41
+ | User sets reminder from desktop app | `all_channels` | `{ preferred_channels: ["vellum"] }` |
42
+ | User says "remind me on Telegram" | `single_channel` | `{ preferred_channels: ["telegram"] }` |
43
+ | User sets reminder from Telegram | `all_channels` | `{ preferred_channels: ["telegram"] }` |
44
+ | No channel preference expressed | `all_channels` | `{}` |
45
+
24
46
  ## Usage Notes
25
47
 
26
48
  - Use reminders ONLY for time-triggered notifications (e.g. "remind me at 3pm", "remind me in 2 hours").
@@ -28,3 +50,28 @@ Optionally pass `routing_hints` (a JSON object) to influence routing decisions (
28
50
  - For task tracking ("add to my tasks", "add to my queue"), use task_list_add instead.
29
51
  - `fire_at` must be a strict ISO 8601 timestamp with timezone offset or Z (e.g. `2025-03-15T09:00:00-05:00` or `2025-03-15T09:00:00Z`). Ambiguous timestamps without timezone info will be rejected.
30
52
  - `label` is a short human-readable summary shown in the notification.
53
+
54
+ ### Anchored & Ambiguous Relative Time
55
+
56
+ Phrases like "at the 45 minute mark", "at the top of the hour", "on the half-hour", "at noon", "20 minutes in", or "when I hit an hour" are **clock-position or anchored relative time** expressions. Do NOT treat them as offsets from now.
57
+
58
+ **Resolution rules (in priority order):**
59
+
60
+ 1. **Clock-position expressions** — map directly to a wall-clock time:
61
+ - "top of the hour" / "on the hour" → next :00 (e.g. 10:00 AM)
62
+ - "the X minute mark" / "at :XX" → current hour's :XX; if already past, advance one hour
63
+ - "the half-hour mark" / "half past" → nearest upcoming :30
64
+ - "noon" / "midnight" → 12:00 PM or 12:00 AM today; if past, tomorrow
65
+ - "quarter past" / "quarter to" → :15 or :45 of current or next hour
66
+
67
+ 2. **Session-anchored expressions** — if the user mentioned a start time earlier in conversation ("I got here at 9", "meeting started at 2pm"), compute `start_time + offset`.
68
+
69
+ 3. **Ask only if truly ambiguous** — if neither rule 1 nor rule 2 resolves, ask: "Do you mean [clock time] or [X minutes from now]?" Never silently default to "from now."
70
+
71
+ **Examples:**
72
+ - "at the 45 min mark" (now: 9:39) → 9:45 AM
73
+ - "at the 45 min mark" (now: 9:50) → 10:45 AM
74
+ - "top of the hour" (now: 9:39) → 10:00 AM
75
+ - "at noon" → 12:00 PM today
76
+ - "20 minutes in, I started at 2pm" → 2:20 PM
77
+ - "at the hour mark" with no start time → ask for clarification
@@ -55,6 +55,31 @@ When the user says "in X minutes/hours", compute the ISO 8601 timestamp yourself
55
55
  - Format as ISO 8601 with timezone: `2025-03-15T09:05:00-05:00`
56
56
  - Pass to `reminder_create` as `fire_at`
57
57
 
58
+ ### Anchored & Ambiguous Relative Time
59
+
60
+ Phrases like "at the 45 minute mark", "at the top of the hour", "on the half-hour", "at noon", "20 minutes in", or "when I hit an hour" are **clock-position or anchored relative time** expressions. Do NOT treat them as offsets from now.
61
+
62
+ **Resolution rules (in priority order):**
63
+
64
+ 1. **Clock-position expressions** — map directly to a wall-clock time:
65
+ - "top of the hour" / "on the hour" → next :00 (e.g. 10:00 AM)
66
+ - "the X minute mark" / "at :XX" → current hour's :XX; if already past, advance one hour
67
+ - "the half-hour mark" / "half past" → nearest upcoming :30
68
+ - "noon" / "midnight" → 12:00 PM or 12:00 AM today; if past, tomorrow
69
+ - "quarter past" / "quarter to" → :15 or :45 of current or next hour
70
+
71
+ 2. **Session-anchored expressions** — if the user mentioned a start time earlier in conversation ("I got here at 9", "meeting started at 2pm"), compute `start_time + offset`.
72
+
73
+ 3. **Ask only if truly ambiguous** — if neither rule 1 nor rule 2 resolves, ask: "Do you mean [clock time] or [X minutes from now]?" Never silently default to "from now."
74
+
75
+ **Examples:**
76
+ - "at the 45 min mark" (now: 9:39) → 9:45 AM
77
+ - "at the 45 min mark" (now: 9:50) → 10:45 AM
78
+ - "top of the hour" (now: 9:39) → 10:00 AM
79
+ - "at noon" → 12:00 PM today
80
+ - "20 minutes in, I started at 2pm" → 2:20 PM
81
+ - "at the hour mark" with no start time → ask for clarification
82
+
58
83
  ## "Remind me to X" Disambiguation
59
84
 
60
85
  The word "remind" is ambiguous. Route based on whether a time is specified:
@@ -77,12 +102,34 @@ Use `notify` for simple alerts. Use `execute` when the reminder should trigger t
77
102
  ## Reminder Routing
78
103
 
79
104
  `reminder_create` supports a `routing_intent` parameter that controls how the reminder is delivered at trigger time:
80
- - **`single_channel`** (default) — deliver to one best channel
105
+ - **`single_channel`** — deliver to one best channel
81
106
  - **`multi_channel`** — deliver to a subset of channels
82
- - **`all_channels`** — deliver to every available channel
107
+ - **`all_channels`** (default) — deliver to every available channel
83
108
 
84
109
  You can also pass `routing_hints` (a JSON object) to influence routing decisions (e.g. preferred channels, exclusions).
85
110
 
111
+ ### Routing Defaults
112
+
113
+ Use the following heuristics to pick `routing_intent`:
114
+
115
+ - **Default to `all_channels`** for most reminders. Users setting reminders usually want to be notified wherever they are, and redundant notifications are less harmful than missed ones.
116
+ - **Use `single_channel`** only when the user explicitly specifies a single channel (e.g. "remind me on Telegram") or the reminder is low-stakes and noise reduction matters.
117
+ - **Check `user_message_channel`** from the turn context. If the user is currently active on a specific channel (e.g. `vellum`), always include that channel. Pass it as a routing hint:
118
+ ```
119
+ routing_hints: { preferred_channels: ["vellum"] }
120
+ routing_intent: "all_channels"
121
+ ```
122
+ - **Never use `single_channel` as a passive default.** If you haven't thought about which channel to use, use `all_channels`.
123
+
124
+ ### Examples
125
+
126
+ | Scenario | routing_intent | routing_hints |
127
+ |---|---|---|
128
+ | User sets reminder from desktop app | `all_channels` | `{ preferred_channels: ["vellum"] }` |
129
+ | User says "remind me on Telegram" | `single_channel` | `{ preferred_channels: ["telegram"] }` |
130
+ | User sets reminder from Telegram | `all_channels` | `{ preferred_channels: ["telegram"] }` |
131
+ | No channel preference expressed | `all_channels` | `{}` |
132
+
86
133
  ## Tool Summary
87
134
 
88
135
  | Tool | Timing | Recurrence | Purpose |
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: "Voice Setup"
3
+ description: "Complete voice configuration in chat — PTT key, wake word, microphone permissions, ElevenLabs TTS, and troubleshooting"
4
+ user-invocable: true
5
+ metadata: {"vellum": {"emoji": "🎙️", "os": ["darwin"]}}
6
+ ---
7
+
8
+ You are helping the user set up and troubleshoot voice features (push-to-talk, wake word, text-to-speech) entirely within this conversation. Do NOT direct the user to the Settings page for initial setup — handle everything in-chat using the tools below.
9
+
10
+ ## Available Tools
11
+
12
+ - `voice_config_update` — Change any voice setting (PTT key, wake word enabled/keyword/timeout)
13
+ - `open_system_settings` — Open macOS System Settings to a specific privacy pane
14
+ - `navigate_settings_tab` — Open the Vellum settings panel to the Voice tab
15
+ - `credential_store` — Collect API keys securely (for ElevenLabs TTS)
16
+
17
+ ## Setup Flow
18
+
19
+ Walk the user through each section in order. Skip sections they don't need. Ask before proceeding to the next section.
20
+
21
+ ### 1. Microphone Permission
22
+
23
+ Check `<channel_capabilities>` for `microphone_permission_granted`.
24
+
25
+ **If `false` or missing:**
26
+ 1. Explain that macOS requires microphone permission for voice features.
27
+ 2. Use `open_system_settings` with `pane: "microphone"` to open the right System Settings pane.
28
+ 3. Tell the user: "I've opened System Settings to the Microphone section. Please toggle **Vellum Assistant** on, then come back here."
29
+ 4. After they confirm, verify by checking capabilities on the next turn.
30
+
31
+ **If `true`:** Tell them microphone is already granted and move on.
32
+
33
+ ### 2. Push-to-Talk Activation Key
34
+
35
+ Present common PTT key options:
36
+ - **Right Option (⌥)** — Default, good general choice
37
+ - **Fn** — Dedicated key on most Mac keyboards
38
+ - **Right Command (⌘)** — Easy to reach
39
+ - **Right Control (⌃)** — Familiar from gaming
40
+
41
+ Ask which key they prefer, then use `voice_config_update` with `setting: "activation_key"` and the chosen value.
42
+
43
+ **Common issues to mention:**
44
+ - If they pick a key that conflicts with their emoji picker (Fn or Globe on newer Macs), warn them and suggest an alternative.
45
+ - If they use a terminal app heavily, warn that some keys may be captured by the terminal.
46
+
47
+ ### 3. Wake Word (Optional)
48
+
49
+ Ask if they want to enable wake word detection (hands-free activation by saying a keyword).
50
+
51
+ **If yes:**
52
+ 1. Use `voice_config_update` with `setting: "wake_word_enabled"`, `value: true`.
53
+ 2. Ask what wake word they want. Common choices: "Hey Vellum", "Computer", "Jarvis", their assistant's name.
54
+ 3. Use `voice_config_update` with `setting: "wake_word_keyword"` and their chosen word.
55
+ 4. Ask about timeout (how long the mic stays active after wake word). Options: 5s, 10s (default), 15s, 30s, 60s.
56
+ 5. Use `voice_config_update` with `setting: "wake_word_timeout"` and their chosen value.
57
+
58
+ **Speech Recognition permission:** Wake word requires Speech Recognition access. Check capabilities — if not granted, use `open_system_settings` with `pane: "speech_recognition"`.
59
+
60
+ ### 4. Text-to-Speech / ElevenLabs (Optional)
61
+
62
+ Ask if they want high-quality text-to-speech voices via ElevenLabs (optional — standard TTS works without it).
63
+
64
+ **If yes:**
65
+ 1. Tell them they need an ElevenLabs API key. They can get one at https://elevenlabs.io (free tier available).
66
+ 2. Use `credential_store` with `action: "prompt"`, `service: "elevenlabs"`, `field: "api_key"` to show a secure input dialog.
67
+ 3. After the key is stored, confirm success.
68
+
69
+ ### 5. Verification
70
+
71
+ After setup is complete:
72
+ 1. Summarize what was configured.
73
+ 2. Suggest they test by pressing their PTT key (or saying their wake word) and speaking.
74
+ 3. Offer to open the Voice settings tab if they want to review: use `navigate_settings_tab` with `tab: "Voice"`.
75
+
76
+ ## Troubleshooting Decision Trees
77
+
78
+ When the user reports a problem, follow the appropriate decision tree:
79
+
80
+ ### "PTT isn't working" / "Can't record"
81
+ 1. **Microphone permission** — Check `microphone_permission_granted` in capabilities. If false, guide through granting it.
82
+ 2. **Key check** — Ask what key they're using. Confirm it matches their configured PTT key.
83
+ 3. **Emoji picker conflict** — On newer Macs, Fn/Globe opens the emoji picker. If they're using Fn, suggest switching to Right Option or Right Command.
84
+ 4. **Speech Recognition permission** — Some voice features need this. Use `open_system_settings` with `pane: "speech_recognition"`.
85
+ 5. **App focus** — PTT may not work when Vellum is not the frontmost app or if another app has captured the key.
86
+
87
+ ### "Recording but no text" / "Transcription not working"
88
+ 1. **Speech Recognition permission** — Must be granted for transcription.
89
+ 2. **Microphone input** — Ask if they see the recording indicator. If yes, the mic works but transcription is failing.
90
+ 3. **Locale/language** — Speech recognition works best with the system language. Ask if they're speaking in a different language.
91
+ 4. **Background noise** — Excessive noise can prevent transcription. Suggest a quieter environment or a closer microphone.
92
+
93
+ ### "Wake word not detecting"
94
+ 1. **Enabled check** — Confirm wake word is enabled in their settings.
95
+ 2. **Keyword** — Confirm what keyword they're using. Shorter or common words may have lower accuracy.
96
+ 3. **Ambient noise** — Wake word detection is sensitive to background noise.
97
+ 4. **Permissions** — Both Microphone and Speech Recognition permissions are required.
98
+ 5. **Timeout** — If wake word activates but cuts off too quickly, increase the timeout.
99
+
100
+ ### "Changed a setting but it didn't work"
101
+ 1. **IPC broadcast** — The setting should take effect immediately. If it didn't, suggest restarting the assistant.
102
+ 2. **Verify** — Open the Voice settings tab with `navigate_settings_tab` to confirm the setting was persisted.
103
+
104
+ ## Deep Debugging
105
+
106
+ For persistent issues, suggest checking system logs:
107
+
108
+ ```bash
109
+ log stream --predicate 'subsystem == "com.vellum.assistant"' --level debug
110
+ ```
111
+
112
+ Key log categories:
113
+ - `voice` — PTT activation, recording state
114
+ - `wake-word` — Wake word detection events
115
+ - `speech` — Speech recognition results
116
+
117
+ ## Rules
118
+
119
+ - Always handle setup conversationally in-chat. Do NOT tell the user to go to Settings for initial configuration.
120
+ - Use `navigate_settings_tab` only for review/verification after in-chat setup, not as the primary setup method.
121
+ - Be concise. Don't explain every option exhaustively — present the most common choices and let the user ask for more.
122
+ - If a permission is denied, acknowledge it gracefully and explain what features won't work without it.
@@ -124,7 +124,7 @@ export const ContextWindowConfigSchema = z.object({
124
124
  .number({ error: 'contextWindow.maxInputTokens must be a number' })
125
125
  .int('contextWindow.maxInputTokens must be an integer')
126
126
  .positive('contextWindow.maxInputTokens must be a positive integer')
127
- .default(180000),
127
+ .default(200000),
128
128
  targetInputTokens: z
129
129
  .number({ error: 'contextWindow.targetInputTokens must be a number' })
130
130
  .int('contextWindow.targetInputTokens must be an integer')
@@ -124,6 +124,16 @@ export function getEnableMonitoring(): boolean {
124
124
  return flag('VELLUM_ENABLE_MONITORING');
125
125
  }
126
126
 
127
+ /**
128
+ * IS_CONTAINERIZED — boolean, default: false
129
+ * When true, indicates the assistant is running inside a container (e.g. Docker).
130
+ * Any new data that needs to survive restarts must be written to BASE_DATA_DIR,
131
+ * which is mapped to a persistent volume.
132
+ */
133
+ export function getIsContainerized(): boolean {
134
+ return flag('IS_CONTAINERIZED');
135
+ }
136
+
127
137
  // ── Known env var names ──────────────────────────────────────────────────────
128
138
 
129
139
  /**
@@ -0,0 +1,61 @@
1
+ {
2
+ "version": 1,
3
+ "flags": [
4
+ {
5
+ "id": "sms",
6
+ "scope": "assistant",
7
+ "key": "feature_flags.sms.enabled",
8
+ "label": "SMS",
9
+ "description": "Show SMS setup in Settings > Connect and enable the SMS setup skill",
10
+ "defaultEnabled": true
11
+ },
12
+ {
13
+ "id": "hatch-new-assistant",
14
+ "scope": "assistant",
15
+ "key": "feature_flags.hatch-new-assistant.enabled",
16
+ "label": "Hatch New Assistant",
17
+ "description": "Show Hatch New Assistant action in Settings > Account",
18
+ "defaultEnabled": true
19
+ },
20
+ {
21
+ "id": "guardian-verify-setup",
22
+ "scope": "assistant",
23
+ "key": "feature_flags.guardian-verify-setup.enabled",
24
+ "label": "Guardian Verification Setup",
25
+ "description": "Enable guardian verification routing in the system prompt",
26
+ "defaultEnabled": true
27
+ },
28
+ {
29
+ "id": "browser",
30
+ "scope": "assistant",
31
+ "key": "feature_flags.browser.enabled",
32
+ "label": "Browser",
33
+ "description": "Enable browser skill prerequisites section in the system prompt",
34
+ "defaultEnabled": true
35
+ },
36
+ {
37
+ "id": "twitter",
38
+ "scope": "assistant",
39
+ "key": "feature_flags.twitter.enabled",
40
+ "label": "Twitter",
41
+ "description": "Enable X (Twitter) skill section in the system prompt",
42
+ "defaultEnabled": true
43
+ },
44
+ {
45
+ "id": "user-hosted-enabled",
46
+ "scope": "macos",
47
+ "key": "user_hosted_enabled",
48
+ "label": "User Hosted Enabled",
49
+ "description": "Enable user-hosted onboarding flow",
50
+ "defaultEnabled": false
51
+ },
52
+ {
53
+ "id": "local-http-enabled",
54
+ "scope": "macos",
55
+ "key": "local_http_enabled",
56
+ "label": "Local HTTP Enabled",
57
+ "description": "Enable local HTTP transport mode in macOS app",
58
+ "defaultEnabled": false
59
+ }
60
+ ]
61
+ }
@@ -1,4 +1,5 @@
1
- import { existsSync, readFileSync, statSync,writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
2
3
 
3
4
  import { deleteSecureKey,getSecureKey, setSecureKey } from '../security/secure-keys.js';
4
5
  import { ConfigError } from '../util/errors.js';
@@ -113,6 +114,7 @@ export function loadConfig(): AssistantConfig {
113
114
  const configPath = getConfigPath();
114
115
 
115
116
  let fileConfig: Record<string, unknown> = {};
117
+ let configFileExisted = true;
116
118
  if (existsSync(configPath)) {
117
119
  const mode = statSync(configPath).mode;
118
120
  if (mode & 0o077) {
@@ -127,6 +129,8 @@ export function loadConfig(): AssistantConfig {
127
129
  } catch (err) {
128
130
  throw new ConfigError(`Failed to parse config at ${configPath}: ${err}`);
129
131
  }
132
+ } else {
133
+ configFileExisted = false;
130
134
  }
131
135
 
132
136
  // Pre-validate apiKeys shape before migration (must be a plain object)
@@ -182,6 +186,23 @@ export function loadConfig(): AssistantConfig {
182
186
  // Validate and apply defaults via Zod schema
183
187
  const config = validateWithSchema(fileConfig);
184
188
 
189
+ // If the config file didn't exist, write the full defaults to disk so
190
+ // users can discover and edit all available options.
191
+ if (!configFileExisted) {
192
+ try {
193
+ const dir = dirname(configPath);
194
+ if (!existsSync(dir)) {
195
+ mkdirSync(dir, { recursive: true });
196
+ }
197
+ // Strip apiKeys (managed in secure storage) and dataDir (runtime-derived)
198
+ const { apiKeys: _, dataDir: _d, ...persistable } = config;
199
+ writeFileSync(configPath, JSON.stringify(persistable, null, 2) + '\n');
200
+ log.info('Wrote default config to %s', configPath);
201
+ } catch (err) {
202
+ log.warn({ err }, 'Failed to write default config file');
203
+ }
204
+ }
205
+
185
206
  // Set cached before secure-key/env overrides so re-entrant calls
186
207
  // return the in-flight config instead of bare defaults.
187
208
  cached = config;