@vellumai/assistant 0.4.35 → 0.4.37
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +5 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +814 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +494 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +134 -245
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +175 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +11 -0
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- package/src/security/keychain.ts +0 -490
|
@@ -79,18 +79,18 @@ The broadcaster (`broadcaster.ts`) iterates over selected channels (vellum first
|
|
|
79
79
|
|
|
80
80
|
Each policy defines:
|
|
81
81
|
|
|
82
|
-
| Field
|
|
83
|
-
|
|
84
|
-
| `notification.deliveryEnabled`
|
|
82
|
+
| Field | Type | Description |
|
|
83
|
+
| ----------------------------------- | ---------------------- | ----------------------------------------------------------------- |
|
|
84
|
+
| `notification.deliveryEnabled` | `boolean` | Whether the channel can receive notification deliveries |
|
|
85
85
|
| `notification.conversationStrategy` | `ConversationStrategy` | How conversations are materialized for deliveries on this channel |
|
|
86
86
|
|
|
87
87
|
### Conversation Strategy Types
|
|
88
88
|
|
|
89
|
-
| Strategy
|
|
90
|
-
|
|
91
|
-
| `start_new_conversation`
|
|
92
|
-
| `continue_existing_conversation` |
|
|
93
|
-
| `not_deliverable`
|
|
89
|
+
| Strategy | Behavior | Used by |
|
|
90
|
+
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
91
|
+
| `start_new_conversation` | Creates a fresh conversation per delivery. The thread is surfaced via IPC. | `vellum` |
|
|
92
|
+
| `continue_existing_conversation` | Looks up a previously bound conversation by binding key (sourceChannel + externalChatId) and appends to it. When no bound conversation exists (first delivery to a destination), creates a new one and upserts the binding for future reuse. | `telegram`, `sms`, `whatsapp`, `slack`, `email` |
|
|
93
|
+
| `not_deliverable` | Channel cannot receive notifications. Pairing returns null IDs. | `voice` |
|
|
94
94
|
|
|
95
95
|
### Helper Functions
|
|
96
96
|
|
|
@@ -122,7 +122,7 @@ When the decision engine selects `reuse_existing` for a channel with a valid can
|
|
|
122
122
|
### New Thread Path (`start_new` / default)
|
|
123
123
|
|
|
124
124
|
- **`start_new_conversation`**: Creates a new conversation with `threadType: 'standard'` and `source: 'notification'`, plus an assistant message containing the thread seed. Memory indexing is skipped on the seed message to prevent notification copy from polluting conversational recall. The result has `createdNewConversation: true`.
|
|
125
|
-
- **`continue_existing_conversation`**:
|
|
125
|
+
- **`continue_existing_conversation`**: Looks up a previously bound conversation by binding key (`sourceChannel` + `externalChatId` via `getBindingByChannelChat()`). When a valid bound conversation with `source: 'notification'` exists, the seed message is appended to it and the binding timestamp is refreshed. When no binding exists or the bound conversation is stale/invalid, a new conversation is created and the binding is upserted for future reuse. The result has `createdNewConversation: false` on reuse, `true` on fresh creation.
|
|
126
126
|
- **`not_deliverable`**: Returns `{ conversationId: null, messageId: null }`.
|
|
127
127
|
|
|
128
128
|
The pairing function is resilient -- errors are caught and logged. A pairing failure never breaks the delivery pipeline.
|
|
@@ -131,11 +131,11 @@ The pairing function is resilient -- errors are caught and logged. A pairing fai
|
|
|
131
131
|
|
|
132
132
|
The system produces **three distinct copy outputs** per notification:
|
|
133
133
|
|
|
134
|
-
| Output
|
|
135
|
-
|
|
136
|
-
| `title` + `body`
|
|
137
|
-
| `deliveryText`
|
|
138
|
-
| Thread seed message | Opening message in the notification thread
|
|
134
|
+
| Output | Purpose | Verbosity |
|
|
135
|
+
| ------------------- | -------------------------------------------- | ------------------------ |
|
|
136
|
+
| `title` + `body` | Native notification popup (macOS/iOS banner) | Short and glanceable |
|
|
137
|
+
| `deliveryText` | Channel-native chat message text (Telegram) | Natural chat phrasing |
|
|
138
|
+
| Thread seed message | Opening message in the notification thread | Richer and context-aware |
|
|
139
139
|
|
|
140
140
|
### How It Works
|
|
141
141
|
|
|
@@ -151,12 +151,13 @@ The system produces **three distinct copy outputs** per notification:
|
|
|
151
151
|
|
|
152
152
|
The thread seed composer adapts verbosity to the delivery surface:
|
|
153
153
|
|
|
154
|
-
| Channel
|
|
155
|
-
|
|
156
|
-
| `vellum`
|
|
157
|
-
| `telegram` | `telegram`
|
|
154
|
+
| Channel | Default Interface | Verbosity | Style |
|
|
155
|
+
| ---------- | ----------------- | --------- | ---------------------------------------------- |
|
|
156
|
+
| `vellum` | `macos` | Rich | 2-4 short sentences with context and next step |
|
|
157
|
+
| `telegram` | `telegram` | Compact | 1-2 concise sentences |
|
|
158
158
|
|
|
159
159
|
Interface inference strategy:
|
|
160
|
+
|
|
160
161
|
1. Explicit `interfaceHint` in the signal's `contextPayload` (if valid `InterfaceId`).
|
|
161
162
|
2. `sourceInterface` from the originating conversation (if valid `InterfaceId`).
|
|
162
163
|
3. Channel default mapping (`vellum` → `macos` → rich, `telegram` → `telegram` → compact).
|
|
@@ -164,17 +165,20 @@ Interface inference strategy:
|
|
|
164
165
|
### Example: Reminder Notification
|
|
165
166
|
|
|
166
167
|
**Native popup (vellum/macos):**
|
|
168
|
+
|
|
167
169
|
```
|
|
168
170
|
Title: Reminder
|
|
169
171
|
Body: Take out the trash
|
|
170
172
|
```
|
|
171
173
|
|
|
172
174
|
**Telegram chat delivery (`deliveryText`):**
|
|
175
|
+
|
|
173
176
|
```
|
|
174
177
|
Take out the trash
|
|
175
178
|
```
|
|
176
179
|
|
|
177
180
|
**Thread seed on vellum/macos (rich):**
|
|
181
|
+
|
|
178
182
|
```
|
|
179
183
|
Reminder. Take out the trash. Action required.
|
|
180
184
|
```
|
|
@@ -189,7 +193,10 @@ This is enforced in `broadcaster.ts` by gating the IPC emission on `pairing.crea
|
|
|
189
193
|
// Emit notification_thread_created only when a NEW conversation was
|
|
190
194
|
// actually created. Reusing an existing thread should not fire the IPC
|
|
191
195
|
// event — the client already knows about the conversation.
|
|
192
|
-
if (
|
|
196
|
+
if (
|
|
197
|
+
pairing.createdNewConversation &&
|
|
198
|
+
pairing.strategy === "start_new_conversation"
|
|
199
|
+
) {
|
|
193
200
|
// ... emit IPC event
|
|
194
201
|
}
|
|
195
202
|
```
|
|
@@ -226,11 +233,11 @@ Reminders carry optional routing metadata that controls how notifications fan ou
|
|
|
226
233
|
|
|
227
234
|
The `routing_intent` field on each reminder row specifies the desired channel coverage:
|
|
228
235
|
|
|
229
|
-
| Intent
|
|
230
|
-
|
|
231
|
-
| `single_channel` | Default LLM-driven routing (no override)
|
|
232
|
-
| `multi_channel`
|
|
233
|
-
| `all_channels`
|
|
236
|
+
| Intent | Behavior | When to use |
|
|
237
|
+
| ---------------- | ----------------------------------------------------- | ------------------------------------------------------------------- |
|
|
238
|
+
| `single_channel` | Default LLM-driven routing (no override) | Standard reminders where the decision engine picks the best channel |
|
|
239
|
+
| `multi_channel` | Ensures delivery on 2+ channels when 2+ are connected | Important reminders the user wants on both desktop and phone |
|
|
240
|
+
| `all_channels` | Forces delivery on every connected channel | Critical reminders that must reach the user everywhere |
|
|
234
241
|
|
|
235
242
|
The default is `single_channel`, preserving backward compatibility. Routing intent is persisted in the `reminders` table (`routing_intent` column) and carried through the notification signal as `routingIntent`.
|
|
236
243
|
|
|
@@ -342,10 +349,10 @@ When the decision is persisted, a `threadActions` summary is included in `valida
|
|
|
342
349
|
|
|
343
350
|
Three columns on `notification_deliveries` record the per-channel thread decision:
|
|
344
351
|
|
|
345
|
-
| Column
|
|
346
|
-
|
|
347
|
-
| `thread_action`
|
|
348
|
-
| `thread_target_conversation_id` | TEXT
|
|
352
|
+
| Column | Type | Description |
|
|
353
|
+
| ------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
|
354
|
+
| `thread_action` | TEXT | `'start_new'` or `'reuse_existing'` — what the model decided |
|
|
355
|
+
| `thread_target_conversation_id` | TEXT | The candidate `conversationId` when action is `reuse_existing` |
|
|
349
356
|
| `thread_decision_fallback_used` | INTEGER | `1` if `reuse_existing` was attempted but the target was invalid, so a new conversation was created instead |
|
|
350
357
|
|
|
351
358
|
### Query Examples
|
|
@@ -398,38 +405,38 @@ All three paths use the same pattern: look up pending deliveries by conversation
|
|
|
398
405
|
|
|
399
406
|
All disambiguation messages are generated through `composeGuardianActionMessageGenerative()` in `guardian-action-message-composer.ts`, which uses a 2-tier priority chain (LLM generator with deterministic fallback). Three disambiguation scenarios exist:
|
|
400
407
|
|
|
401
|
-
| Scenario
|
|
402
|
-
|
|
403
|
-
| `guardian_disambiguation`
|
|
404
|
-
| `guardian_expired_disambiguation`
|
|
408
|
+
| Scenario | When triggered |
|
|
409
|
+
| ---------------------------------- | ------------------------------------------------------ |
|
|
410
|
+
| `guardian_disambiguation` | Multiple pending approval requests in a thread |
|
|
411
|
+
| `guardian_expired_disambiguation` | Multiple expired requests with late replies |
|
|
405
412
|
| `guardian_followup_disambiguation` | Multiple follow-up deliveries awaiting guardian action |
|
|
406
413
|
|
|
407
414
|
## Key Files
|
|
408
415
|
|
|
409
|
-
| File
|
|
410
|
-
|
|
411
|
-
| `../channels/config.ts`
|
|
412
|
-
| `emit-signal.ts`
|
|
413
|
-
| `signal.ts`
|
|
414
|
-
| `types.ts`
|
|
415
|
-
| `thread-candidates.ts`
|
|
416
|
-
| `conversation-pairing.ts` | Materializes conversation + message per delivery based on channel strategy
|
|
417
|
-
| `decision-engine.ts`
|
|
418
|
-
| `deterministic-checks.ts` | Pre-send gate checks (dedupe, source-active, channel availability)
|
|
419
|
-
| `runtime-dispatch.ts`
|
|
420
|
-
| `broadcaster.ts`
|
|
421
|
-
| `copy-composer.ts`
|
|
422
|
-
| `thread-seed-composer.ts` | Surface-aware thread seed generation (richer than notification copy)
|
|
423
|
-
| `destination-resolver.ts` | Resolves per-channel endpoints (vellum IPC, Telegram chat ID)
|
|
424
|
-
| `adapters/macos.ts`
|
|
425
|
-
| `adapters/telegram.ts`
|
|
426
|
-
| `adapters/sms.ts`
|
|
427
|
-
| `preference-extractor.ts` | Detects notification preferences in conversation messages
|
|
428
|
-
| `preference-summary.ts`
|
|
429
|
-
| `preferences-store.ts`
|
|
430
|
-
| `events-store.ts`
|
|
431
|
-
| `decisions-store.ts`
|
|
432
|
-
| `deliveries-store.ts`
|
|
416
|
+
| File | Purpose |
|
|
417
|
+
| ------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
418
|
+
| `../channels/config.ts` | Channel policy registry -- single source of truth for per-channel notification behavior |
|
|
419
|
+
| `emit-signal.ts` | Single entry point for producers; orchestrates the full pipeline |
|
|
420
|
+
| `signal.ts` | `NotificationSignal` and `AttentionHints` type definitions |
|
|
421
|
+
| `types.ts` | Channel adapter interfaces, delivery types, decision output contract, `ThreadAction` union |
|
|
422
|
+
| `thread-candidates.ts` | Builds per-channel candidate set of recent notification conversations for the decision engine |
|
|
423
|
+
| `conversation-pairing.ts` | Materializes conversation + message per delivery based on channel strategy |
|
|
424
|
+
| `decision-engine.ts` | LLM-based routing with forced tool_choice; deterministic fallback |
|
|
425
|
+
| `deterministic-checks.ts` | Pre-send gate checks (dedupe, source-active, channel availability) |
|
|
426
|
+
| `runtime-dispatch.ts` | Dispatch gating (no-op decisions, empty channels) |
|
|
427
|
+
| `broadcaster.ts` | Fan-out to channel adapters with delivery audit trail; emits `notification_thread_created` IPC |
|
|
428
|
+
| `copy-composer.ts` | Template-based fallback notification copy when LLM copy is unavailable |
|
|
429
|
+
| `thread-seed-composer.ts` | Surface-aware thread seed generation (richer than notification copy) |
|
|
430
|
+
| `destination-resolver.ts` | Resolves per-channel endpoints (vellum IPC, Telegram chat ID) |
|
|
431
|
+
| `adapters/macos.ts` | Vellum adapter -- broadcasts `notification_intent` via IPC with deep-link metadata |
|
|
432
|
+
| `adapters/telegram.ts` | Telegram adapter -- POSTs to gateway `/deliver/telegram` |
|
|
433
|
+
| `adapters/sms.ts` | SMS adapter -- POSTs to gateway `/deliver/sms` via Twilio Messages API |
|
|
434
|
+
| `preference-extractor.ts` | Detects notification preferences in conversation messages |
|
|
435
|
+
| `preference-summary.ts` | Builds preference context string for the decision engine prompt |
|
|
436
|
+
| `preferences-store.ts` | CRUD for `notification_preferences` table |
|
|
437
|
+
| `events-store.ts` | CRUD for `notification_events` table |
|
|
438
|
+
| `decisions-store.ts` | CRUD for `notification_decisions` table |
|
|
439
|
+
| `deliveries-store.ts` | CRUD for `notification_deliveries` table |
|
|
433
440
|
|
|
434
441
|
## How to Add a New Notification Producer
|
|
435
442
|
|
|
@@ -437,22 +444,24 @@ All disambiguation messages are generated through `composeGuardianActionMessageG
|
|
|
437
444
|
2. Call it with the signal parameters:
|
|
438
445
|
|
|
439
446
|
```ts
|
|
440
|
-
import { emitNotificationSignal } from
|
|
447
|
+
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
441
448
|
|
|
442
449
|
await emitNotificationSignal({
|
|
443
|
-
sourceEventName:
|
|
444
|
-
sourceChannel:
|
|
450
|
+
sourceEventName: "your_event_name",
|
|
451
|
+
sourceChannel: "scheduler", // where the event originated
|
|
445
452
|
sourceSessionId: sessionId,
|
|
446
453
|
attentionHints: {
|
|
447
454
|
requiresAction: true,
|
|
448
|
-
urgency:
|
|
455
|
+
urgency: "high",
|
|
449
456
|
isAsyncBackground: false,
|
|
450
457
|
visibleInSourceNow: false,
|
|
451
458
|
},
|
|
452
|
-
contextPayload: {
|
|
459
|
+
contextPayload: {
|
|
460
|
+
/* arbitrary data for the decision engine */
|
|
461
|
+
},
|
|
453
462
|
// Optional: control multi-channel fanout behavior
|
|
454
|
-
routingIntent:
|
|
455
|
-
routingHints: { preferredChannels: [
|
|
463
|
+
routingIntent: "multi_channel", // 'single_channel' | 'multi_channel' | 'all_channels'
|
|
464
|
+
routingHints: { preferredChannels: ["telegram", "sms"] },
|
|
456
465
|
});
|
|
457
466
|
```
|
|
458
467
|
|
|
@@ -483,11 +492,11 @@ For vellum (macOS/iOS) deliveries, the audit trail now extends past the IPC broa
|
|
|
483
492
|
|
|
484
493
|
The ack populates three columns on `notification_deliveries`:
|
|
485
494
|
|
|
486
|
-
| Column
|
|
487
|
-
|
|
488
|
-
| `client_delivery_status` | TEXT
|
|
489
|
-
| `client_delivery_error`
|
|
490
|
-
| `client_delivery_at`
|
|
495
|
+
| Column | Type | Description |
|
|
496
|
+
| ------------------------ | ------- | ------------------------------------------------------------------------------ |
|
|
497
|
+
| `client_delivery_status` | TEXT | `'delivered'` if the OS accepted the notification, `'client_failed'` otherwise |
|
|
498
|
+
| `client_delivery_error` | TEXT | Error description when the post failed (e.g. authorization denied) |
|
|
499
|
+
| `client_delivery_at` | INTEGER | Epoch ms timestamp of when the client reported the outcome |
|
|
491
500
|
|
|
492
501
|
This means the audit trail can now answer three questions for each vellum delivery:
|
|
493
502
|
|
|
@@ -539,8 +548,8 @@ Preferences are sanitized against prompt injection (angle brackets replaced with
|
|
|
539
548
|
|
|
540
549
|
All settings live under the `notifications` key in `config.json`:
|
|
541
550
|
|
|
542
|
-
| Key
|
|
543
|
-
|
|
551
|
+
| Key | Type | Default | Description |
|
|
552
|
+
| ----------------------------------- | ------ | --------------------- | ------------------------------------------------------------------------ |
|
|
544
553
|
| `notifications.decisionModelIntent` | string | `"latency-optimized"` | Model intent used for both the decision engine and preference extraction |
|
|
545
554
|
|
|
546
555
|
The notification pipeline is always active -- signals are processed and dispatched as soon as the daemon is running. The audit trail (events, decisions, deliveries) is written for every signal.
|
|
@@ -185,11 +185,12 @@ export class NotificationBroadcaster {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// Pair the delivery with a conversation before sending, passing the thread action
|
|
188
|
+
// and destination binding context for channel-scoped continuation
|
|
188
189
|
const pairing = await pairDeliveryWithConversation(
|
|
189
190
|
signal,
|
|
190
191
|
channel,
|
|
191
192
|
copy,
|
|
192
|
-
{ threadAction },
|
|
193
|
+
{ threadAction, bindingContext: destination.bindingContext },
|
|
193
194
|
);
|
|
194
195
|
|
|
195
196
|
// For the vellum channel, merge the conversationId into deep-link metadata
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* auditable conversation trail and enables the macOS/iOS client to
|
|
7
7
|
* deep-link directly into the notification thread.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Resolution order:
|
|
10
|
+
* 1. Explicit `reuse_existing` thread action — highest precedence.
|
|
11
|
+
* 2. Binding-key reuse — for `continue_existing_conversation` channels,
|
|
12
|
+
* looks up a previously bound conversation by (sourceChannel, externalChatId).
|
|
13
|
+
* 3. Default — creates a fresh conversation and, when binding context is
|
|
14
|
+
* present, upserts it into the external-conversation store for future reuse.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import type { ConversationStrategy } from "../channels/config.js";
|
|
@@ -19,14 +22,34 @@ import {
|
|
|
19
22
|
createConversation,
|
|
20
23
|
getConversation,
|
|
21
24
|
} from "../memory/conversation-store.js";
|
|
25
|
+
import {
|
|
26
|
+
getBindingByChannelChat,
|
|
27
|
+
upsertOutboundBinding,
|
|
28
|
+
} from "../memory/external-conversation-store.js";
|
|
22
29
|
import { getLogger } from "../util/logger.js";
|
|
23
30
|
import type { NotificationSignal } from "./signal.js";
|
|
24
31
|
import { composeThreadSeed, isThreadSeedSane } from "./thread-seed-composer.js";
|
|
25
|
-
import type {
|
|
32
|
+
import type {
|
|
33
|
+
DestinationBindingContext,
|
|
34
|
+
NotificationChannel,
|
|
35
|
+
ThreadAction,
|
|
36
|
+
} from "./types.js";
|
|
26
37
|
import type { RenderedChannelCopy } from "./types.js";
|
|
27
38
|
|
|
28
39
|
const log = getLogger("notification-conversation-pairing");
|
|
29
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Prefix applied to sourceChannel values in notification bindings so they
|
|
43
|
+
* occupy a separate namespace from messaging adapter bindings in the
|
|
44
|
+
* external_conversation_bindings table. Without this, notification pairing
|
|
45
|
+
* and messaging adapters (Telegram, SMS, etc.) would destructively overwrite
|
|
46
|
+
* each other's bindings since both use (sourceChannel, externalChatId) as key.
|
|
47
|
+
*/
|
|
48
|
+
const NOTIFICATION_CHANNEL_PREFIX = "notification:";
|
|
49
|
+
function notificationChannel(sourceChannel: string): string {
|
|
50
|
+
return `${NOTIFICATION_CHANNEL_PREFIX}${sourceChannel}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
export interface PairingResult {
|
|
31
54
|
conversationId: string | null;
|
|
32
55
|
messageId: string | null;
|
|
@@ -40,6 +63,8 @@ export interface PairingResult {
|
|
|
40
63
|
export interface PairingOptions {
|
|
41
64
|
/** Per-channel thread action from the decision engine. */
|
|
42
65
|
threadAction?: ThreadAction;
|
|
66
|
+
/** Destination binding data for channel-scoped conversation continuation. */
|
|
67
|
+
bindingContext?: DestinationBindingContext;
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
/**
|
|
@@ -48,11 +73,13 @@ export interface PairingOptions {
|
|
|
48
73
|
* Looks up the channel's conversation strategy from the policy registry
|
|
49
74
|
* and materializes a conversation + assistant message accordingly.
|
|
50
75
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* a
|
|
55
|
-
*
|
|
76
|
+
* Resolution precedence:
|
|
77
|
+
* 1. `options.threadAction === "reuse_existing"` — reuse the explicit target.
|
|
78
|
+
* 2. `continue_existing_conversation` strategy with binding context —
|
|
79
|
+
* look up a previously bound conversation by (sourceChannel, externalChatId).
|
|
80
|
+
* 3. Create a new conversation (and upsert the binding when context is present).
|
|
81
|
+
*
|
|
82
|
+
* Invalid/stale targets at any level fall through to the next.
|
|
56
83
|
*
|
|
57
84
|
* Errors are caught and logged — this function never throws so the
|
|
58
85
|
* notification pipeline is not disrupted by pairing failures.
|
|
@@ -78,10 +105,9 @@ export async function pairDeliveryWithConversation(
|
|
|
78
105
|
|
|
79
106
|
const title = copy.threadTitle ?? copy.title ?? signal.sourceEventName;
|
|
80
107
|
|
|
81
|
-
// Only start_new_conversation threads should be user-visible
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// true continuation-by-key is implemented.
|
|
108
|
+
// Only start_new_conversation threads should be user-visible in the sidebar.
|
|
109
|
+
// Channels with continue_existing_conversation reuse bound external threads
|
|
110
|
+
// and mark them as background so they don't clutter the sidebar UI.
|
|
85
111
|
const threadType =
|
|
86
112
|
strategy === "start_new_conversation" ? "standard" : "background";
|
|
87
113
|
|
|
@@ -93,6 +119,7 @@ export async function pairDeliveryWithConversation(
|
|
|
93
119
|
: composeThreadSeed(signal, channel, copy);
|
|
94
120
|
|
|
95
121
|
const threadAction = options?.threadAction;
|
|
122
|
+
const bindingContext = options?.bindingContext;
|
|
96
123
|
|
|
97
124
|
// Attempt to reuse an existing conversation when the model requests it
|
|
98
125
|
if (threadAction?.action === "reuse_existing") {
|
|
@@ -109,6 +136,16 @@ export async function pairDeliveryWithConversation(
|
|
|
109
136
|
{ skipIndexing: true },
|
|
110
137
|
);
|
|
111
138
|
|
|
139
|
+
// Rebind the destination so subsequent deliveries to the same
|
|
140
|
+
// (sourceChannel, externalChatId) resolve to this conversation.
|
|
141
|
+
if (bindingContext?.sourceChannel && bindingContext?.externalChatId) {
|
|
142
|
+
upsertOutboundBinding({
|
|
143
|
+
conversationId: existing.id,
|
|
144
|
+
sourceChannel: notificationChannel(bindingContext.sourceChannel),
|
|
145
|
+
externalChatId: bindingContext.externalChatId,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
112
149
|
log.info(
|
|
113
150
|
{
|
|
114
151
|
signalId: signal.signalId,
|
|
@@ -156,6 +193,16 @@ export async function pairDeliveryWithConversation(
|
|
|
156
193
|
{ skipIndexing: true },
|
|
157
194
|
);
|
|
158
195
|
|
|
196
|
+
// Bind the new conversation to the destination so subsequent
|
|
197
|
+
// deliveries reuse it instead of creating yet another conversation.
|
|
198
|
+
if (bindingContext?.sourceChannel && bindingContext?.externalChatId) {
|
|
199
|
+
upsertOutboundBinding({
|
|
200
|
+
conversationId: conversation.id,
|
|
201
|
+
sourceChannel: notificationChannel(bindingContext.sourceChannel),
|
|
202
|
+
externalChatId: bindingContext.externalChatId,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
159
206
|
return {
|
|
160
207
|
conversationId: conversation.id,
|
|
161
208
|
messageId: message.id,
|
|
@@ -165,6 +212,83 @@ export async function pairDeliveryWithConversation(
|
|
|
165
212
|
};
|
|
166
213
|
}
|
|
167
214
|
|
|
215
|
+
// For channels with continue_existing_conversation strategy, try to
|
|
216
|
+
// reuse a previously bound conversation keyed by (sourceChannel, externalChatId)
|
|
217
|
+
// before falling through to create a new one.
|
|
218
|
+
if (
|
|
219
|
+
strategy === "continue_existing_conversation" &&
|
|
220
|
+
bindingContext?.sourceChannel &&
|
|
221
|
+
bindingContext?.externalChatId
|
|
222
|
+
) {
|
|
223
|
+
// Look up by namespaced key first; fall back to pre-namespace key for
|
|
224
|
+
// bindings created before the notification: prefix was introduced.
|
|
225
|
+
const existingBinding =
|
|
226
|
+
getBindingByChannelChat(
|
|
227
|
+
notificationChannel(bindingContext.sourceChannel),
|
|
228
|
+
bindingContext.externalChatId,
|
|
229
|
+
) ??
|
|
230
|
+
getBindingByChannelChat(
|
|
231
|
+
bindingContext.sourceChannel,
|
|
232
|
+
bindingContext.externalChatId,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (existingBinding) {
|
|
236
|
+
const boundConversation = getConversation(
|
|
237
|
+
existingBinding.conversationId,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (boundConversation && boundConversation.source === "notification") {
|
|
241
|
+
const message = await addMessage(
|
|
242
|
+
boundConversation.id,
|
|
243
|
+
"assistant",
|
|
244
|
+
messageContent,
|
|
245
|
+
undefined,
|
|
246
|
+
{ skipIndexing: true },
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Touch the outbound timestamp so the binding stays fresh.
|
|
250
|
+
upsertOutboundBinding({
|
|
251
|
+
conversationId: boundConversation.id,
|
|
252
|
+
sourceChannel: notificationChannel(bindingContext.sourceChannel),
|
|
253
|
+
externalChatId: bindingContext.externalChatId,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
log.info(
|
|
257
|
+
{
|
|
258
|
+
signalId: signal.signalId,
|
|
259
|
+
channel,
|
|
260
|
+
strategy,
|
|
261
|
+
conversationId: boundConversation.id,
|
|
262
|
+
messageId: message.id,
|
|
263
|
+
bindingKey: `${bindingContext.sourceChannel}:${bindingContext.externalChatId}`,
|
|
264
|
+
},
|
|
265
|
+
"Reused bound conversation for channel destination",
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
conversationId: boundConversation.id,
|
|
270
|
+
messageId: message.id,
|
|
271
|
+
strategy,
|
|
272
|
+
createdNewConversation: false,
|
|
273
|
+
threadDecisionFallbackUsed: false,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Binding exists but conversation is stale or wrong source — fall through
|
|
278
|
+
// to create a new one and re-bind below.
|
|
279
|
+
log.warn(
|
|
280
|
+
{
|
|
281
|
+
signalId: signal.signalId,
|
|
282
|
+
channel,
|
|
283
|
+
boundConversationId: existingBinding.conversationId,
|
|
284
|
+
boundConversationExists: !!boundConversation,
|
|
285
|
+
boundConversationSource: boundConversation?.source,
|
|
286
|
+
},
|
|
287
|
+
"Bound conversation stale or invalid — creating fresh conversation",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
168
292
|
// Default path: create a new conversation
|
|
169
293
|
// Memory indexing is skipped on the seed message below to prevent
|
|
170
294
|
// notification copy from polluting conversational recall.
|
|
@@ -184,6 +308,16 @@ export async function pairDeliveryWithConversation(
|
|
|
184
308
|
{ skipIndexing: true },
|
|
185
309
|
);
|
|
186
310
|
|
|
311
|
+
// When binding context is available, record the new conversation so
|
|
312
|
+
// subsequent deliveries to the same destination reuse it.
|
|
313
|
+
if (bindingContext?.sourceChannel && bindingContext?.externalChatId) {
|
|
314
|
+
upsertOutboundBinding({
|
|
315
|
+
conversationId: conversation.id,
|
|
316
|
+
sourceChannel: notificationChannel(bindingContext.sourceChannel),
|
|
317
|
+
externalChatId: bindingContext.externalChatId,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
187
321
|
log.info(
|
|
188
322
|
{
|
|
189
323
|
signalId: signal.signalId,
|
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
buildGuardianRequestCodeInstruction,
|
|
14
14
|
resolveGuardianQuestionInstructionMode,
|
|
15
15
|
} from "./guardian-question-mode.js";
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
NotificationSignal,
|
|
18
|
+
NotificationSourceEventName,
|
|
19
|
+
} from "./signal.js";
|
|
17
20
|
import type { NotificationChannel, RenderedChannelCopy } from "./types.js";
|
|
18
21
|
|
|
19
22
|
type CopyTemplate = (payload: Record<string, unknown>) => RenderedChannelCopy;
|
|
@@ -229,7 +232,7 @@ export function buildAccessRequestContractText(
|
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
// Templates keyed by dot-separated sourceEventName strings matching producers.
|
|
232
|
-
const TEMPLATES: Record<
|
|
235
|
+
const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
|
|
233
236
|
"reminder.fired": (payload) => ({
|
|
234
237
|
title: "Reminder",
|
|
235
238
|
body: str(payload.message, str(payload.label, "A reminder has fired")),
|
|
@@ -367,7 +370,8 @@ export function composeFallbackCopy(
|
|
|
367
370
|
signal: NotificationSignal,
|
|
368
371
|
channels: NotificationChannel[],
|
|
369
372
|
): Partial<Record<NotificationChannel, RenderedChannelCopy>> {
|
|
370
|
-
const template =
|
|
373
|
+
const template =
|
|
374
|
+
TEMPLATES[signal.sourceEventName as NotificationSourceEventName];
|
|
371
375
|
|
|
372
376
|
const baseCopy: RenderedChannelCopy = template
|
|
373
377
|
? template(signal.contextPayload)
|
|
@@ -66,12 +66,19 @@ export function resolveDestinations(
|
|
|
66
66
|
case "sms": {
|
|
67
67
|
const guardianResult = findGuardianForChannel(channel);
|
|
68
68
|
if (guardianResult && guardianResult.channel.externalChatId) {
|
|
69
|
+
const externalChatId = guardianResult.channel.externalChatId;
|
|
69
70
|
result.set(channel as NotificationChannel, {
|
|
70
71
|
channel: channel as NotificationChannel,
|
|
71
|
-
endpoint:
|
|
72
|
+
endpoint: externalChatId,
|
|
72
73
|
metadata: {
|
|
73
74
|
externalUserId: guardianResult.channel.externalUserId,
|
|
74
75
|
},
|
|
76
|
+
bindingContext: {
|
|
77
|
+
sourceChannel: channel as NotificationChannel,
|
|
78
|
+
externalChatId,
|
|
79
|
+
externalUserId:
|
|
80
|
+
guardianResult.channel.externalUserId ?? undefined,
|
|
81
|
+
},
|
|
75
82
|
});
|
|
76
83
|
}
|
|
77
84
|
log.debug(
|
|
@@ -97,6 +104,12 @@ export function resolveDestinations(
|
|
|
97
104
|
metadata: {
|
|
98
105
|
externalUserId: guardianResult.channel.externalUserId,
|
|
99
106
|
},
|
|
107
|
+
bindingContext: {
|
|
108
|
+
sourceChannel: "slack",
|
|
109
|
+
externalChatId: chatId,
|
|
110
|
+
externalUserId:
|
|
111
|
+
guardianResult.channel.externalUserId ?? undefined,
|
|
112
|
+
},
|
|
100
113
|
});
|
|
101
114
|
} else if (guardianResult && chatId) {
|
|
102
115
|
log.warn(
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
AttentionHints,
|
|
35
35
|
NotificationContextPayload,
|
|
36
36
|
NotificationSignal,
|
|
37
|
+
NotificationSourceChannel,
|
|
37
38
|
RoutingIntent,
|
|
38
39
|
} from "./signal.js";
|
|
39
40
|
import type {
|
|
@@ -151,8 +152,8 @@ function getConnectedChannels(): NotificationChannel[] {
|
|
|
151
152
|
export interface EmitSignalParams<TEventName extends string = string> {
|
|
152
153
|
/** Free-form event name, e.g. 'reminder.fired', 'schedule.complete'. */
|
|
153
154
|
sourceEventName: TEventName;
|
|
154
|
-
/** Source channel that produced the event. */
|
|
155
|
-
sourceChannel:
|
|
155
|
+
/** Source channel that produced the event — must be a registered channel. */
|
|
156
|
+
sourceChannel: NotificationSourceChannel;
|
|
156
157
|
/** Session or conversation ID from the source context. */
|
|
157
158
|
sourceSessionId: string;
|
|
158
159
|
/** Attention hints for the decision engine. */
|