@vellumai/assistant 0.3.15 → 0.3.18

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 (306) hide show
  1. package/ARCHITECTURE.md +211 -12
  2. package/Dockerfile +1 -1
  3. package/README.md +11 -5
  4. package/docs/architecture/http-token-refresh.md +274 -0
  5. package/docs/architecture/memory.md +5 -4
  6. package/docs/architecture/scheduling.md +4 -88
  7. package/docs/runbook-trusted-contacts.md +283 -0
  8. package/docs/trusted-contact-access.md +247 -0
  9. package/package.json +1 -1
  10. package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
  11. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
  12. package/src/__tests__/access-request-decision.test.ts +328 -0
  13. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  14. package/src/__tests__/asset-search-tool.test.ts +15 -15
  15. package/src/__tests__/attachments-store.test.ts +13 -13
  16. package/src/__tests__/call-controller.test.ts +150 -4
  17. package/src/__tests__/call-conversation-messages.test.ts +2 -2
  18. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  19. package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +108 -12
  21. package/src/__tests__/channel-guardian.test.ts +19 -15
  22. package/src/__tests__/checker.test.ts +103 -48
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +356 -0
  25. package/src/__tests__/conversation-pairing.test.ts +127 -27
  26. package/src/__tests__/conversation-store.test.ts +36 -36
  27. package/src/__tests__/date-context.test.ts +179 -1
  28. package/src/__tests__/db-migration-rollback.test.ts +4 -7
  29. package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
  30. package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
  31. package/src/__tests__/gateway-only-guard.test.ts +188 -0
  32. package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
  33. package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
  34. package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
  35. package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
  36. package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-store.test.ts +182 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  40. package/src/__tests__/guardian-dispatch.test.ts +120 -0
  41. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  42. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  43. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  44. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  45. package/src/__tests__/hooks-runner.test.ts +13 -4
  46. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  47. package/src/__tests__/intent-routing.test.ts +14 -0
  48. package/src/__tests__/ipc-snapshot.test.ts +23 -5
  49. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  50. package/src/__tests__/memory-regressions.test.ts +16 -12
  51. package/src/__tests__/non-member-access-request.test.ts +281 -0
  52. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  53. package/src/__tests__/notification-decision-strategy.test.ts +138 -1
  54. package/src/__tests__/notification-deep-link.test.ts +44 -1
  55. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  56. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  57. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  58. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  59. package/src/__tests__/recording-intent.test.ts +1 -0
  60. package/src/__tests__/recording-state-machine.test.ts +328 -17
  61. package/src/__tests__/registry.test.ts +17 -8
  62. package/src/__tests__/relay-server.test.ts +105 -0
  63. package/src/__tests__/reminder.test.ts +13 -0
  64. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  65. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  66. package/src/__tests__/server-history-render.test.ts +8 -8
  67. package/src/__tests__/session-agent-loop.test.ts +1 -0
  68. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -0
  70. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  71. package/src/__tests__/slack-channel-config.test.ts +230 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  73. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  74. package/src/__tests__/system-prompt.test.ts +43 -0
  75. package/src/__tests__/task-management-tools.test.ts +3 -3
  76. package/src/__tests__/task-tools.test.ts +3 -3
  77. package/src/__tests__/trust-store.test.ts +38 -22
  78. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
  79. package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
  80. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  81. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  82. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  83. package/src/__tests__/update-bulletin.test.ts +323 -0
  84. package/src/__tests__/update-template-contract.test.ts +24 -0
  85. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  86. package/src/agent/loop.ts +2 -2
  87. package/src/amazon/client.ts +2 -3
  88. package/src/calls/call-controller.ts +241 -39
  89. package/src/calls/call-conversation-messages.ts +2 -2
  90. package/src/calls/call-domain.ts +10 -3
  91. package/src/calls/call-pointer-messages.ts +17 -5
  92. package/src/calls/guardian-action-sweep.ts +77 -36
  93. package/src/calls/guardian-dispatch.ts +8 -0
  94. package/src/calls/relay-server.ts +51 -12
  95. package/src/calls/twilio-routes.ts +3 -1
  96. package/src/calls/types.ts +1 -1
  97. package/src/calls/voice-session-bridge.ts +8 -6
  98. package/src/cli/core-commands.ts +43 -3
  99. package/src/cli/map.ts +8 -5
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  101. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  102. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  103. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  104. package/src/config/computer-use-prompt.ts +1 -0
  105. package/src/config/core-schema.ts +16 -0
  106. package/src/config/env-registry.ts +1 -0
  107. package/src/config/env.ts +16 -1
  108. package/src/config/memory-schema.ts +5 -0
  109. package/src/config/schema.ts +4 -0
  110. package/src/config/system-prompt.ts +69 -2
  111. package/src/config/templates/BOOTSTRAP.md +1 -1
  112. package/src/config/templates/IDENTITY.md +8 -4
  113. package/src/config/templates/SOUL.md +14 -0
  114. package/src/config/templates/UPDATES.md +15 -0
  115. package/src/config/templates/USER.md +5 -1
  116. package/src/config/types.ts +1 -0
  117. package/src/config/update-bulletin-format.ts +54 -0
  118. package/src/config/update-bulletin-state.ts +49 -0
  119. package/src/config/update-bulletin-template-path.ts +6 -0
  120. package/src/config/update-bulletin.ts +97 -0
  121. package/src/config/vellum-skills/catalog.json +6 -0
  122. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  123. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  124. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  125. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  126. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  127. package/src/context/window-manager.ts +43 -3
  128. package/src/daemon/config-watcher.ts +4 -2
  129. package/src/daemon/connection-policy.ts +21 -1
  130. package/src/daemon/daemon-control.ts +219 -8
  131. package/src/daemon/date-context.ts +174 -1
  132. package/src/daemon/guardian-action-generators.ts +175 -0
  133. package/src/daemon/guardian-verification-intent.ts +120 -0
  134. package/src/daemon/handlers/apps.ts +1 -3
  135. package/src/daemon/handlers/config-channels.ts +2 -2
  136. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  137. package/src/daemon/handlers/config-inbox.ts +55 -159
  138. package/src/daemon/handlers/config-ingress.ts +1 -1
  139. package/src/daemon/handlers/config-integrations.ts +1 -1
  140. package/src/daemon/handlers/config-platform.ts +1 -1
  141. package/src/daemon/handlers/config-scheduling.ts +2 -2
  142. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  143. package/src/daemon/handlers/config-telegram.ts +1 -1
  144. package/src/daemon/handlers/config-twilio.ts +1 -1
  145. package/src/daemon/handlers/config-voice.ts +100 -0
  146. package/src/daemon/handlers/config.ts +3 -0
  147. package/src/daemon/handlers/identity.ts +45 -25
  148. package/src/daemon/handlers/misc.ts +83 -5
  149. package/src/daemon/handlers/navigate-settings.ts +27 -0
  150. package/src/daemon/handlers/recording.ts +270 -144
  151. package/src/daemon/handlers/sessions.ts +100 -17
  152. package/src/daemon/handlers/subagents.ts +3 -3
  153. package/src/daemon/handlers/work-items.ts +10 -7
  154. package/src/daemon/ipc-contract/integrations.ts +9 -1
  155. package/src/daemon/ipc-contract/messages.ts +4 -0
  156. package/src/daemon/ipc-contract/sessions.ts +1 -1
  157. package/src/daemon/ipc-contract/settings.ts +26 -0
  158. package/src/daemon/ipc-contract/shared.ts +2 -0
  159. package/src/daemon/ipc-contract/work-items.ts +1 -7
  160. package/src/daemon/ipc-contract/workspace.ts +12 -1
  161. package/src/daemon/ipc-contract-inventory.json +6 -1
  162. package/src/daemon/ipc-contract.ts +5 -1
  163. package/src/daemon/lifecycle.ts +314 -266
  164. package/src/daemon/recording-intent.ts +0 -41
  165. package/src/daemon/response-tier.ts +2 -2
  166. package/src/daemon/server.ts +31 -9
  167. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  168. package/src/daemon/session-agent-loop.ts +15 -8
  169. package/src/daemon/session-history.ts +3 -2
  170. package/src/daemon/session-media-retry.ts +3 -0
  171. package/src/daemon/session-messaging.ts +38 -4
  172. package/src/daemon/session-notifiers.ts +2 -2
  173. package/src/daemon/session-process.ts +546 -59
  174. package/src/daemon/session-queue-manager.ts +2 -0
  175. package/src/daemon/session-runtime-assembly.ts +39 -0
  176. package/src/daemon/session-skill-tools.ts +13 -4
  177. package/src/daemon/session-tool-setup.ts +5 -6
  178. package/src/daemon/session.ts +19 -8
  179. package/src/daemon/tls-certs.ts +60 -13
  180. package/src/daemon/tool-side-effects.ts +13 -5
  181. package/src/gallery/default-gallery.ts +32 -9
  182. package/src/influencer/client.ts +2 -1
  183. package/src/memory/channel-delivery-store.ts +35 -567
  184. package/src/memory/channel-guardian-store.ts +63 -1317
  185. package/src/memory/conflict-store.ts +4 -4
  186. package/src/memory/conversation-attention-store.ts +0 -3
  187. package/src/memory/conversation-crud.ts +668 -0
  188. package/src/memory/conversation-queries.ts +361 -0
  189. package/src/memory/conversation-store.ts +44 -983
  190. package/src/memory/db-connection.ts +3 -0
  191. package/src/memory/db-init.ts +33 -0
  192. package/src/memory/delivery-channels.ts +175 -0
  193. package/src/memory/delivery-crud.ts +211 -0
  194. package/src/memory/delivery-status.ts +199 -0
  195. package/src/memory/embedding-backend.ts +70 -4
  196. package/src/memory/embedding-local.ts +12 -2
  197. package/src/memory/entity-extractor.ts +3 -8
  198. package/src/memory/fts-reconciler.ts +136 -0
  199. package/src/memory/guardian-action-store.ts +418 -5
  200. package/src/memory/guardian-approvals.ts +569 -0
  201. package/src/memory/guardian-bindings.ts +130 -0
  202. package/src/memory/guardian-rate-limits.ts +196 -0
  203. package/src/memory/guardian-verification.ts +521 -0
  204. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  205. package/src/memory/job-utils.ts +8 -5
  206. package/src/memory/jobs-store.ts +66 -6
  207. package/src/memory/jobs-worker.ts +23 -1
  208. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  209. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  210. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  211. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  212. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  213. package/src/memory/migrations/100-core-tables.ts +1 -1
  214. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  215. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  216. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  217. package/src/memory/migrations/113-late-migrations.ts +1 -1
  218. package/src/memory/migrations/116-messages-fts.ts +13 -0
  219. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  220. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  221. package/src/memory/migrations/index.ts +10 -3
  222. package/src/memory/migrations/validate-migration-state.ts +114 -15
  223. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  224. package/src/memory/retriever.ts +46 -13
  225. package/src/memory/schema-migration.ts +4 -0
  226. package/src/memory/schema.ts +31 -8
  227. package/src/memory/search/semantic.ts +8 -90
  228. package/src/notifications/README.md +159 -18
  229. package/src/notifications/broadcaster.ts +69 -33
  230. package/src/notifications/conversation-pairing.ts +99 -21
  231. package/src/notifications/decision-engine.ts +176 -8
  232. package/src/notifications/deliveries-store.ts +39 -8
  233. package/src/notifications/emit-signal.ts +1 -0
  234. package/src/notifications/preferences-store.ts +7 -7
  235. package/src/notifications/thread-candidates.ts +269 -0
  236. package/src/notifications/types.ts +19 -0
  237. package/src/permissions/checker.ts +1 -16
  238. package/src/permissions/defaults.ts +25 -5
  239. package/src/permissions/prompter.ts +17 -0
  240. package/src/permissions/trust-store.ts +2 -0
  241. package/src/providers/failover.ts +19 -0
  242. package/src/providers/registry.ts +46 -1
  243. package/src/runtime/approval-message-composer.ts +1 -1
  244. package/src/runtime/channel-guardian-service.ts +15 -3
  245. package/src/runtime/channel-retry-sweep.ts +7 -2
  246. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  247. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  248. package/src/runtime/guardian-action-message-composer.ts +245 -0
  249. package/src/runtime/guardian-outbound-actions.ts +26 -6
  250. package/src/runtime/guardian-verification-templates.ts +15 -9
  251. package/src/runtime/http-errors.ts +93 -0
  252. package/src/runtime/http-server.ts +133 -44
  253. package/src/runtime/http-types.ts +53 -0
  254. package/src/runtime/ingress-service.ts +237 -0
  255. package/src/runtime/middleware/error-handler.ts +4 -3
  256. package/src/runtime/middleware/rate-limiter.ts +160 -0
  257. package/src/runtime/middleware/request-logger.ts +71 -0
  258. package/src/runtime/middleware/twilio-validation.ts +7 -6
  259. package/src/runtime/pending-interactions.ts +12 -0
  260. package/src/runtime/routes/access-request-decision.ts +215 -0
  261. package/src/runtime/routes/app-routes.ts +25 -18
  262. package/src/runtime/routes/approval-routes.ts +18 -47
  263. package/src/runtime/routes/attachment-routes.ts +15 -41
  264. package/src/runtime/routes/call-routes.ts +20 -20
  265. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  266. package/src/runtime/routes/contact-routes.ts +4 -9
  267. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  268. package/src/runtime/routes/conversation-routes.ts +26 -57
  269. package/src/runtime/routes/debug-routes.ts +71 -0
  270. package/src/runtime/routes/events-routes.ts +3 -2
  271. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  272. package/src/runtime/routes/identity-routes.ts +14 -10
  273. package/src/runtime/routes/inbound-conversation.ts +3 -2
  274. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  275. package/src/runtime/routes/ingress-routes.ts +174 -0
  276. package/src/runtime/routes/integration-routes.ts +78 -16
  277. package/src/runtime/routes/pairing-routes.ts +11 -10
  278. package/src/runtime/routes/secret-routes.ts +10 -18
  279. package/src/runtime/verification-rate-limiter.ts +83 -0
  280. package/src/schedule/schedule-store.ts +13 -1
  281. package/src/schedule/scheduler.ts +1 -1
  282. package/src/security/secret-ingress.ts +5 -2
  283. package/src/security/secret-scanner.ts +72 -6
  284. package/src/subagent/manager.ts +6 -4
  285. package/src/swarm/plan-validator.ts +4 -1
  286. package/src/tasks/task-runner.ts +3 -1
  287. package/src/tools/browser/api-map.ts +9 -6
  288. package/src/tools/calls/call-start.ts +20 -0
  289. package/src/tools/executor.ts +50 -568
  290. package/src/tools/permission-checker.ts +271 -0
  291. package/src/tools/registry.ts +14 -6
  292. package/src/tools/reminder/reminder-store.ts +7 -7
  293. package/src/tools/reminder/reminder.ts +6 -3
  294. package/src/tools/secret-detection-handler.ts +301 -0
  295. package/src/tools/subagent/message.ts +1 -1
  296. package/src/tools/system/voice-config.ts +62 -0
  297. package/src/tools/tasks/index.ts +3 -3
  298. package/src/tools/tasks/work-item-list.ts +3 -3
  299. package/src/tools/tasks/work-item-update.ts +4 -5
  300. package/src/tools/tool-approval-handler.ts +192 -0
  301. package/src/tools/tool-manifest.ts +2 -0
  302. package/src/version.ts +29 -2
  303. package/src/watcher/watcher-store.ts +9 -9
  304. package/src/work-items/work-item-runner.ts +9 -6
  305. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  306. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Thread candidate builder for notification thread reuse.
