@vellumai/assistant 0.3.15 → 0.3.16

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 (290) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +1 -1
  3. package/README.md +5 -5
  4. package/docs/architecture/http-token-refresh.md +252 -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 +331 -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 +16 -14
  22. package/src/__tests__/checker.test.ts +24 -0
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +358 -0
  25. package/src/__tests__/conversation-pairing.test.ts +24 -24
  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 +294 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  39. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  40. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  41. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  43. package/src/__tests__/hooks-runner.test.ts +13 -4
  44. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  45. package/src/__tests__/intent-routing.test.ts +14 -0
  46. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  47. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  48. package/src/__tests__/memory-regressions.test.ts +16 -12
  49. package/src/__tests__/non-member-access-request.test.ts +282 -0
  50. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  51. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  52. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  53. package/src/__tests__/recording-intent.test.ts +1 -0
  54. package/src/__tests__/recording-state-machine.test.ts +328 -17
  55. package/src/__tests__/registry.test.ts +17 -8
  56. package/src/__tests__/relay-server.test.ts +105 -0
  57. package/src/__tests__/reminder.test.ts +13 -0
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  59. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  60. package/src/__tests__/server-history-render.test.ts +8 -8
  61. package/src/__tests__/session-agent-loop.test.ts +1 -0
  62. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  63. package/src/__tests__/session-skill-tools.test.ts +1 -0
  64. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  65. package/src/__tests__/slack-channel-config.test.ts +230 -0
  66. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  67. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  68. package/src/__tests__/system-prompt.test.ts +43 -0
  69. package/src/__tests__/task-management-tools.test.ts +3 -3
  70. package/src/__tests__/task-tools.test.ts +3 -3
  71. package/src/__tests__/trust-store.test.ts +17 -1
  72. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  73. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  74. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  75. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  76. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  77. package/src/__tests__/update-bulletin.test.ts +260 -0
  78. package/src/__tests__/update-template-contract.test.ts +29 -0
  79. package/src/agent/loop.ts +2 -2
  80. package/src/amazon/client.ts +2 -3
  81. package/src/calls/call-controller.ts +115 -34
  82. package/src/calls/call-conversation-messages.ts +2 -2
  83. package/src/calls/call-domain.ts +10 -3
  84. package/src/calls/call-pointer-messages.ts +17 -5
  85. package/src/calls/guardian-action-sweep.ts +77 -36
  86. package/src/calls/relay-server.ts +51 -12
  87. package/src/calls/twilio-routes.ts +3 -1
  88. package/src/calls/types.ts +1 -1
  89. package/src/calls/voice-session-bridge.ts +4 -4
  90. package/src/cli/core-commands.ts +3 -3
  91. package/src/cli/map.ts +8 -5
  92. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  93. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  94. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  95. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  96. package/src/config/computer-use-prompt.ts +1 -0
  97. package/src/config/core-schema.ts +16 -0
  98. package/src/config/env-registry.ts +1 -0
  99. package/src/config/env.ts +16 -1
  100. package/src/config/memory-schema.ts +5 -0
  101. package/src/config/schema.ts +4 -0
  102. package/src/config/system-prompt.ts +69 -2
  103. package/src/config/templates/BOOTSTRAP.md +1 -1
  104. package/src/config/templates/IDENTITY.md +8 -4
  105. package/src/config/templates/SOUL.md +14 -0
  106. package/src/config/templates/UPDATES.md +16 -0
  107. package/src/config/templates/USER.md +5 -1
  108. package/src/config/types.ts +1 -0
  109. package/src/config/update-bulletin-format.ts +52 -0
  110. package/src/config/update-bulletin-state.ts +49 -0
  111. package/src/config/update-bulletin.ts +82 -0
  112. package/src/config/vellum-skills/catalog.json +6 -0
  113. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  114. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  115. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  116. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  117. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  118. package/src/context/window-manager.ts +43 -3
  119. package/src/daemon/config-watcher.ts +1 -0
  120. package/src/daemon/connection-policy.ts +21 -1
  121. package/src/daemon/daemon-control.ts +164 -7
  122. package/src/daemon/date-context.ts +174 -1
  123. package/src/daemon/guardian-action-generators.ts +175 -0
  124. package/src/daemon/guardian-verification-intent.ts +120 -0
  125. package/src/daemon/handlers/apps.ts +1 -3
  126. package/src/daemon/handlers/config-channels.ts +2 -2
  127. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  128. package/src/daemon/handlers/config-inbox.ts +55 -159
  129. package/src/daemon/handlers/config-ingress.ts +1 -1
  130. package/src/daemon/handlers/config-integrations.ts +1 -1
  131. package/src/daemon/handlers/config-platform.ts +1 -1
  132. package/src/daemon/handlers/config-scheduling.ts +2 -2
  133. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  134. package/src/daemon/handlers/config-telegram.ts +1 -1
  135. package/src/daemon/handlers/config-twilio.ts +1 -1
  136. package/src/daemon/handlers/config-voice.ts +100 -0
  137. package/src/daemon/handlers/config.ts +3 -0
  138. package/src/daemon/handlers/misc.ts +83 -5
  139. package/src/daemon/handlers/navigate-settings.ts +27 -0
  140. package/src/daemon/handlers/recording.ts +270 -144
  141. package/src/daemon/handlers/sessions.ts +100 -17
  142. package/src/daemon/handlers/subagents.ts +3 -3
  143. package/src/daemon/handlers/work-items.ts +10 -7
  144. package/src/daemon/ipc-contract/integrations.ts +9 -1
  145. package/src/daemon/ipc-contract/messages.ts +4 -0
  146. package/src/daemon/ipc-contract/sessions.ts +1 -1
  147. package/src/daemon/ipc-contract/settings.ts +26 -0
  148. package/src/daemon/ipc-contract/shared.ts +2 -0
  149. package/src/daemon/ipc-contract/work-items.ts +1 -7
  150. package/src/daemon/ipc-contract-inventory.json +5 -1
  151. package/src/daemon/ipc-contract.ts +5 -1
  152. package/src/daemon/lifecycle.ts +306 -266
  153. package/src/daemon/recording-intent.ts +0 -41
  154. package/src/daemon/response-tier.ts +2 -2
  155. package/src/daemon/server.ts +6 -6
  156. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  157. package/src/daemon/session-agent-loop.ts +15 -8
  158. package/src/daemon/session-history.ts +3 -2
  159. package/src/daemon/session-media-retry.ts +3 -0
  160. package/src/daemon/session-messaging.ts +38 -4
  161. package/src/daemon/session-notifiers.ts +2 -2
  162. package/src/daemon/session-process.ts +256 -23
  163. package/src/daemon/session-queue-manager.ts +2 -0
  164. package/src/daemon/session-runtime-assembly.ts +39 -0
  165. package/src/daemon/session-skill-tools.ts +13 -4
  166. package/src/daemon/session-tool-setup.ts +5 -6
  167. package/src/daemon/session.ts +19 -8
  168. package/src/daemon/tls-certs.ts +55 -13
  169. package/src/daemon/tool-side-effects.ts +13 -5
  170. package/src/gallery/default-gallery.ts +32 -9
  171. package/src/influencer/client.ts +2 -1
  172. package/src/memory/channel-delivery-store.ts +37 -567
  173. package/src/memory/channel-guardian-store.ts +66 -1317
  174. package/src/memory/conflict-store.ts +4 -4
  175. package/src/memory/conversation-attention-store.ts +0 -3
  176. package/src/memory/conversation-crud.ts +668 -0
  177. package/src/memory/conversation-queries.ts +361 -0
  178. package/src/memory/conversation-store.ts +45 -983
  179. package/src/memory/db-connection.ts +3 -0
  180. package/src/memory/db-init.ts +25 -0
  181. package/src/memory/delivery-channels.ts +175 -0
  182. package/src/memory/delivery-crud.ts +211 -0
  183. package/src/memory/delivery-status.ts +199 -0
  184. package/src/memory/embedding-backend.ts +70 -4
  185. package/src/memory/embedding-local.ts +12 -2
  186. package/src/memory/entity-extractor.ts +3 -8
  187. package/src/memory/fts-reconciler.ts +121 -0
  188. package/src/memory/guardian-action-store.ts +366 -3
  189. package/src/memory/guardian-approvals.ts +569 -0
  190. package/src/memory/guardian-bindings.ts +130 -0
  191. package/src/memory/guardian-rate-limits.ts +196 -0
  192. package/src/memory/guardian-verification.ts +520 -0
  193. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  194. package/src/memory/job-utils.ts +8 -5
  195. package/src/memory/jobs-store.ts +66 -6
  196. package/src/memory/jobs-worker.ts +23 -1
  197. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  198. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  199. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  200. package/src/memory/migrations/100-core-tables.ts +1 -1
  201. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  202. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  203. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  204. package/src/memory/migrations/113-late-migrations.ts +1 -1
  205. package/src/memory/migrations/116-messages-fts.ts +13 -0
  206. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  207. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  208. package/src/memory/migrations/index.ts +8 -3
  209. package/src/memory/migrations/validate-migration-state.ts +114 -15
  210. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  211. package/src/memory/retriever.ts +46 -13
  212. package/src/memory/schema-migration.ts +3 -0
  213. package/src/memory/schema.ts +25 -7
  214. package/src/memory/search/semantic.ts +8 -90
  215. package/src/notifications/README.md +1 -1
  216. package/src/notifications/broadcaster.ts +20 -2
  217. package/src/notifications/conversation-pairing.ts +3 -3
  218. package/src/notifications/decision-engine.ts +173 -8
  219. package/src/notifications/deliveries-store.ts +27 -8
  220. package/src/notifications/preferences-store.ts +7 -7
  221. package/src/notifications/thread-candidates.ts +234 -0
  222. package/src/notifications/types.ts +18 -0
  223. package/src/permissions/defaults.ts +11 -1
  224. package/src/permissions/prompter.ts +17 -0
  225. package/src/permissions/trust-store.ts +2 -0
  226. package/src/providers/failover.ts +19 -0
  227. package/src/providers/registry.ts +46 -1
  228. package/src/runtime/approval-message-composer.ts +1 -1
  229. package/src/runtime/channel-guardian-service.ts +15 -3
  230. package/src/runtime/channel-retry-sweep.ts +7 -2
  231. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  232. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  233. package/src/runtime/guardian-action-message-composer.ts +245 -0
  234. package/src/runtime/guardian-outbound-actions.ts +26 -6
  235. package/src/runtime/guardian-verification-templates.ts +15 -9
  236. package/src/runtime/http-errors.ts +93 -0
  237. package/src/runtime/http-server.ts +133 -44
  238. package/src/runtime/http-types.ts +53 -0
  239. package/src/runtime/ingress-service.ts +237 -0
  240. package/src/runtime/middleware/error-handler.ts +4 -3
  241. package/src/runtime/middleware/rate-limiter.ts +160 -0
  242. package/src/runtime/middleware/request-logger.ts +71 -0
  243. package/src/runtime/middleware/twilio-validation.ts +7 -6
  244. package/src/runtime/pending-interactions.ts +12 -0
  245. package/src/runtime/routes/access-request-decision.ts +215 -0
  246. package/src/runtime/routes/app-routes.ts +25 -18
  247. package/src/runtime/routes/approval-routes.ts +18 -47
  248. package/src/runtime/routes/attachment-routes.ts +15 -41
  249. package/src/runtime/routes/call-routes.ts +20 -20
  250. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  251. package/src/runtime/routes/contact-routes.ts +4 -9
  252. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  253. package/src/runtime/routes/conversation-routes.ts +26 -57
  254. package/src/runtime/routes/debug-routes.ts +71 -0
  255. package/src/runtime/routes/events-routes.ts +3 -2
  256. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  257. package/src/runtime/routes/identity-routes.ts +14 -10
  258. package/src/runtime/routes/inbound-conversation.ts +3 -2
  259. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  260. package/src/runtime/routes/ingress-routes.ts +174 -0
  261. package/src/runtime/routes/integration-routes.ts +78 -16
  262. package/src/runtime/routes/pairing-routes.ts +11 -10
  263. package/src/runtime/routes/secret-routes.ts +10 -18
  264. package/src/runtime/verification-rate-limiter.ts +83 -0
  265. package/src/schedule/schedule-store.ts +13 -1
  266. package/src/schedule/scheduler.ts +1 -1
  267. package/src/security/secret-ingress.ts +5 -2
  268. package/src/security/secret-scanner.ts +72 -6
  269. package/src/subagent/manager.ts +6 -4
  270. package/src/swarm/plan-validator.ts +4 -1
  271. package/src/tasks/task-runner.ts +3 -1
  272. package/src/tools/browser/api-map.ts +9 -6
  273. package/src/tools/calls/call-start.ts +20 -0
  274. package/src/tools/executor.ts +50 -568
  275. package/src/tools/permission-checker.ts +272 -0
  276. package/src/tools/registry.ts +14 -6
  277. package/src/tools/reminder/reminder-store.ts +7 -7
  278. package/src/tools/reminder/reminder.ts +6 -3
  279. package/src/tools/secret-detection-handler.ts +301 -0
  280. package/src/tools/subagent/message.ts +1 -1
  281. package/src/tools/system/voice-config.ts +62 -0
  282. package/src/tools/tasks/index.ts +3 -3
  283. package/src/tools/tasks/work-item-list.ts +3 -3
  284. package/src/tools/tasks/work-item-update.ts +4 -5
  285. package/src/tools/tool-approval-handler.ts +192 -0
  286. package/src/tools/tool-manifest.ts +2 -0
  287. package/src/watcher/watcher-store.ts +9 -9
  288. package/src/work-items/work-item-runner.ts +9 -6
  289. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  290. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
