@vellumai/assistant 0.3.12 → 0.3.14
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.
- package/ARCHITECTURE.md +17 -3
- package/README.md +2 -0
- package/docs/architecture/scheduling.md +81 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +22 -0
- package/src/__tests__/channel-policy.test.ts +19 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +584 -0
- package/src/__tests__/intent-routing.test.ts +22 -0
- package/src/__tests__/ipc-snapshot.test.ts +10 -0
- package/src/__tests__/notification-routing-intent.test.ts +186 -0
- package/src/__tests__/recording-handler.test.ts +191 -31
- package/src/__tests__/recording-intent-fallback.test.ts +181 -0
- package/src/__tests__/recording-intent-handler.test.ts +593 -73
- package/src/__tests__/recording-intent.test.ts +739 -343
- package/src/__tests__/recording-state-machine.test.ts +1109 -0
- package/src/__tests__/reminder-store.test.ts +20 -18
- package/src/__tests__/reminder.test.ts +2 -1
- package/src/channels/config.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -11
- package/src/config/bundled-skills/screen-recording/SKILL.md +91 -12
- package/src/config/system-prompt.ts +5 -0
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/daemon/computer-use-session.ts +12 -0
- package/src/daemon/handlers/misc.ts +258 -102
- package/src/daemon/handlers/recording.ts +417 -5
- package/src/daemon/handlers/sessions.ts +136 -62
- package/src/daemon/ipc-contract/computer-use.ts +25 -3
- package/src/daemon/ipc-contract/messages.ts +3 -1
- package/src/daemon/ipc-contract/shared.ts +6 -0
- package/src/daemon/ipc-contract-inventory.json +2 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/recording-executor.ts +180 -0
- package/src/daemon/recording-intent-fallback.ts +132 -0
- package/src/daemon/recording-intent.ts +306 -15
- package/src/daemon/session-tool-setup.ts +4 -0
- package/src/notifications/README.md +69 -1
- package/src/notifications/adapters/sms.ts +80 -0
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +3 -3
- package/src/notifications/decision-engine.ts +70 -1
- package/src/notifications/decisions-store.ts +24 -0
- package/src/notifications/destination-resolver.ts +2 -1
- package/src/notifications/emit-signal.ts +35 -3
- package/src/notifications/signal.ts +6 -0
- package/src/notifications/types.ts +3 -0
- package/src/schedule/scheduler.ts +15 -3
- package/src/tools/executor.ts +29 -0
- package/src/tools/guardian-control-plane-policy.ts +141 -0
- package/src/tools/types.ts +2 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -57,6 +57,14 @@ The HTTP route handlers (`integration-routes.ts`) and the legacy IPC handlers (`
|
|
|
57
57
|
| `src/daemon/handlers/config-channels.ts` | IPC handler that delegates to the same shared actions |
|
|
58
58
|
| `src/config/vellum-skills/guardian-verify-setup/SKILL.md` | Skill that teaches the assistant how to orchestrate guardian verification via chat |
|
|
59
59
|
|
|
60
|
+
**Guardian-Only Tool Invocation Gate:**
|
|
61
|
+
|
|
62
|
+
Guardian verification control-plane endpoints (`/v1/integrations/guardian/*`) are protected by a deterministic gate in the tool executor (`src/tools/executor.ts`). Before any tool invocation proceeds, the executor checks whether the invocation targets a guardian control-plane endpoint and whether the actor role is allowed. The policy uses an allowlist: only `guardian` and `undefined` (desktop/trusted) actor roles can invoke these endpoints. Non-guardian and unverified-channel actors receive a denial message explaining the restriction.
|
|
63
|
+
|
|
64
|
+
The policy is implemented in `src/tools/guardian-control-plane-policy.ts`, which inspects tool inputs (bash commands, URLs) for guardian endpoint paths. This is a defense-in-depth measure — even if the LLM attempts to call guardian endpoints on behalf of a non-guardian actor, the tool executor blocks it deterministically.
|
|
65
|
+
|
|
66
|
+
The `guardian-verify-setup` skill is the exclusive handler for guardian verification intents in the system prompt. Other skills (e.g., `phone-calls`) hand off to `guardian-verify-setup` rather than orchestrating verification directly.
|
|
67
|
+
|
|
60
68
|
### SMS Channel (Twilio)
|
|
61
69
|
|
|
62
70
|
The SMS channel provides text-only messaging via Twilio, sharing the same telephony provider as voice calls. It follows the same ingress/egress pattern as Telegram but uses Twilio's HMAC-SHA1 signature validation instead of a secret header.
|
|
@@ -170,7 +178,7 @@ graph LR
|
|
|
170
178
|
EMB["memory_embeddings<br/>───────────────<br/>target: segment | item | summary<br/>provider + model metadata<br/>vector_json (float array)<br/>Powers semantic search"]
|
|
171
179
|
JOBS["memory_jobs<br/>───────────────<br/>Async task queue<br/>Types: embed, extract,<br/>summarize, backfill,<br/>conflict resolution, cleanup<br/>Status: pending → running →<br/>completed | failed"]
|
|
172
180
|
ATT["attachments<br/>───────────────<br/>base64-encoded file data<br/>mime_type, size_bytes<br/>Linked to messages via<br/>message_attachments join"]
|
|
173
|
-
REM["reminders<br/>───────────────<br/>One-time scheduled reminders<br/>label, message, fireAt<br/>mode: notify | execute<br/>status: pending → fired | cancelled"]
|
|
181
|
+
REM["reminders<br/>───────────────<br/>One-time scheduled reminders<br/>label, message, fireAt<br/>mode: notify | execute<br/>status: pending → fired | cancelled<br/>routing_intent: single_channel |<br/>multi_channel | all_channels<br/>routing_hints_json (free-form)"]
|
|
174
182
|
SCHED_JOBS["cron_jobs (recurrence schedules)<br/>───────────────<br/>Recurring schedule definitions<br/>cron_expression: cron or RRULE string<br/>schedule_syntax: 'cron' | 'rrule'<br/>timezone, message, next_run_at<br/>enabled, retry_count<br/>Legacy alias: scheduleJobs"]
|
|
175
183
|
SCHED_RUNS["cron_runs (schedule runs)<br/>───────────────<br/>Execution history per schedule<br/>job_id (FK → cron_jobs)<br/>status: ok | error<br/>duration_ms, output, error<br/>Legacy alias: scheduleRuns"]
|
|
176
184
|
TASKS["tasks<br/>───────────────<br/>Reusable prompt templates<br/>title, Handlebars template<br/>inputSchema, contextFlags<br/>requiredTools, status"]
|
|
@@ -1440,14 +1448,19 @@ Two IPC push events surface new threads in the macOS/iOS client sidebar:
|
|
|
1440
1448
|
|
|
1441
1449
|
All events follow the same pattern: the daemon creates a server-side conversation, persists an initial message, and broadcasts the IPC event so the macOS `ThreadManager` can create a visible thread in the sidebar.
|
|
1442
1450
|
|
|
1451
|
+
### Reminder Routing Metadata
|
|
1452
|
+
|
|
1453
|
+
Reminders carry optional `routingIntent` (`single_channel` | `multi_channel` | `all_channels`) and free-form `routingHints` metadata. When a reminder fires, this metadata flows through the notification signal into a post-decision enforcement step (`enforceRoutingIntent()` in `decision-engine.ts`) that overrides the LLM's channel selection to match the requested coverage. This enables single-reminder fanout: one reminder can produce multi-channel delivery without duplicate reminders. See `assistant/docs/architecture/scheduling.md` for the full trigger-time data flow.
|
|
1454
|
+
|
|
1443
1455
|
### Channel Delivery
|
|
1444
1456
|
|
|
1445
|
-
Notifications are delivered to
|
|
1457
|
+
Notifications are delivered to three channel types:
|
|
1446
1458
|
|
|
1447
1459
|
- **Vellum (always connected)**: Local IPC via the daemon's broadcast mechanism. The `VellumAdapter` emits a `notification_intent` message with rendered copy and optional `deepLinkMetadata`.
|
|
1448
1460
|
- **Telegram (when guardian binding exists)**: HTTP POST to the gateway's `/deliver/telegram` endpoint. Requires an active guardian binding for the assistant.
|
|
1461
|
+
- **SMS (when guardian binding exists)**: HTTP POST to the gateway's `/deliver/sms` endpoint. Follows the same pattern as Telegram; the `SmsAdapter` sends text-only messages via the Twilio Messages API. The `assistantId` is threaded through the delivery payload for multi-assistant phone number resolution.
|
|
1449
1462
|
|
|
1450
|
-
Connected channels are resolved at signal emission time: vellum is always included, and binding-based channels (Telegram) are included only when an active guardian binding exists for the assistant.
|
|
1463
|
+
Connected channels are resolved at signal emission time: vellum is always included, and binding-based channels (Telegram, SMS) are included only when an active guardian binding exists for the assistant.
|
|
1451
1464
|
|
|
1452
1465
|
**Key modules:**
|
|
1453
1466
|
|
|
@@ -1461,6 +1474,7 @@ Connected channels are resolved at signal emission time: vellum is always includ
|
|
|
1461
1474
|
| `assistant/src/notifications/conversation-pairing.ts` | Materializes conversation + message per delivery based on channel strategy |
|
|
1462
1475
|
| `assistant/src/notifications/adapters/macos.ts` | Vellum adapter — broadcasts `notification_intent` via IPC with deep-link metadata |
|
|
1463
1476
|
| `assistant/src/notifications/adapters/telegram.ts` | Telegram adapter — POSTs to gateway `/deliver/telegram` |
|
|
1477
|
+
| `assistant/src/notifications/adapters/sms.ts` | SMS adapter — POSTs to gateway `/deliver/sms` via Twilio Messages API |
|
|
1464
1478
|
| `assistant/src/notifications/destination-resolver.ts` | Resolves per-channel endpoints (vellum IPC, Telegram chat ID from guardian binding) |
|
|
1465
1479
|
| `assistant/src/notifications/copy-composer.ts` | Template-based fallback copy when LLM copy is unavailable |
|
|
1466
1480
|
| `assistant/src/notifications/preference-extractor.ts` | Detects preference statements in conversation messages |
|
package/README.md
CHANGED
|
@@ -343,6 +343,8 @@ Guardian verification can also be initiated through normal desktop chat. When th
|
|
|
343
343
|
|
|
344
344
|
These endpoints share the same business logic as the IPC-based verification flow via `guardian-outbound-actions.ts`.
|
|
345
345
|
|
|
346
|
+
**Security constraint:** Guardian verification control-plane endpoints are restricted to guardian and desktop (trusted) actors only. Non-guardian and unverified-channel actors cannot invoke these endpoints conversationally via tools. Attempts are denied with a message explaining that guardian verification actions are restricted to guardian users.
|
|
347
|
+
|
|
346
348
|
## Channel Readiness
|
|
347
349
|
|
|
348
350
|
The `channel_readiness` IPC contract provides a unified way to check whether a channel (SMS, Telegram, etc.) is fully configured and operational. It runs local checks (credential presence, phone number assignment, ingress config) synchronously and optional remote checks (API reachability) asynchronously with a 5-minute TTL cache.
|
|
@@ -43,6 +43,87 @@ The database column is named `cron_expression` and the Drizzle table is `cronJob
|
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
46
|
+
## Reminder Routing — Trigger-Time Multi-Channel Delivery
|
|
47
|
+
|
|
48
|
+
Reminders support optional routing metadata that controls how the notification pipeline fans out delivery across channels when a reminder fires. This allows a single reminder to reach the user on multiple channels (desktop, Telegram, SMS) without requiring duplicate reminders.
|
|
49
|
+
|
|
50
|
+
### Routing Metadata Model
|
|
51
|
+
|
|
52
|
+
Two columns on the `reminders` table carry routing metadata:
|
|
53
|
+
|
|
54
|
+
| Column | Type | Default | Description |
|
|
55
|
+
|--------|------|---------|-------------|
|
|
56
|
+
| `routing_intent` | TEXT | `'single_channel'` | Controls channel coverage: `single_channel`, `multi_channel`, or `all_channels` |
|
|
57
|
+
| `routing_hints_json` | TEXT (JSON) | `'{}'` | Free-form hints for the decision engine (e.g. preferred channels) |
|
|
58
|
+
|
|
59
|
+
### Trigger-Time Data Flow
|
|
60
|
+
|
|
61
|
+
When the scheduler fires a reminder, routing metadata flows through the full notification pipeline:
|
|
62
|
+
|
|
63
|
+
```mermaid
|
|
64
|
+
sequenceDiagram
|
|
65
|
+
participant Scheduler as Scheduler<br/>(15s tick)
|
|
66
|
+
participant Store as ReminderStore<br/>(SQLite)
|
|
67
|
+
participant Lifecycle as Daemon Lifecycle<br/>(notifyReminder)
|
|
68
|
+
participant Signal as emitNotificationSignal
|
|
69
|
+
participant Engine as Decision Engine<br/>(LLM)
|
|
70
|
+
participant Enforce as enforceRoutingIntent
|
|
71
|
+
participant Broadcaster as Broadcaster
|
|
72
|
+
participant Adapters as Channel Adapters<br/>(Vellum, Telegram, SMS)
|
|
73
|
+
|
|
74
|
+
Scheduler->>Store: claimDueReminders(now)
|
|
75
|
+
Store-->>Scheduler: ReminderRow[] (with routingIntent, routingHints)
|
|
76
|
+
Scheduler->>Lifecycle: notifyReminder({ id, label, message, routingIntent, routingHints })
|
|
77
|
+
Lifecycle->>Signal: emitNotificationSignal({ routingIntent, routingHints, ... })
|
|
78
|
+
Signal->>Engine: evaluateSignal(signal, connectedChannels)
|
|
79
|
+
Engine-->>Signal: NotificationDecision (LLM channel selection)
|
|
80
|
+
Signal->>Enforce: enforceRoutingIntent(decision, routingIntent, connectedChannels)
|
|
81
|
+
Note over Enforce: Override channel selection<br/>based on routing intent
|
|
82
|
+
Enforce-->>Signal: Enforced decision (re-persisted if changed)
|
|
83
|
+
Signal->>Broadcaster: dispatchDecision(signal, decision)
|
|
84
|
+
Broadcaster->>Adapters: Fan-out to each selected channel
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Enforcement Behavior
|
|
88
|
+
|
|
89
|
+
The `enforceRoutingIntent()` step runs after the LLM produces a channel selection but before deterministic checks. It acts as a post-decision guard:
|
|
90
|
+
|
|
91
|
+
| Intent | Enforcement Rule |
|
|
92
|
+
|--------|-----------------|
|
|
93
|
+
| `single_channel` | No override. The LLM's channel selection stands. |
|
|
94
|
+
| `multi_channel` | If the LLM selected < 2 channels and 2+ are connected, expand to all connected channels. |
|
|
95
|
+
| `all_channels` | Replace the LLM's selection with all connected channels. |
|
|
96
|
+
|
|
97
|
+
When enforcement changes the decision, the updated `selectedChannels` and annotated `reasoningSummary` are re-persisted to `notification_decisions` so the audit trail reflects what was actually dispatched.
|
|
98
|
+
|
|
99
|
+
### Single-Reminder Fanout
|
|
100
|
+
|
|
101
|
+
One reminder creates one notification signal. The routing intent on that single signal controls how many channels receive the notification. The notification pipeline handles per-channel copy rendering, conversation pairing, and delivery through existing adapters. No duplicate reminders are needed for multi-channel delivery.
|
|
102
|
+
|
|
103
|
+
### Connected Channels at Fire Time
|
|
104
|
+
|
|
105
|
+
Channel availability is resolved when the signal is emitted (not when the reminder is created):
|
|
106
|
+
|
|
107
|
+
- **Vellum** — always connected (local IPC)
|
|
108
|
+
- **Telegram** — connected when an active guardian binding exists
|
|
109
|
+
- **SMS** — connected when an active guardian binding exists
|
|
110
|
+
|
|
111
|
+
If a channel becomes unavailable between reminder creation and fire time, it is silently excluded from delivery. The routing intent enforcement operates only on channels that are connected at fire time.
|
|
112
|
+
|
|
113
|
+
### Key Source Files
|
|
114
|
+
|
|
115
|
+
| File | Responsibility |
|
|
116
|
+
|------|---------------|
|
|
117
|
+
| `assistant/src/tools/reminder/reminder-store.ts` | CRUD with `routingIntent` and `routingHints` fields |
|
|
118
|
+
| `assistant/src/memory/schema.ts` | `reminders` table schema with `routing_intent` and `routing_hints_json` columns |
|
|
119
|
+
| `assistant/src/schedule/scheduler.ts` | Claims due reminders and passes routing metadata to the notifier |
|
|
120
|
+
| `assistant/src/daemon/lifecycle.ts` | Wires the reminder notifier to `emitNotificationSignal()` with routing metadata |
|
|
121
|
+
| `assistant/src/notifications/emit-signal.ts` | Orchestrates the full pipeline including routing intent enforcement |
|
|
122
|
+
| `assistant/src/notifications/decision-engine.ts` | `enforceRoutingIntent()` post-decision guard |
|
|
123
|
+
| `assistant/src/notifications/signal.ts` | `RoutingIntent` type and `NotificationSignal` fields |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
46
127
|
## Watcher System — Event-Driven Polling
|
|
47
128
|
|
|
48
129
|
Watchers poll external APIs on an interval, detect new events via watermark-based change tracking, and process them through a background LLM session.
|
package/package.json
CHANGED
|
@@ -9,6 +9,10 @@ exports[`IPC message snapshots ClientMessage types auth serializes to expected J
|
|
|
9
9
|
|
|
10
10
|
exports[`IPC message snapshots ClientMessage types user_message serializes to expected JSON 1`] = `
|
|
11
11
|
{
|
|
12
|
+
"commandIntent": {
|
|
13
|
+
"action": "start",
|
|
14
|
+
"domain": "screen_recording",
|
|
15
|
+
},
|
|
12
16
|
"content": "Hello, assistant!",
|
|
13
17
|
"interface": "cli",
|
|
14
18
|
"sessionId": "sess-001",
|
|
@@ -206,6 +210,10 @@ exports[`IPC message snapshots ClientMessage types watch_observation serializes
|
|
|
206
210
|
|
|
207
211
|
exports[`IPC message snapshots ClientMessage types task_submit serializes to expected JSON 1`] = `
|
|
208
212
|
{
|
|
213
|
+
"commandIntent": {
|
|
214
|
+
"action": "start",
|
|
215
|
+
"domain": "screen_recording",
|
|
216
|
+
},
|
|
209
217
|
"screenHeight": 1080,
|
|
210
218
|
"screenWidth": 1920,
|
|
211
219
|
"task": "Open Safari and search for weather",
|
|
@@ -3056,6 +3064,20 @@ exports[`IPC message snapshots ServerMessage types approved_device_remove_respon
|
|
|
3056
3064
|
}
|
|
3057
3065
|
`;
|
|
3058
3066
|
|
|
3067
|
+
exports[`IPC message snapshots ServerMessage types recording_pause serializes to expected JSON 1`] = `
|
|
3068
|
+
{
|
|
3069
|
+
"recordingId": "rec-001",
|
|
3070
|
+
"type": "recording_pause",
|
|
3071
|
+
}
|
|
3072
|
+
`;
|
|
3073
|
+
|
|
3074
|
+
exports[`IPC message snapshots ServerMessage types recording_resume serializes to expected JSON 1`] = `
|
|
3075
|
+
{
|
|
3076
|
+
"recordingId": "rec-001",
|
|
3077
|
+
"type": "recording_resume",
|
|
3078
|
+
}
|
|
3079
|
+
`;
|
|
3080
|
+
|
|
3059
3081
|
exports[`IPC message snapshots ServerMessage types recording_start serializes to expected JSON 1`] = `
|
|
3060
3082
|
{
|
|
3061
3083
|
"recordingId": "rec-001",
|
|
@@ -97,6 +97,7 @@ describe('channel policy registry', () => {
|
|
|
97
97
|
const expectedStrategies: [ChannelId, ConversationStrategy][] = [
|
|
98
98
|
['vellum', 'start_new_conversation'],
|
|
99
99
|
['telegram', 'continue_existing_conversation'],
|
|
100
|
+
['sms', 'continue_existing_conversation'],
|
|
100
101
|
['voice', 'not_deliverable'],
|
|
101
102
|
];
|
|
102
103
|
|
|
@@ -118,6 +119,24 @@ describe('channel policy registry', () => {
|
|
|
118
119
|
}
|
|
119
120
|
});
|
|
120
121
|
|
|
122
|
+
// ── SMS channel policy ───────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
test('SMS is a deliverable notification channel', () => {
|
|
125
|
+
expect(isNotificationDeliverable('sms')).toBe(true);
|
|
126
|
+
expect(getDeliverableChannels()).toContain('sms');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('SMS uses continue_existing_conversation strategy', () => {
|
|
130
|
+
expect(getConversationStrategy('sms')).toBe('continue_existing_conversation');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('all_channels includes SMS when SMS is deliverable', () => {
|
|
134
|
+
const deliverable = getDeliverableChannels();
|
|
135
|
+
expect(deliverable).toContain('vellum');
|
|
136
|
+
expect(deliverable).toContain('telegram');
|
|
137
|
+
expect(deliverable).toContain('sms');
|
|
138
|
+
});
|
|
139
|
+
|
|
121
140
|
// ── Consistency checks ────────────────────────────────────────────────
|
|
122
141
|
|
|
123
142
|
test('channels with not_deliverable strategy have deliveryEnabled: false', () => {
|