3
+ *
4
+ * Builds a lightweight candidate set of recent notification conversations
5
+ * per channel that the decision engine can choose to reuse instead of
6
+ * starting a new thread. Includes guardian-specific context (pending
7
+ * unresolved request count) when available.
8
+ *
9
+ * The candidate set is intentionally compact — only the fields the LLM
10
+ * needs for a routing decision, not full conversation contents.
11
+ */
12
+
13
+ import { and, count, desc, eq, inArray, isNotNull } from 'drizzle-orm';
14
+
15
+ import { getDb } from '../memory/db.js';
16
+ import { channelGuardianApprovalRequests, conversations, notificationDecisions, notificationDeliveries, notificationEvents } from '../memory/schema.js';
17
+ import { getLogger } from '../util/logger.js';
18
+ import type { NotificationChannel } from './types.js';
19
+
20
+ const log = getLogger('thread-candidates');
21
+
22
+ /** Maximum number of candidate threads to surface per channel. */
23
+ const MAX_CANDIDATES_PER_CHANNEL = 5;
24
+
25
+ /** Only consider conversations updated within this window (ms). */
26
+ const CANDIDATE_RECENCY_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
27
+
28
+ // -- Public types -------------------------------------------------------------
29
+
30
+ /** Guardian-specific context attached to a thread candidate when available. */
31
+ export interface GuardianCandidateContext {
32
+ /** Number of unresolved (pending) guardian approval requests in this conversation. */
33
+ pendingUnresolvedRequestCount: number;
34
+ }
35
+
36
+ /** A single candidate conversation that the decision engine can select for reuse. */
37
+ export interface ThreadCandidate {
38
+ conversationId: string;
39
+ title: string | null;
40
+ updatedAt: number;
41
+ /** The source event name from the most recent notification delivered to this conversation. */
42
+ latestSourceEventName: string | null;
43
+ channel: NotificationChannel;
44
+ /** Guardian-specific context, present only when there are relevant guardian records. */
45
+ guardianContext?: GuardianCandidateContext;
46
+ }
47
+
48
+ /** Candidate set for the decision engine, keyed by channel. */
49
+ export type ThreadCandidateSet = Partial<Record<NotificationChannel, ThreadCandidate[]>>;
50
+
51
+ // -- Core builder -------------------------------------------------------------
52
+
53
+ /**
54
+ * Build the thread candidate set for all selected channels.
55
+ *
56
+ * Queries recent notification-sourced conversations that were delivered
57
+ * to each channel and enriches them with guardian-specific metadata
58
+ * when available.
59
+ *
60
+ * Errors are caught per-channel so a failure in one channel does not
61
+ * block candidates for others.
62
+ */
63
+ export function buildThreadCandidates(
64
+ channels: NotificationChannel[],
65
+ assistantId: string,
66
+ ): ThreadCandidateSet {
67
+ const result: ThreadCandidateSet = {};
68
+ const cutoff = Date.now() - CANDIDATE_RECENCY_WINDOW_MS;
69
+
70
+ for (const channel of channels) {
71
+ try {
72
+ const candidates = buildCandidatesForChannel(channel, assistantId, cutoff);
73
+ if (candidates.length > 0) {
74
+ result[channel] = candidates;
75
+ }
76
+ } catch (err) {
77
+ const errMsg = err instanceof Error ? err.message : String(err);
78
+ log.warn({ err: errMsg, channel }, 'Failed to build thread candidates for channel');
79
+ }
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ // -- Per-channel query --------------------------------------------------------
86
+
87
+ /**
88
+ * Query recent notification conversations for a given channel.
89
+ *
90
+ * Joins notification_deliveries -> notification_decisions -> notification_events
91
+ * to find conversations that were created by the notification pipeline for
92
+ * this channel, then enriches with guardian context.
93
+ */
94
+ function buildCandidatesForChannel(
95
+ channel: NotificationChannel,
96
+ assistantId: string,
97
+ cutoffMs: number,
98
+ ): ThreadCandidate[] {
99
+ const db = getDb();
100
+
101
+ // Find recent notification deliveries for this channel that have a
102
+ // conversationId and were successfully sent.
103
+ const rows = db
104
+ .select({
105
+ conversationId: notificationDeliveries.conversationId,
106
+ channel: notificationDeliveries.channel,
107
+ deliverySentAt: notificationDeliveries.sentAt,
108
+ sourceEventName: notificationEvents.sourceEventName,
109
+ convTitle: conversations.title,
110
+ convUpdatedAt: conversations.updatedAt,
111
+ })
112
+ .from(notificationDeliveries)
113
+ .innerJoin(
114
+ notificationDecisions,
115
+ eq(notificationDeliveries.notificationDecisionId, notificationDecisions.id),
116
+ )
117
+ .innerJoin(
118
+ notificationEvents,
119
+ eq(notificationDecisions.notificationEventId, notificationEvents.id),
120
+ )
121
+ .innerJoin(
122
+ conversations,
123
+ eq(notificationDeliveries.conversationId, conversations.id),
124
+ )
125
+ .where(
126
+ and(
127
+ eq(notificationDeliveries.channel, channel),
128
+ eq(notificationDeliveries.assistantId, assistantId),
129
+ eq(notificationDeliveries.status, 'sent'),
130
+ isNotNull(notificationDeliveries.conversationId),
131
+ ),
132
+ )
133
+ .orderBy(desc(notificationDeliveries.sentAt))
134
+ .limit(MAX_CANDIDATES_PER_CHANNEL * 3) // over-fetch to allow deduplication
135
+ .all();
136
+
137
+ // Deduplicate by conversationId (keep the most recent delivery per conversation)
138
+ const seen = new Set<string>();
139
+ const candidates: ThreadCandidate[] = [];
140
+
141
+ for (const row of rows) {
142
+ if (!row.conversationId) continue;
143
+ if (seen.has(row.conversationId)) continue;
144
+
145
+ // Apply recency filter on the conversation's updatedAt
146
+ if (row.convUpdatedAt < cutoffMs) continue;
147
+
148
+ seen.add(row.conversationId);
149
+
150
+ candidates.push({
151
+ conversationId: row.conversationId,
152
+ title: row.convTitle,
153
+ updatedAt: row.convUpdatedAt,
154
+ latestSourceEventName: row.sourceEventName ?? null,
155
+ channel: channel,
156
+ });
157
+
158
+ if (candidates.length >= MAX_CANDIDATES_PER_CHANNEL) break;
159
+ }
160
+
161
+ // Batch-enrich all candidates with guardian context in a single query
162
+ if (candidates.length > 0) {
163
+ const pendingCounts = batchCountPendingByConversation(
164
+ candidates.map((c) => c.conversationId),
165
+ assistantId,
166
+ );
167
+ for (const candidate of candidates) {
168
+ const pendingCount = pendingCounts.get(candidate.conversationId) ?? 0;
169
+ if (pendingCount > 0) {
170
+ candidate.guardianContext = { pendingUnresolvedRequestCount: pendingCount };
171
+ }
172
+ }
173
+ }
174
+
175
+ return candidates;
176
+ }
177
+
178
+ // -- Guardian context enrichment ----------------------------------------------
179
+
180
+ /**
181
+ * Batch-count pending guardian approval requests for multiple conversations
182
+ * in a single query. Returns a map from conversationId to pending count
183
+ * (only entries with count > 0 are included).
184
+ */
185
+ function batchCountPendingByConversation(
186
+ conversationIds: string[],
187
+ assistantId: string,
188
+ ): Map<string, number> {
189
+ const result = new Map<string, number>();
190
+ if (conversationIds.length === 0) return result;
191
+
192
+ try {
193
+ const db = getDb();
194
+
195
+ const rows = db
196
+ .select({
197
+ conversationId: channelGuardianApprovalRequests.conversationId,
198
+ count: count(),
199
+ })
200
+ .from(channelGuardianApprovalRequests)
201
+ .where(
202
+ and(
203
+ inArray(channelGuardianApprovalRequests.conversationId, conversationIds),
204
+ eq(channelGuardianApprovalRequests.status, 'pending'),
205
+ eq(channelGuardianApprovalRequests.assistantId, assistantId),
206
+ ),
207
+ )
208
+ .groupBy(channelGuardianApprovalRequests.conversationId)
209
+ .all();
210
+
211
+ for (const row of rows) {
212
+ if (row.count > 0) {
213
+ result.set(row.conversationId, row.count);
214
+ }
215
+ }
216
+ } catch (err) {
217
+ const errMsg = err instanceof Error ? err.message : String(err);
218
+ log.warn({ err: errMsg }, 'Failed to batch-query guardian context for candidates');
219
+ }
220
+
221
+ return result;
222
+ }
223
+
224
+ // -- Prompt serialization -----------------------------------------------------
225
+
226
+ /**
227
+ * Serialize a thread candidate set into a compact text block suitable for
228
+ * injection into the decision engine's user prompt.
229
+ *
230
+ * Designed to be token-efficient while giving the LLM enough context
231
+ * to make a reuse decision.
232
+ */
233
+ export function serializeCandidatesForPrompt(candidateSet: ThreadCandidateSet): string | null {
234
+ const channelEntries = Object.entries(candidateSet) as [NotificationChannel, ThreadCandidate[]][];
235
+ if (channelEntries.length === 0) return null;
236
+
237
+ const sections: string[] = [];
238
+
239
+ for (const [channel, candidates] of channelEntries) {
240
+ if (candidates.length === 0) continue;
241
+
242
+ const lines: string[] = [`Channel: ${channel}`];
243
+ for (const c of candidates) {
244
+ // Escape title to prevent format corruption from quotes or newlines in
245
+ // user/model-provided text. JSON.stringify produces a safe single-line
246
+ // quoted string; we strip the outer quotes since we wrap in our own.
247
+ const safeTitle = c.title
248
+ ? JSON.stringify(c.title).slice(1, -1)
249
+ : '(untitled)';
250
+ const parts: string[] = [
251
+ ` - id=${c.conversationId}`,
252
+ `title="${safeTitle}"`,
253
+ `updated=${new Date(c.updatedAt).toISOString()}`,
254
+ ];
255
+ if (c.latestSourceEventName) {
256
+ const safeEventName = JSON.stringify(c.latestSourceEventName).slice(1, -1);
257
+ parts.push(`lastEvent="${safeEventName}"`);
258
+ }
259
+ if (c.guardianContext) {
260
+ parts.push(`pendingRequests=${c.guardianContext.pendingUnresolvedRequestCount}`);
261
+ }
262
+ lines.push(parts.join(' '));
263
+ }
264
+ sections.push(lines.join('\n'));
265
+ }
266
+
267
+ if (sections.length === 0) return null;
268
+ return sections.join('\n\n');
269
+ }
@@ -79,12 +79,31 @@ export interface RenderedChannelCopy {
79
79
  threadSeedMessage?: string;
80
80
  }
81
81
 
82
+ // -- Thread action types ------------------------------------------------------
83
+
84
+ /** Start a new conversation thread for the notification delivery. */
85
+ export interface ThreadActionStartNew {
86
+ action: 'start_new';
87
+ }
88
+
89
+ /** Reuse an existing conversation thread identified by conversationId. */
90
+ export interface ThreadActionReuseExisting {
91
+ action: 'reuse_existing';
92
+ conversationId: string;
93
+ }
94
+
95
+ /** Per-channel thread action — either start a new thread or reuse an existing one. */
96
+ export type ThreadAction = ThreadActionStartNew | ThreadActionReuseExisting;
97
+
98
+
82
99
  /** Output produced by the notification decision engine for a given signal. */
83
100
  export interface NotificationDecision {
84
101
  shouldNotify: boolean;
85
102
  selectedChannels: NotificationChannel[];
86
103
  reasoningSummary: string;
87
104
  renderedCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>>;
105
+ /** Per-channel thread actions decided by the model. Absent channels default to start_new. */
106
+ threadActions?: Partial<Record<NotificationChannel, ThreadAction>>;
88
107
  deepLinkTarget?: Record<string, unknown>;
89
108
  dedupeKey: string;
90
109
  confidence: number;
@@ -123,19 +123,6 @@ function getWrappedProgram(seg: { program: string; args: string[] }): string | u
123
123
  return undefined;
124
124
  }
125
125
 
126
- function isHighRiskRm(args: string[]): boolean {
127
- // rm with -r, -rf, -fr, or targeting root/home
128
- for (const arg of args) {
129
- if (arg.startsWith('-') && (arg.includes('r') || arg.includes('f'))) {
130
- return true;
131
- }
132
- if (arg === '/' || arg === '~' || arg === '$HOME') {
133
- return true;
134
- }
135
- }
136
- return false;
137
- }
138
-
139
126
  function getStringField(input: Record<string, unknown>, ...keys: string[]): string {
140
127
  for (const key of keys) {
141
128
  const value = input[key];
@@ -398,9 +385,7 @@ async function classifyRiskUncached(toolName: string, input: Record<string, unkn
398
385
  if (HIGH_RISK_PROGRAMS.has(prog)) return RiskLevel.High;
399
386
 
400
387
  if (prog === 'rm') {
401
- if (isHighRiskRm(seg.args)) return RiskLevel.High;
402
- maxRisk = RiskLevel.Medium;
403
- continue;
388
+ return RiskLevel.High;
404
389
  }
405
390
 
406
391
  if (prog === 'chmod' || prog === 'chown' || prog === 'chgrp'
@@ -37,6 +37,13 @@ const COMPUTER_USE_TOOLS = [
37
37
  * Computed at runtime so paths reflect the configured root directory.
38
38
  */
39
39
  export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
40
+ // Some test suites mock getConfig() with partial objects; treat missing
41
+ // branches as defaults so rule generation remains deterministic.
42
+ const config = getConfig() as {
43
+ sandbox?: { enabled?: boolean };
44
+ skills?: { load?: { extraDirs?: unknown } };
45
+ };
46
+
40
47
  const hostFileRules = HOST_FILE_TOOLS.map((tool) => ({
41
48
  id: `default:ask-${tool}-global`,
42
49
  tool,
@@ -50,11 +57,11 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
50
57
  // global default ask rule uses "**" (globstar) instead of a "tool:*" prefix
51
58
  // because commands often contain "/" (e.g. "cat /etc/hosts").
52
59
  const hostShellRule: DefaultRuleTemplate = {
53
- id: 'default:ask-host_bash-global',
60
+ id: 'default:allow-host_bash-global',
54
61
  tool: 'host_bash',
55
62
  pattern: '**',
56
63
  scope: 'everywhere',
57
- decision: 'ask',
64
+ decision: 'allow',
58
65
  priority: 50,
59
66
  };
60
67
 
@@ -62,7 +69,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
62
69
  // them (including high-risk) so the user is never prompted for sandbox work.
63
70
  // Only emit this rule when the sandbox is actually enabled; otherwise bash
64
71
  // commands execute on the host and must go through normal permission checks.
65
- const sandboxEnabled = getConfig().sandbox.enabled;
72
+ const sandboxEnabled = config.sandbox?.enabled !== false;
66
73
  const sandboxShellRule: DefaultRuleTemplate | null = sandboxEnabled
67
74
  ? {
68
75
  id: 'default:allow-bash-global',
@@ -105,7 +112,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
105
112
  // and write these without prompting. Also allow `rm BOOTSTRAP.md` so the
106
113
  // agent can delete it at the end of the onboarding ritual.
107
114
  const workspaceDir = join(getRootDir(), 'workspace').replaceAll('\\', '/');
108
- const WORKSPACE_PROMPT_FILES = ['IDENTITY.md', 'USER.md', 'SOUL.md', 'BOOTSTRAP.md'] as const;
115
+ const WORKSPACE_PROMPT_FILES = ['IDENTITY.md', 'USER.md', 'SOUL.md', 'BOOTSTRAP.md', 'UPDATES.md'] as const;
109
116
  const WORKSPACE_FILE_TOOLS = ['file_read', 'file_write', 'file_edit'] as const;
110
117
  const workspacePromptRules = WORKSPACE_FILE_TOOLS.flatMap((tool) =>
111
118
  WORKSPACE_PROMPT_FILES.map((file) => ({
@@ -127,6 +134,15 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
127
134
  priority: 100,
128
135
  };
129
136
 
137
+ const updatesDeleteRule: DefaultRuleTemplate = {
138
+ id: 'default:allow-bash-rm-updates',
139
+ tool: 'bash',
140
+ pattern: 'rm UPDATES.md',
141
+ scope: workspaceDir,
142
+ decision: 'allow',
143
+ priority: 100,
144
+ };
145
+
130
146
  // Skill source directories — writing or editing skill source files should
131
147
  // require explicit user approval so a compromised agent loop cannot silently
132
148
  // modify skill code to escalate privileges.
@@ -140,7 +156,10 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
140
156
 
141
157
  // Append any user-configured extra skill directories so they get the
142
158
  // same default ask rules as managed and bundled dirs.
143
- const extraDirs = getConfig().skills.load.extraDirs;
159
+ const rawExtraDirs = config.skills?.load?.extraDirs;
160
+ const extraDirs = Array.isArray(rawExtraDirs)
161
+ ? rawExtraDirs.filter((dir): dir is string => typeof dir === 'string')
162
+ : [];
144
163
  for (let i = 0; i < extraDirs.length; i++) {
145
164
  skillDirs.push({ dir: extraDirs[i].replaceAll('\\', '/'), label: `extra-${i}` });
146
165
  }
@@ -248,6 +267,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
248
267
  ...managedSkillRules,
249
268
  ...workspacePromptRules,
250
269
  bootstrapDeleteRule,
270
+ updatesDeleteRule,
251
271
  ...skillSourceMutationRules,
252
272
  skillLoadRule,
253
273
  browserNavigateRule,
@@ -114,6 +114,23 @@ export class PermissionPrompter {
114
114
  pending.resolve({ decision, selectedPattern, selectedScope, decisionContext });
115
115
  }
116
116
 
117
+ /**
118
+ * Deny all pending confirmation prompts at once. Used when a new user
119
+ * message arrives while confirmations are outstanding — the agent will
120
+ * see the denial and can re-request if still needed.
121
+ */
122
+ denyAllPending(): void {
123
+ for (const [requestId, pending] of this.pending) {
124
+ clearTimeout(pending.timer);
125
+ this.pending.delete(requestId);
126
+ pending.resolve({ decision: 'deny', decisionContext: 'The user sent a new message instead of responding to this permission prompt. Stop what you are doing and respond to the user\'s new message. Do NOT retry this tool or request permission again until the user asks you to.' });
127
+ }
128
+ }
129
+
130
+ get hasPending(): boolean {
131
+ return this.pending.size > 0;
132
+ }
133
+
117
134
  dispose(): void {
118
135
  for (const [, pending] of this.pending) {
119
136
  clearTimeout(pending.timer);
@@ -276,6 +276,8 @@ function loadFromDisk(): TrustRule[] {
276
276
  // on loaded rules would silently widen their scope to global
277
277
  // wildcards. Stripping them and re-saving prevents scope escalation.
278
278
  for (const rule of rules) {
279
+ // Legacy v3 rules may carry principal-scoped fields that no longer
280
+ // exist in the TrustRule interface — cast to strip them at runtime.
279
281
  const r = rule as unknown as Record<string, unknown>;
280
282
  if ('principalKind' in r || 'principalId' in r || 'principalVersion' in r) {
281
283
  delete r.principalKind;
@@ -47,6 +47,12 @@ function isFailoverError(error: unknown): boolean {
47
47
  return false;
48
48
  }
49
49
 
50
+ export interface ProviderHealthStatus {
51
+ name: string;
52
+ healthy: boolean;
53
+ unhealthySince: string | null;
54
+ }
55
+
50
56
  export class FailoverProvider implements Provider {
51
57
  public readonly name: string;
52
58
  private readonly healthMap = new Map<string, ProviderHealth>();
@@ -126,4 +132,17 @@ export class FailoverProvider implements Provider {
126
132
  undefined,
127
133
  );
128
134
  }
135
+
136
+ getHealthStatus(): ProviderHealthStatus[] {
137
+ return this.providers.map((p) => {
138
+ const health = this.healthMap.get(p.name)!;
139
+ return {
140
+ name: p.name,
141
+ healthy: health.unhealthySince == null,
142
+ unhealthySince: health.unhealthySince != null
143
+ ? new Date(health.unhealthySince).toISOString()
144
+ : null,
145
+ };
146
+ });
147
+ }
129
148
  }
@@ -1,7 +1,7 @@
1
1
  import { wrapWithLogfire } from "../logfire.js";
2
2
  import { ConfigError } from "../util/errors.js";
3
3
  import { AnthropicProvider } from "./anthropic/client.js";
4
- import { FailoverProvider } from "./failover.js";
4
+ import { FailoverProvider, type ProviderHealthStatus } from "./failover.js";
5
5
  import { FireworksProvider } from "./fireworks/client.js";
6
6
  import { GeminiProvider } from "./gemini/client.js";
7
7
  import { getProviderDefaultModel } from "./model-intents.js";
@@ -138,6 +138,51 @@ function resolveModel(config: ProvidersConfig, providerName: string): string {
138
138
  return getProviderDefaultModel(providerName);
139
139
  }
140
140
 
141
+ export interface ProviderDebugStatus {
142
+ configuredPrimary: string;
143
+ activePrimary: string | null;
144
+ usedFallback: boolean;
145
+ registeredProviders: string[];
146
+ failoverHealth: ProviderHealthStatus[] | null;
147
+ overallHealth: 'healthy' | 'degraded' | 'down';
148
+ }
149
+
150
+ export function getProviderDebugStatus(
151
+ configuredProvider: string,
152
+ providerOrder: string[],
153
+ ): ProviderDebugStatus {
154
+ const registered = listProviders();
155
+ const selection = resolveProviderSelection(configuredProvider, providerOrder);
156
+
157
+ let failoverHealth: ProviderHealthStatus[] | null = null;
158
+ if (cachedFailoverProvider) {
159
+ failoverHealth = cachedFailoverProvider.getHealthStatus();
160
+ }
161
+
162
+ let overallHealth: 'healthy' | 'degraded' | 'down' = 'down';
163
+ if (registered.length > 0 && selection.selectedPrimary) {
164
+ if (!failoverHealth) {
165
+ overallHealth = 'healthy';
166
+ } else {
167
+ const healthyCount = failoverHealth.filter((h) => h.healthy).length;
168
+ if (healthyCount === failoverHealth.length) {
169
+ overallHealth = 'healthy';
170
+ } else if (healthyCount > 0) {
171
+ overallHealth = 'degraded';
172
+ }
173
+ }
174
+ }
175
+
176
+ return {
177
+ configuredPrimary: configuredProvider,
178
+ activePrimary: selection.selectedPrimary,
179
+ usedFallback: selection.usedFallbackPrimary,
180
+ registeredProviders: registered,
181
+ failoverHealth,
182
+ overallHealth,
183
+ };
184
+ }
185
+
141
186
  export function initializeProviders(config: ProvidersConfig): void {
142
187
  providers.clear();
143
188
  cachedFailoverProvider = null;
@@ -223,7 +223,7 @@ export function getFallbackMessage(context: ApprovalMessageContext): string {
223
223
  // consistency; wording adapts to channel and code type.
224
224
  const code = context.verifyCommand ?? 'the verification code';
225
225
  // Detect whether the code is a short numeric (identity-bound outbound)
226
- // or a high-entropy hex (inbound challenge) and adjust wording.
226
+ // or a high-entropy hex (inbound challenge/bootstrap) and adjust wording.
227
227
  const isNumeric = /^\d{4,8}$/.test(code);
228
228
  if (context.channel === 'voice') {
229
229
  if (isNumeric) {
@@ -9,7 +9,7 @@
9
9
  import { createHash,randomBytes } from 'crypto';
10
10
  import { v4 as uuid } from 'uuid';
11
11
 
12
- import type { GuardianBinding, IdentityBindingStatus,SessionStatus, VerificationChallenge } from '../memory/channel-guardian-store.js';
12
+ import type { GuardianBinding, IdentityBindingStatus, SessionStatus, VerificationChallenge, VerificationPurpose } from '../memory/channel-guardian-store.js';
13
13
  import {
14
14
  bindSessionIdentity as storeBindSessionIdentity,
15
15
  consumeChallenge,
@@ -62,7 +62,8 @@ export interface CreateChallengeResult {
62
62
  }
63
63
 
64
64
  export type ValidateChallengeResult =
65
- | { success: true; bindingId: string }
65
+ | { success: true; bindingId: string; verificationType: 'guardian' }
66
+ | { success: true; verificationType: 'trusted_contact' }
66
67
  | { success: false; reason: string };
67
68
 
68
69
  // ---------------------------------------------------------------------------
@@ -272,6 +273,15 @@ export function validateAndConsumeChallenge(
272
273
  // Reset the rate-limit counter on success
273
274
  resetRateLimit(assistantId, channel, actorExternalUserId, actorChatId);
274
275
 
276
+ // Trusted contact verification sessions (created by the access request
277
+ // approval flow) should NOT create a guardian binding — the requester is
278
+ // becoming a trusted contact, not a guardian. The explicit verificationPurpose
279
+ // field distinguishes this from guardian outbound verification which also uses
280
+ // identity-bound sessions.
281
+ if (challenge.verificationPurpose === 'trusted_contact') {
282
+ return { success: true, verificationType: 'trusted_contact' };
283
+ }
284
+
275
285
  // Reject if a different user already holds the guardian binding
276
286
  const existingBinding = getActiveBinding(assistantId, channel);
277
287
  if (existingBinding && existingBinding.guardianExternalUserId !== actorExternalUserId) {
@@ -302,7 +312,7 @@ export function validateAndConsumeChallenge(
302
312
  metadataJson: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null,
303
313
  });
304
314
 
305
- return { success: true, bindingId: binding.id };
315
+ return { success: true, bindingId: binding.id, verificationType: 'guardian' };
306
316
  }
307
317
 
308
318
  /**
@@ -396,6 +406,7 @@ export function createOutboundSession(params: {
396
406
  maxAttempts?: number;
397
407
  sessionId?: string;
398
408
  bootstrapTokenHash?: string;
409
+ verificationPurpose?: VerificationPurpose;
399
410
  }): CreateOutboundSessionResult {
400
411
  // Use high-entropy hex for unbound bootstrap sessions to prevent brute-force;
401
412
  // 6-digit numeric codes are only safe when identity is already bound.
@@ -421,6 +432,7 @@ export function createOutboundSession(params: {
421
432
  destinationAddress: params.destinationAddress,
422
433
  codeDigits: params.codeDigits,
423
434
  maxAttempts: params.maxAttempts,
435
+ verificationPurpose: params.verificationPurpose,
424
436
  bootstrapTokenHash: params.bootstrapTokenHash,
425
437
  });
426
438
 
@@ -117,6 +117,7 @@ export async function sweepFailedEvents(
117
117
  },
118
118
  assistantId,
119
119
  guardianContext,
120
+ isInteractive: guardianContext?.actorRole === 'guardian',
120
121
  },
121
122
  sourceChannel,
122
123
  sourceInterface,
@@ -133,7 +134,11 @@ export async function sweepFailedEvents(
133
134
  ? payload.externalChatId
134
135
  : undefined;
135
136
  if (externalChatId) {
136
- const startFromSegment = channelDeliveryStore.getDeliveredSegmentCount(event.id);
137
+ // processMessage above generated a fresh assistant response, so any
138
+ // previously tracked segment progress belongs to the old response and
139
+ // must not carry over. Reset to 0 so we deliver all segments of the
140
+ // new response.
141
+ channelDeliveryStore.updateDeliveredSegmentCount(event.id, 0);
137
142
  await deliverReplyViaCallback(
138
143
  event.conversationId,
139
144
  externalChatId,
@@ -141,7 +146,7 @@ export async function sweepFailedEvents(
141
146
  bearerToken,
142
147
  assistantId,
143
148
  {
144
- startFromSegment,
149
+ startFromSegment: 0,
145
150
  onSegmentDelivered: (count) =>
146
151
  channelDeliveryStore.updateDeliveredSegmentCount(event.id, count),
147
152
  },