package/ARCHITECTURE.md CHANGED
@@ -65,6 +65,57 @@ The policy is implemented in `src/tools/guardian-control-plane-policy.ts`, which
65
65
 
66
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
67
 
68
+ ### Guardian Action Timeout-to-Follow-Up Lifecycle
69
+
70
+ When a voice call's ASK_GUARDIAN consultation times out before the guardian responds, the system enters a follow-up lifecycle that allows the guardian to act on their late answer after the call has moved on. The entire flow uses LLM-generated copy (never hardcoded user-facing strings) to maintain a natural, conversational tone across voice and text channels.
71
+
72
+ **Lifecycle stages:**
73
+
74
+ ```
75
+ ASK_GUARDIAN fires on call
76
+ |
77
+ v
78
+ [pending] -- guardian answers in time --> [answered] (normal flow)
79
+ |
80
+ | (timeout expires)
81
+ v
82
+ [expired, followup_state=none]
83
+ |
84
+ | (guardian replies late)
85
+ v
86
+ [expired, followup_state=awaiting_guardian_choice]
87
+ |
88
+ | (conversation engine classifies intent)
89
+ v
90
+ call_back / message_back / decline
91
+ | |
92
+ v v
93
+ [dispatching] [declined] (terminal)
94
+ |
95
+ | (executor runs action)
96
+ v
97
+ [completed] or [failed] (terminal)
98
+ ```
99
+
100
+ **Generated messaging requirement:** All user-facing copy in the guardian timeout/follow-up path is generated through the `guardian-action-message-composer.ts` composition system, which uses a 2-tier priority chain: (1) daemon-injected LLM generator for natural, varied text; (2) deterministic fallback templates for reliability. No hardcoded user-facing strings exist in the flow files (call-controller, inbound-message-handler, session-process) outside of internal log messages and LLM-instruction prompts. A guard test (`guardian-action-no-hardcoded-copy.test.ts`) enforces this invariant.
101
+
102
+ **Callback/message-back branch:** When the conversation engine classifies the guardian's intent as `call_back`, the executor starts an outbound call to the counterparty with context about the guardian's answer. When classified as `message_back`, the executor sends an SMS to the counterparty via the gateway's `/deliver/sms` endpoint. The counterparty phone number is resolved from the original call session by call direction (inbound: `fromNumber`; outbound: `toNumber`).
103
+
104
+ **Key source files:**
105
+
106
+ | File | Purpose |
107
+ |------|---------|
108
+ | `src/memory/guardian-action-store.ts` | Follow-up state machine with atomic transitions (`startFollowupFromExpiredRequest`, `progressFollowupState`, `finalizeFollowup`) and query helpers for pending/expired/follow-up deliveries |
109
+ | `src/runtime/guardian-action-message-composer.ts` | 2-tier text generation: daemon-injected LLM generator with deterministic fallback templates. Covers all scenarios from timeout acknowledgment through follow-up completion |
110
+ | `src/runtime/guardian-action-conversation-turn.ts` | Follow-up decision engine: classifies guardian replies into `call_back`, `message_back`, `decline`, or `keep_pending` dispositions using LLM tool calling |
111
+ | `src/runtime/guardian-action-followup-executor.ts` | Action dispatch: resolves counterparty from call session, executes `message_back` (SMS via gateway) or `call_back` (outbound call via `startCall`), finalizes follow-up state |
112
+ | `src/daemon/guardian-action-generators.ts` | Daemon-injected generator factories: `createGuardianActionCopyGenerator` (latency-optimized text rewriting) and `createGuardianFollowUpConversationGenerator` (tool-calling intent classification) |
113
+ | `src/calls/call-controller.ts` | Voice timeout handling: marks requests as timed out, sends expiry notices, injects `[GUARDIAN_TIMEOUT]` instruction for generated voice response |
114
+ | `src/runtime/routes/inbound-message-handler.ts` | Late reply interception for Telegram/SMS channels: matches late answers to expired requests, routes follow-up conversation turns, dispatches actions |
115
+ | `src/daemon/session-process.ts` | Late reply interception for mac/IPC channel: same logic as inbound-message-handler but using conversation-ID-based delivery lookup |
116
+ | `src/calls/guardian-action-sweep.ts` | Periodic sweep for stale pending requests; sends expiry notices to guardian destinations |
117
+ | `src/memory/migrations/030-guardian-action-followup.ts` | Schema migration adding follow-up columns (`followup_state`, `late_answer_text`, `late_answered_at`, `followup_action`, `followup_completed_at`) |
118
+
68
119
  ### SMS Channel (Twilio)
69
120
 
70
121
  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.
@@ -134,6 +185,97 @@ These can be set via environment variables or stored in the credential vault (ke
134
185
 
135
186
  **SMS Compliance & Admin**: The `twilio_config` IPC contract extends beyond credential and number management with compliance and admin actions: `sms_compliance_status` detects toll-free vs local number type and fetches verification status; `sms_submit_tollfree_verification`, `sms_update_tollfree_verification`, and `sms_delete_tollfree_verification` manage the Twilio toll-free verification lifecycle; `release_number` removes a phone number from the Twilio account and clears all local references. All compliance actions validate required fields and Twilio enum values before calling the API.
136
187
 
188
+ ### Slack Channel (Socket Mode)
189
+
190
+ The Slack channel provides text-based messaging via Slack's Socket Mode API. Unlike other channels that use HTTP webhooks, Slack uses a persistent WebSocket connection managed by the gateway — no public ingress URL is required. The assistant-side manages credential storage and validation through HTTP config endpoints.
191
+
192
+ **Control-plane endpoints** (`/v1/integrations/slack/channel/config`):
193
+
194
+ | Endpoint | Method | Description |
195
+ |----------|--------|-------------|
196
+ | `/v1/integrations/slack/channel/config` | GET | Returns current config status: `hasBotToken`, `hasAppToken`, `connected`, plus workspace metadata (`teamId`, `teamName`, `botUserId`, `botUsername`) |
197
+ | `/v1/integrations/slack/channel/config` | POST | Validates and stores credentials. Body: `{ botToken?: string, appToken?: string }` |
198
+ | `/v1/integrations/slack/channel/config` | DELETE | Clears all Slack channel credentials from secure storage and credential metadata |
199
+
200
+ All endpoints are bearer-authenticated via the runtime HTTP token (`~/.vellum/http-token`).
201
+
202
+ **Credential storage pattern:**
203
+
204
+ Both tokens are stored in the secure key store (macOS Keychain with encrypted file fallback):
205
+
206
+ | Secure key | Content |
207
+ |-----------|---------|
208
+ | `credential:slack_channel:bot_token` | Slack bot token (used for `chat.postMessage` and `auth.test`) |
209
+ | `credential:slack_channel:app_token` | Slack app token (`xapp-...`, used for Socket Mode `apps.connections.open`) |
210
+
211
+ Workspace metadata (team ID, team name, bot user ID, bot username) is stored as JSON in the credential metadata store under `('slack_channel', 'bot_token')`.
212
+
213
+ **Token validation via `auth.test`:**
214
+
215
+ When a bot token is provided via `POST /v1/integrations/slack/channel/config`, the handler calls `POST https://slack.com/api/auth.test` with the token before storing it. A successful response yields workspace metadata (`team_id`, `team`, `user_id`, `user`) that is persisted alongside the token. If `auth.test` fails, the token is rejected and not stored.
216
+
217
+ The app token is validated by format only — it must start with `xapp-`.
218
+
219
+ **Connection status:**
220
+
221
+ The `GET` endpoint reports `connected: true` only when both `hasBotToken` and `hasAppToken` are true. If only one token is stored, a `warning` field describes which token is missing.
222
+
223
+ **Key source files:**
224
+
225
+ | File | Purpose |
226
+ |------|---------|
227
+ | `src/daemon/handlers/config-slack-channel.ts` | Business logic for get/set/clear Slack channel config |
228
+ | `src/runtime/routes/integration-routes.ts` | HTTP route handlers for `/v1/integrations/slack/channel/config` |
229
+
230
+ ### Trusted Contact Access (Channel-Agnostic)
231
+
232
+ External users who are not the guardian can gain access to the assistant through a guardian-mediated verification flow. The flow is channel-agnostic — it works identically on Telegram, SMS, voice, and any future channel.
233
+
234
+ **Full design doc:** [`docs/trusted-contact-access.md`](docs/trusted-contact-access.md)
235
+
236
+ **Flow summary:**
237
+ 1. Unknown user messages the assistant on any channel.
238
+ 2. Ingress ACL (`inbound-message-handler.ts`) rejects the message and emits an `ingress.access_request` notification signal to the guardian.
239
+ 3. Guardian approves or denies via callback button or conversational intent (routed through `guardian-approval-interception.ts`).
240
+ 4. On approval, an identity-bound verification session with a 6-digit code is created (`access-request-decision.ts` → `channel-guardian-service.ts`).
241
+ 5. Guardian gives the code to the requester out-of-band.
242
+ 6. Requester enters the code; identity binding is verified, the challenge is consumed, and an active member record is created in `assistant_ingress_members`.
243
+ 7. All subsequent messages are accepted through the ingress ACL.
244
+
245
+ **Channel-agnostic design:** The entire flow operates on abstract `ChannelId` and `externalUserId`/`externalChatId` fields. Identity binding adapts per channel: Telegram uses chat IDs, SMS/voice use E.164 phone numbers, HTTP API uses caller-provided identity. No channel-specific branching exists in the trusted contact code paths.
246
+
247
+ **Lifecycle states:** `requested → pending_guardian → verification_pending → active | denied | expired`
248
+
249
+ **Notification signals:** The flow emits signals at each lifecycle transition via `emitNotificationSignal()`:
250
+ - `ingress.access_request` — non-member denied, guardian notified
251
+ - `ingress.trusted_contact.guardian_decision` — guardian approved or denied
252
+ - `ingress.trusted_contact.verification_sent` — code created and delivered
253
+ - `ingress.trusted_contact.activated` — requester verified, member active
254
+ - `ingress.trusted_contact.denied` — guardian explicitly denied
255
+
256
+ **HTTP API (for management):**
257
+
258
+ | Endpoint | Method | Description |
259
+ |----------|--------|-------------|
260
+ | `/v1/ingress/members` | GET | List trusted contacts (filterable by channel, status, policy) |
261
+ | `/v1/ingress/members` | POST | Upsert a member (add/update trusted contact) |
262
+ | `/v1/ingress/members/:id` | DELETE | Revoke a trusted contact |
263
+ | `/v1/ingress/members/:id/block` | POST | Block a member |
264
+
265
+ **Key source files:**
266
+
267
+ | File | Purpose |
268
+ |------|---------|
269
+ | `src/runtime/routes/inbound-message-handler.ts` | Ingress ACL, non-member rejection, verification code interception |
270
+ | `src/runtime/routes/access-request-decision.ts` | Guardian decision → verification session creation |
271
+ | `src/runtime/routes/guardian-approval-interception.ts` | Routes guardian decisions (button + conversational) to access request handler |
272
+ | `src/runtime/channel-guardian-service.ts` | Verification challenge lifecycle, identity binding, rate limiting |
273
+ | `src/runtime/routes/ingress-routes.ts` | HTTP API handlers for member/invite management |
274
+ | `src/runtime/ingress-service.ts` | Business logic for member CRUD |
275
+ | `src/memory/ingress-member-store.ts` | Member record persistence |
276
+ | `src/memory/channel-guardian-store.ts` | Approval request and verification challenge persistence |
277
+ | `src/config/vellum-skills/trusted-contacts/SKILL.md` | Skill teaching the assistant to manage contacts via HTTP API |
278
+
137
279
  ---
138
280
 
139
281
 
package/Dockerfile CHANGED
@@ -89,7 +89,7 @@ RUN echo 'Dir::State "/data/dpkg";' > /etc/apt/apt.conf.d/99data-dir && \
89
89
  ENV PATH="/data/usr/bin:/data/usr/sbin:${PATH}"
90
90
  ENV LD_LIBRARY_PATH="/data/usr/lib:/data/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
91
91
 
92
- USER assistant
92
+ USER root
93
93
 
94
94
  EXPOSE 3001
95
95
 
package/README.md CHANGED
@@ -262,7 +262,7 @@ The channel guardian service generates verification challenge instructions with
262
262
 
263
263
  ### Operator Notes
264
264
 
265
- - **Verify command formats:** The `/guardian_verify` command accepts both `/guardian_verify <code>` and `/guardian_verify@BotName <code>` (Telegram appends the bot username in group chats). The handler normalizes both formats automatically.
265
+ - **Verification input format:** Channel verification accepts a bare code reply only (6-digit numeric for identity-bound sessions; 64-char hex for unbound inbound/bootstrap compatibility).
266
266
  - **Rebind requirement:** Creating a new guardian challenge when a binding already exists requires `rebind: true` in the IPC request. Without it, the daemon returns `already_bound`. This prevents accidental guardian replacement.
267
267
  - **Takeover prevention:** Verification is rejected when an active binding exists for a different external user. Same-user re-verification is allowed.
268
268
 
@@ -274,9 +274,9 @@ This section documents the end-to-end flow from guardian verification through in
274
274
 
275
275
  Guardian verification establishes a cryptographic trust binding between a human identity and an `(assistantId, channel)` pair. The flow is:
276
276
 
277
- 1. **Challenge creation** — The owner initiates verification from the desktop UI, which sends a `guardian_verify` IPC message (action: `create_challenge`) to the daemon. The daemon generates a random secret (32-byte hex for text channels, 6-digit numeric for voice), hashes it with SHA-256, stores the hash with a 10-minute TTL, and returns the raw secret to the desktop.
278
- 2. **Secret sharing** — The desktop displays the secret and instructs the owner to send `/guardian_verify <secret>` to the bot on the target channel (e.g., Telegram).
279
- 3. **Verification** — When the message arrives at `/channels/inbound`, the handler intercepts the `/guardian_verify` prefix before normal message processing. It hashes the provided secret, looks up a matching pending challenge, validates expiry, and consumes the challenge (preventing replay).
277
+ 1. **Challenge creation** — The owner initiates verification from the desktop UI, which sends a guardian-verification IPC message (`create_challenge` action) to the daemon. The daemon generates a random secret (32-byte hex for unbound inbound/bootstrap sessions, 6-digit numeric for identity-bound sessions), hashes it with SHA-256, stores the hash with a 10-minute TTL, and returns the raw secret to the desktop.
278
+ 2. **Code sharing** — The desktop displays the code and instructs the owner to reply with that code in the target channel conversation (e.g., Telegram or SMS).
279
+ 3. **Verification** — When the message arrives at `/channels/inbound`, the handler intercepts valid verification-code replies before normal message processing. It hashes the provided code, looks up a matching pending challenge, validates expiry, and consumes the challenge (preventing replay).
280
280
  4. **Binding** — On success, any existing active binding for the `(assistantId, channel)` pair is revoked, and a new guardian binding is created with the verifier's `externalUserId` and `chatId`. The verifier receives a confirmation message.
281
281
 
282
282
  Rate limiting protects against brute-force attempts: 5 invalid attempts within 15 minutes trigger a 30-minute lockout per `(assistantId, channel, actor)` tuple. The same generic failure message is returned for both invalid codes and rate-limited attempts to avoid leaking state.
@@ -317,7 +317,7 @@ Guardian verification and ingress membership are complementary but independent s
317
317
  |------|---------|
318
318
  | `src/runtime/channel-guardian-service.ts` | Challenge lifecycle: `createVerificationChallenge`, `validateAndConsumeChallenge`, `getGuardianBinding`, `isGuardian` |
319
319
  | `src/runtime/guardian-context-resolver.ts` | Actor role classification: guardian / non-guardian / unverified_channel |
320
- | `src/runtime/routes/inbound-message-handler.ts` | Ingress ACL enforcement, `/guardian_verify` command intercept, escalation creation |
320
+ | `src/runtime/routes/inbound-message-handler.ts` | Ingress ACL enforcement, verification-code intercept, escalation creation |
321
321
  | `src/memory/ingress-member-store.ts` | Member CRUD: `findMember`, `upsertMember`, `revokeMember`, `blockMember` |
322
322
  | `src/memory/ingress-invite-store.ts` | Invite lifecycle: `createInvite`, `redeemInvite` (atomically creates member record) |
323
323
  | `src/memory/channel-guardian-store.ts` | Persistence for guardian bindings, verification challenges, and approval requests |
@@ -0,0 +1,252 @@
1
+ # HTTP Token Refresh Protocol
2
+
3
+ Design for how the daemon notifies clients of bearer token rotation and how clients recover from stale tokens.
4
+
5
+ ## Current State
6
+
7
+ The daemon's HTTP bearer token is generated at startup and persisted to `~/.vellum/http-token` (mode 0600). Clients read this file at connection time:
8
+
9
+ - **macOS (local)**: Reads `~/.vellum/http-token` from disk via `resolveHttpTokenPath()` / `readHttpToken()`. Has direct filesystem access to the token file.
10
+ - **iOS (remote)**: Receives the bearer token during the QR-code pairing flow. The token is stored in the iOS Keychain and used for all subsequent HTTP/SSE requests.
11
+ - **Chrome extension**: User manually pastes the token from `~/.vellum/http-token` into the extension popup.
12
+
13
+ Token regeneration today (macOS Settings > Connect > Regenerate Bearer Token):
14
+ 1. macOS client writes a new random token to `~/.vellum/http-token`.
15
+ 2. macOS client kills the daemon process.
16
+ 3. The health monitor restarts the daemon, which reads the new token from disk.
17
+ 4. macOS client re-reads the token from disk on next health check.
18
+ 5. **iOS clients are broken** -- they still hold the old token and get 401s. The only recovery is to re-pair via QR code.
19
+
20
+ ## Problem
21
+
22
+ When the bearer token is rotated (manually or programmatically), remote clients (iOS, Chrome extension) have no way to learn about the new token. They receive 401 responses and cannot recover without manual re-configuration.
23
+
24
+ ## Design
25
+
26
+ ### 1. SSE Token Rotation Event
27
+
28
+ When the daemon detects that its bearer token has changed, it emits a `token_rotated` SSE event to all connected clients **before** the old token is invalidated. This gives clients a window to capture the new token and seamlessly reconnect.
29
+
30
+ **Event format** (delivered as an `AssistantEvent` envelope wrapping a new `ServerMessage` type):
31
+
32
+ ```typescript
33
+ // New ServerMessage variant
34
+ interface TokenRotatedMessage {
35
+ type: 'token_rotated';
36
+ newToken: string; // The replacement bearer token
37
+ expiresOldAt: number; // Unix timestamp (ms) -- old token stops working after this
38
+ }
39
+ ```
40
+
41
+ **Grace period**: The daemon accepts both old and new tokens for a configurable grace window (default: 30 seconds) after emitting the event. This gives slow clients time to process the event and switch tokens. After the grace period, only the new token is valid.
42
+
43
+ **Sequence diagram (routine rotation)**:
44
+
45
+ ```mermaid
46
+ sequenceDiagram
47
+ participant Trigger as Rotation Trigger
48
+ participant Daemon as Daemon
49
+ participant SSE as SSE Stream
50
+ participant Client as iOS / Chrome Client
51
+
52
+ Trigger->>Daemon: Rotate token (manual, API, periodic)
53
+ Daemon->>Daemon: Generate new token, write to ~/.vellum/http-token
54
+ Daemon->>Daemon: Enter grace period (accept old + new)
55
+ Daemon->>SSE: Emit token_rotated {newToken, expiresOldAt}
56
+ SSE->>Client: token_rotated event
57
+ Client->>Client: Store new token (Keychain / localStorage)
58
+ Client->>Client: Update Authorization header
59
+ Client->>Daemon: Reconnect SSE with new token
60
+ Note over Daemon: Grace period expires
61
+ Daemon->>Daemon: Reject old token (401)
62
+ ```
63
+
64
+ **Sequence diagram (revocation rotation)**:
65
+
66
+ ```mermaid
67
+ sequenceDiagram
68
+ participant Trigger as Security Event
69
+ participant Daemon as Daemon
70
+ participant SSE as SSE Stream
71
+ participant Client as iOS / Chrome Client
72
+
73
+ Trigger->>Daemon: Rotate token (revoke: true)
74
+ Daemon->>Daemon: Generate new token, immediately invalidate old token
75
+ Daemon->>SSE: Terminate all old-token connections
76
+ Daemon->>Daemon: Write new token to ~/.vellum/http-token
77
+ Note over SSE: No token_rotated event emitted
78
+ Client->>Daemon: Next request with old token → 401
79
+ Client->>Client: Trigger fallback recovery (re-pair / re-read / re-paste)
80
+ ```
81
+
82
+ ### 2. Client 401 Recovery (Stale Token Detection)
83
+
84
+ If a client misses the SSE event (network partition, app backgrounded, SSE disconnected at rotation time), it needs a fallback recovery mechanism.
85
+
86
+ **401 response handling**:
87
+
88
+ When a client receives a `401 Unauthorized` response:
89
+
90
+ 1. **macOS (local)**: Re-reads `~/.vellum/http-token` from disk. If the token differs from the in-memory token, updates and retries. This already works implicitly since macOS re-reads the token on most HTTP calls via `resolveLocalDaemonHTTPEndpoint()`.
91
+
92
+ 2. **iOS (remote)**: Cannot read the token file. Must re-pair via QR code. The 401 response triggers the client to surface a "Session expired -- re-pair required" UI prompt. This is the expected behavior when the SSE notification is missed.
93
+
94
+ 3. **Chrome extension**: Surfaces an error message directing the user to paste the new token.
95
+
96
+ **Retry logic**: Clients should retry at most once after a 401 before surfacing the error UI. This prevents retry storms during legitimate auth failures (wrong token, not just stale).
97
+
98
+ ### 3. Token Rotation Triggers
99
+
100
+ The token can be rotated via:
101
+
102
+ | Trigger | Description | Current | Proposed | Mode |
103
+ |---------|-------------|---------|----------|------|
104
+ | Manual (macOS Settings) | User clicks "Regenerate Bearer Token" | Yes (kills daemon) | Graceful rotation via daemon API | Routine |
105
+ | API endpoint | `POST /v1/auth/rotate-token` | No | New endpoint | Routine (default) or Revocation (`revoke: true`) |
106
+ | Periodic rotation | Automatic rotation on a configurable schedule | No | Future consideration | Routine |
107
+ | Security event | Forced rotation after suspicious activity | No | Future consideration | Revocation |
108
+
109
+ **`POST /v1/auth/rotate-token`** (new endpoint):
110
+
111
+ Allows programmatic token rotation without restarting the daemon.
112
+
113
+ Request body (optional): `{ "revoke": boolean }` (default: `false`).
114
+
115
+ **Routine mode** (`revoke: false`, default):
116
+ 1. Generates a new random token.
117
+ 2. Writes it to `~/.vellum/http-token`.
118
+ 3. Emits the `token_rotated` SSE event with grace period.
119
+ 4. Starts accepting both tokens during the grace period.
120
+ 5. After grace period, rejects the old token.
121
+
122
+ **Revocation mode** (`revoke: true`):
123
+ 1. Generates a new random token.
124
+ 2. Immediately invalidates the old token in memory (no grace period).
125
+ 3. Terminates all SSE connections authenticated with the old token.
126
+ 4. Writes the new token to `~/.vellum/http-token`.
127
+ 5. Does **not** emit `token_rotated` -- the new token is never sent to old-token sessions.
128
+ 6. Clients must recover via their platform-specific fallback (re-read from disk, re-pair, or re-paste).
129
+
130
+ This eliminates the current "kill and restart" approach for token rotation.
131
+
132
+ ### 4. Daemon-Side Implementation
133
+
134
+ **Grace period token validation**: During routine rotation, `verifyBearerToken()` accepts either the old or new token. The `RuntimeHttpServer` holds both tokens:
135
+
136
+ ```typescript
137
+ // Conceptual extension to RuntimeHttpServer
138
+ private currentToken: string;
139
+ private previousToken: string | null = null;
140
+ private graceDeadline: number | null = null;
141
+
142
+ // Modified auth check
143
+ private isValidToken(provided: string): boolean {
144
+ if (verifyBearerToken(provided, this.currentToken)) return true;
145
+ if (this.previousToken && this.graceDeadline && Date.now() < this.graceDeadline) {
146
+ return verifyBearerToken(provided, this.previousToken);
147
+ }
148
+ return false;
149
+ }
150
+
151
+ // Rotation has two ordering strategies depending on revoke mode:
152
+ // - Revocation: invalidate in-memory FIRST (security-critical), then persist.
153
+ // A disk write failure must never leave a compromised token valid.
154
+ // - Routine: persist to disk FIRST, then update in-memory state.
155
+ // A disk write failure aborts rotation — clients keep the old token
156
+ // rather than being locked out by an in-memory-only switch.
157
+ private rotateToken(revoke: boolean): string {
158
+ const newToken = generateToken();
159
+
160
+ if (revoke) {
161
+ // Revocation: invalidate the compromised token immediately.
162
+ // Even if the disk write below fails, the old token is gone from memory.
163
+ this.currentToken = newToken;
164
+ this.previousToken = null;
165
+ this.graceDeadline = null;
166
+ this.terminateOldTokenSSEConnections();
167
+ writeTokenToDisk(newToken);
168
+ } else {
169
+ // Routine: persist to disk first — if this throws, auth state is untouched
170
+ writeTokenToDisk(newToken);
171
+ this.previousToken = this.currentToken;
172
+ this.currentToken = newToken;
173
+ this.graceDeadline = Date.now() + GRACE_PERIOD_MS;
174
+ this.emitTokenRotatedEvent(newToken, this.graceDeadline);
175
+ }
176
+ return newToken;
177
+ }
178
+ ```
179
+
180
+ **SSE event emission** (routine rotation only): The `token_rotated` event is published to `assistantEventHub` as a `ServerMessage`, reaching all connected SSE subscribers across all conversations. This event is never emitted during revocation rotations.
181
+
182
+ ### 5. iOS Client Implementation
183
+
184
+ **SSE event handler** (in `HTTPTransport`):
185
+
186
+ ```swift
187
+ // In parseSSEData, handle the new message type
188
+ case .tokenRotated(let msg):
189
+ // Persist the new token to Keychain
190
+ DaemonConfigStore.shared.updateBearerToken(msg.newToken)
191
+ // Update in-memory token
192
+ self.bearerToken = msg.newToken
193
+ // Reconnect SSE with the new token
194
+ self.stopSSE()
195
+ self.startSSE()
196
+ ```
197
+
198
+ **401 response handler**:
199
+
200
+ ```swift
201
+ // In any HTTP request that receives 401
202
+ if http.statusCode == 401 {
203
+ // Token is stale and we missed the rotation event
204
+ // Surface re-pairing UI
205
+ onMessage?(.sessionError(SessionErrorMessage(
206
+ sessionId: sessionId,
207
+ code: .authenticationRequired,
208
+ userMessage: "Session expired. Please re-pair your device.",
209
+ retryable: false
210
+ )))
211
+ }
212
+ ```
213
+
214
+ ### 6. Security Considerations
215
+
216
+ - **Rotation modes**: Token rotation has two distinct modes with different security requirements:
217
+
218
+ 1. **Routine rotation** (manual refresh, periodic schedule): The old token is not compromised -- the goal is seamless credential rollover. The SSE `token_rotated` event delivers the new token to connected clients, and the grace period allows them to transition. This is safe because the SSE channel is authenticated, and any session holding the old token is a legitimate client.
219
+
220
+ 2. **Revocation rotation** (security event, suspected compromise): The old token may be in the hands of an attacker. In this mode, the daemon **must not** push the replacement token to old-token SSE sessions -- doing so would hand the new credential to the very sessions being revoked. Instead:
221
+ - The daemon immediately invalidates the old token (no grace period).
222
+ - All SSE connections authenticated with the old token are terminated.
223
+ - The `POST /v1/auth/rotate-token` endpoint accepts an optional `revoke: true` flag to select this mode.
224
+ - Legitimate clients recover via their fallback path: macOS re-reads `~/.vellum/http-token` from disk; iOS prompts for re-pairing; Chrome extension prompts for a new token.
225
+
226
+ The `token_rotated` SSE event is only emitted during routine rotations. The rotation trigger determines the mode.
227
+
228
+ - **Grace period length**: 30 seconds is long enough for clients to process the event but short enough to limit the window where both tokens are valid. Only applies to routine rotations.
229
+ - **No token in logs**: The `token_rotated` event payload must be excluded from any server-side event logging. Use the existing log-redaction patterns.
230
+ - **Constant-time comparison**: The existing `verifyBearerToken()` using `timingSafeEqual` continues to be used for both old and new token checks during the grace period.
231
+
232
+ ### 7. Migration Path
233
+
234
+ This design is additive and backward-compatible:
235
+
236
+ 1. **Phase 1**: Add `POST /v1/auth/rotate-token` endpoint and `token_rotated` SSE event to the daemon. Update macOS Settings to call the API endpoint instead of kill-and-restart.
237
+ 2. **Phase 2**: Add `token_rotated` handler to `HTTPTransport.swift` (shared between macOS and iOS). Add 401 retry-once logic.
238
+ 3. **Phase 3** (future): Add periodic rotation and security-event-triggered rotation.
239
+
240
+ Clients that do not understand the `token_rotated` event will simply ignore it (SSE events with unknown types are safe to skip). They will eventually get 401s after the grace period and fall back to their existing recovery path (re-read from disk for macOS, re-pair for iOS).
241
+
242
+ ## Key Files
243
+
244
+ | File | Role |
245
+ |------|------|
246
+ | `assistant/src/runtime/http-server.ts` | Auth check, grace period logic, rotation endpoint |
247
+ | `assistant/src/runtime/middleware/auth.ts` | `verifyBearerToken()` -- constant-time token comparison |
248
+ | `assistant/src/runtime/assistant-event.ts` | `AssistantEvent` envelope, SSE framing |
249
+ | `assistant/src/daemon/lifecycle.ts` | Token generation and persistence at startup |
250
+ | `clients/shared/IPC/HTTPDaemonClient.swift` | `HTTPTransport` -- SSE stream, 401 handling |
251
+ | `clients/shared/IPC/DaemonClient.swift` | `readHttpToken()`, `resolveHttpTokenPath()` |
252
+ | `clients/macos/.../SettingsConnectTab.swift` | Manual token regeneration UI |
@@ -366,14 +366,14 @@ The Anthropic provider places `cache_control: { type: 'ephemeral' }` on the **la
366
366
 
367
367
  ## Temporal Context Injection — Date Grounding
368
368
 
369
- The session injects a `<temporal_context>` block into every user message at runtime, giving the model awareness of the current date, timezone, upcoming weekend/work week windows, and a 14-day horizon of labelled future dates. This enables reliable reasoning about future dates (e.g. "plan a trip for next weekend") without persisting volatile temporal data in conversation history.
369
+ The session injects a `<temporal_context>` block into every user message at runtime, giving the model awareness of the current date, current local time, current UTC time, timezone source metadata, upcoming weekend/work week windows, and a 14-day horizon of labelled future dates. This enables reliable reasoning about future dates (e.g. "plan a trip for next weekend") without persisting volatile temporal data in conversation history.
370
370
 
371
371
  ### Per-turn flow
372
372
 
373
373
  ```mermaid
374
374
  graph TB
375
375
  subgraph "Per-Turn Flow"
376
- BUILD["buildTemporalContext(timeZone)<br/>→ compact XML block"]
376
+ BUILD["buildTemporalContext(timeZone, hostTimeZone, userTimeZone)<br/>→ compact XML block"]
377
377
  INJECT["applyRuntimeInjections<br/>prepend temporal block<br/>to user message"]
378
378
  AGENT["AgentLoop.run(runMessages)"]
379
379
  STRIP["stripTemporalContext<br/>remove block from persisted history"]
@@ -387,7 +387,9 @@ graph TB
387
387
  ### Key design decisions
388
388
 
389
389
  - **Fresh each turn**: `buildTemporalContext()` is called at the start of every agent loop invocation, ensuring the model always sees the current date even in long-running conversations.
390
- - **Timezone-aware**: Uses `Intl.DateTimeFormat` APIs for DST-safe date arithmetic. The host timezone (`Intl.DateTimeFormat().resolvedOptions().timeZone`) is used by default.
390
+ - **Clock source invariant**: Absolute time (`now`) always comes from the assistant host clock (`Date.now()`), never from channel/client clocks.
391
+ - **Timezone precedence**: If `ui.userTimezone` is configured, temporal context uses it for local-date interpretation. Otherwise it falls back to dynamic profile memory, then assistant host timezone.
392
+ - **Timezone-aware**: Uses `Intl.DateTimeFormat` APIs for DST-safe date arithmetic and timezone validation/canonicalization.
391
393
  - **Bounded output**: Hard-capped at 1500 characters and 14 horizon entries to prevent prompt bloat.
392
394
  - **Runtime-only**: The injected `<temporal_context>` block is stripped from `this.messages` after the agent loop completes via `stripTemporalContext`. It never persists in conversation history.
393
395
  - **Specific strip prefix**: The strip function matches the exact injected prefix (`<temporal_context>\nToday:`) to avoid accidentally removing user-authored text that starts with `<temporal_context>`.
@@ -512,4 +514,3 @@ graph TB
512
514
  | `assistant/src/config/schema.ts` | `WorkspaceGitConfigSchema`: timeout, backoff, and enrichment queue configuration |
513
515
 
514
516
  ---
515
-
@@ -91,7 +91,7 @@ The `enforceRoutingIntent()` step runs after the LLM produces a channel selectio
91
91
  | Intent | Enforcement Rule |
92
92
  |--------|-----------------|
93
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. |
94
+ | `multi_channel` | If the LLM selected < 2 channels and 2+ are connected, expand to at least two connected channels. |
95
95
  | `all_channels` | Replace the LLM's selection with all connected channels. |
96
96
 
97
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.
@@ -189,9 +189,9 @@ graph TD
189
189
 
190
190
  **Data tables:** `watchers` (config, watermark, status, error tracking) and `watcher_events` (detected events, dedup on `(watcher_id, external_id)`, disposition tracking).
191
191
 
192
- ## Task Queue — Queued Task Execution and Review
192
+ ## Task Queue — Conversation-Managed Task Execution
193
193
 
194
- The Task Queue builds on top of the existing Tasks system to provide an ordered execution pipeline with human-in-the-loop review.
194
+ The Task Queue provides an ordered execution pipeline with human-in-the-loop review. Task management happens entirely through conversation — the user creates, updates, runs, and reviews tasks by talking to the assistant. There is no standalone Tasks UI window.
195
195
 
196
196
  ### Terminology
197
197
 
@@ -258,19 +258,6 @@ flowchart TD
258
258
  DB[(SQLite)]
259
259
  end
260
260
 
261
- subgraph "Daemon IPC Handlers"
262
- HC[handleWorkItemCreate]
263
- HU[handleWorkItemUpdate]
264
- HCo[handleWorkItemComplete]
265
- HR[handleWorkItemRunTask]
266
- BC[tasks_changed broadcast]
267
- end
268
-
269
- subgraph "macOS Client"
270
- TW[TasksWindowView]
271
- DC[DaemonClient]
272
- end
273
-
274
261
  TLA -->|"if_exists check"| DUPE
275
262
  DUPE -->|"no match"| WIS
276
263
  DUPE -->|"match found → reuse/update"| TLU
@@ -278,81 +265,10 @@ flowchart TD
278
265
  RWI --> WIS
279
266
  TLS --> WIS
280
267
  WIS --> DB
281
-
282
- HC --> WIS
283
- HU --> WIS
284
- HCo --> WIS
285
- HR --> WIS
286
- HC --> BC
287
- HU --> BC
288
- HCo --> BC
289
- HR --> BC
290
-
291
- BC -->|"via socket"| DC
292
- DC -->|"onTasksChanged"| TW
293
- TW -->|"debounced refetch (300ms)"| DC
294
268
  ```
295
269
 
296
270
  **Key behaviors:**
297
271
 
272
+ - **Conversation-first management** — All task operations (create, update, run, review, delete) are performed through natural language conversation with the assistant, which invokes the model tools (`task_list_add`, `task_list_update`, `task_list_show`) on the user's behalf.
298
273
  - **`task_list_update`** uses `resolveWorkItem` to find the target work item by work item ID, task ID, or title (case-insensitive exact match). When multiple items match by task ID or title, the resolver applies a deterministic tie-break (lowest priority tier, then earliest `createdAt`).
299
274
  - **`task_list_add`** has duplicate prevention via the `if_exists` parameter (default: `reuse_existing`). Before creating, it calls `findActiveWorkItemsByTitle` to check for active items with the same title. If a match is found, the tool either returns the existing item (`reuse_existing`), updates it in place (`update_existing`), or proceeds to create a duplicate (`create_duplicate`).
300
- - **All daemon work-item handlers** (`handleWorkItemCreate`, `handleWorkItemUpdate`, `handleWorkItemComplete`, `handleWorkItemRunTask`) emit a `tasks_changed` broadcast after mutations via `ctx.broadcast({ type: 'tasks_changed' })`. They also emit the more specific `work_item_status_changed` with the affected item's current state.
301
- - **The macOS Tasks window** (`TasksWindowView`) subscribes to both `tasks_changed` and `work_item_status_changed` callbacks on `DaemonClient`. Both trigger a debounced refetch (300ms) so rapid successive mutations coalesce into a single re-fetch.
302
-
303
- ### IPC Messages
304
-
305
- **Client → Server:**
306
-
307
- | Message | Purpose |
308
- |---------|---------|
309
- | `work_items_list` | List work items, filterable by status |
310
- | `work_item_get` | Fetch a single work item with full details |
311
- | `work_item_create` | Create a new work item pointing to a Task |
312
- | `work_item_update` | Update title, notes, priority, or sort order |
313
- | `work_item_complete` | Mark an item as `done` after review |
314
- | `work_item_run_task` | Trigger execution of a queued work item |
315
- | `work_item_delete` | Delete a work item from the queue |
316
-
317
- **Server → Client (push):**
318
-
319
- | Message | Purpose |
320
- |---------|---------|
321
- | `work_item_status_changed` | Notify the client when a work item transitions state (includes item snapshot) |
322
- | `tasks_changed` | Lightweight broadcast after any work-item mutation; triggers client-side refetch |
323
-
324
- ### Run-Button State Machine
325
-
326
- When the user clicks "Run" on a queued work item, the button follows a deterministic state machine:
327
-
328
- ```
329
- idle (visible) → in-flight (hidden) → success/failure → re-enabled (via refetch)
330
- ```
331
-
332
- **Sequence:**
333
-
334
- 1. **Idle** — The run button is visible only when `item.status == "queued"`. The `TasksWindowRow` renders it conditionally based on the `WorkItemStatus` enum.
335
- 2. **In-flight** — The client sends `work_item_run_task` with the work item ID. The daemon validates the request, sets the item's status to `running`, and returns `work_item_run_task_response` with `success: true`. It then broadcasts `work_item_status_changed` and `tasks_changed`. The client's debounced refetch picks up the `running` status, which hides the run button and shows a spinner in the status column.
336
- 3. **Completion** — The daemon executes the task asynchronously. On success, the item transitions to `awaiting_review`; on failure, to `failed`. Both trigger another `work_item_status_changed` + `tasks_changed` broadcast, which the client refetches and renders accordingly (showing a "Reviewed" button for `awaiting_review`, or the run button again for `failed` to allow retry).
337
-
338
- **Error handling in `work_item_run_task_response`:**
339
-
340
- The response includes a typed `errorCode` field (`WorkItemRunTaskErrorCode`) so the client can deterministically decide what to do without parsing error strings:
341
-
342
- | `errorCode` | Meaning | Client behavior |
343
- |-------------|---------|-----------------|
344
- | `not_found` | Work item does not exist (deleted concurrently) | Refetch removes the stale row |
345
- | `already_running` | Item is already executing | No-op; status column already shows spinner |
346
- | `invalid_status` | Item is `done` or `archived` and cannot be run | Refetch updates the row to reflect terminal status |
347
- | `no_task` | The associated Task template was deleted | Refetch; row may show an error state |
348
-
349
- In all error cases, the subsequent `tasks_changed` broadcast triggers a refetch that brings the UI back to a consistent state, so the button is never stuck in a disabled/hidden state without a path to recovery.
350
-
351
- ### Delete Flow
352
-
353
- Deletion uses optimistic UI with rollback:
354
-
355
- 1. **Optimistic removal** — `TasksWindowViewModel.removeTask()` snapshots the current `items` array, then immediately removes the target item with animation.
356
- 2. **IPC request** — Sends `work_item_delete` with the item ID. The daemon looks up the item; if found, deletes it and responds with `work_item_delete_response { success: true }`, then broadcasts `tasks_changed`.
357
- 3. **Failure rollback** — If the send throws (socket error), the view model restores the snapshot with animation. If the daemon responds with `success: false` (item not found), the `onWorkItemDeleteResponse` callback triggers a full refetch to reconcile.
358
-