@vellumai/assistant 0.3.14 → 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 (295) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +2 -2
  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-control-plane-policy.test.ts +1 -3
  40. package/src/__tests__/guardian-outbound-http.test.ts +202 -10
  41. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  42. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  43. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  44. package/src/__tests__/hooks-runner.test.ts +13 -4
  45. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  46. package/src/__tests__/intent-routing.test.ts +14 -0
  47. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  48. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  49. package/src/__tests__/memory-regressions.test.ts +16 -12
  50. package/src/__tests__/non-member-access-request.test.ts +282 -0
  51. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  52. package/src/__tests__/notification-routing-intent.test.ts +11 -2
  53. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  54. package/src/__tests__/recording-intent-fallback.test.ts +0 -1
  55. package/src/__tests__/recording-intent-handler.test.ts +6 -3
  56. package/src/__tests__/recording-intent.test.ts +3 -2
  57. package/src/__tests__/recording-state-machine.test.ts +337 -26
  58. package/src/__tests__/registry.test.ts +17 -8
  59. package/src/__tests__/relay-server.test.ts +105 -0
  60. package/src/__tests__/reminder.test.ts +13 -0
  61. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  63. package/src/__tests__/server-history-render.test.ts +8 -8
  64. package/src/__tests__/session-agent-loop.test.ts +1 -0
  65. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  66. package/src/__tests__/session-skill-tools.test.ts +1 -0
  67. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  68. package/src/__tests__/slack-channel-config.test.ts +230 -0
  69. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  70. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  71. package/src/__tests__/system-prompt.test.ts +43 -0
  72. package/src/__tests__/task-management-tools.test.ts +3 -3
  73. package/src/__tests__/task-tools.test.ts +3 -3
  74. package/src/__tests__/trust-store.test.ts +17 -1
  75. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  76. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  77. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  78. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  79. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  80. package/src/__tests__/update-bulletin.test.ts +260 -0
  81. package/src/__tests__/update-template-contract.test.ts +29 -0
  82. package/src/agent/loop.ts +2 -2
  83. package/src/amazon/client.ts +2 -3
  84. package/src/calls/call-controller.ts +115 -34
  85. package/src/calls/call-conversation-messages.ts +2 -2
  86. package/src/calls/call-domain.ts +10 -3
  87. package/src/calls/call-pointer-messages.ts +17 -5
  88. package/src/calls/guardian-action-sweep.ts +77 -36
  89. package/src/calls/relay-server.ts +51 -12
  90. package/src/calls/twilio-routes.ts +3 -1
  91. package/src/calls/types.ts +1 -1
  92. package/src/calls/voice-session-bridge.ts +4 -4
  93. package/src/cli/core-commands.ts +3 -3
  94. package/src/cli/map.ts +8 -5
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  96. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  97. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  98. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  99. package/src/config/computer-use-prompt.ts +1 -0
  100. package/src/config/core-schema.ts +16 -0
  101. package/src/config/env-registry.ts +1 -0
  102. package/src/config/env.ts +16 -1
  103. package/src/config/memory-schema.ts +5 -0
  104. package/src/config/schema.ts +4 -0
  105. package/src/config/system-prompt.ts +69 -2
  106. package/src/config/templates/BOOTSTRAP.md +1 -1
  107. package/src/config/templates/IDENTITY.md +8 -4
  108. package/src/config/templates/SOUL.md +14 -0
  109. package/src/config/templates/UPDATES.md +16 -0
  110. package/src/config/templates/USER.md +5 -1
  111. package/src/config/types.ts +1 -0
  112. package/src/config/update-bulletin-format.ts +52 -0
  113. package/src/config/update-bulletin-state.ts +49 -0
  114. package/src/config/update-bulletin.ts +82 -0
  115. package/src/config/vellum-skills/catalog.json +6 -0
  116. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  117. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  119. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  120. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  121. package/src/context/window-manager.ts +43 -3
  122. package/src/daemon/config-watcher.ts +1 -0
  123. package/src/daemon/connection-policy.ts +21 -1
  124. package/src/daemon/daemon-control.ts +164 -7
  125. package/src/daemon/date-context.ts +174 -1
  126. package/src/daemon/guardian-action-generators.ts +175 -0
  127. package/src/daemon/guardian-verification-intent.ts +120 -0
  128. package/src/daemon/handlers/apps.ts +1 -3
  129. package/src/daemon/handlers/config-channels.ts +8 -8
  130. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  131. package/src/daemon/handlers/config-inbox.ts +55 -159
  132. package/src/daemon/handlers/config-ingress.ts +1 -1
  133. package/src/daemon/handlers/config-integrations.ts +1 -1
  134. package/src/daemon/handlers/config-platform.ts +1 -1
  135. package/src/daemon/handlers/config-scheduling.ts +2 -2
  136. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  137. package/src/daemon/handlers/config-telegram.ts +1 -1
  138. package/src/daemon/handlers/config-twilio.ts +1 -1
  139. package/src/daemon/handlers/config-voice.ts +100 -0
  140. package/src/daemon/handlers/config.ts +3 -0
  141. package/src/daemon/handlers/index.ts +1 -1
  142. package/src/daemon/handlers/misc.ts +84 -6
  143. package/src/daemon/handlers/navigate-settings.ts +27 -0
  144. package/src/daemon/handlers/recording.ts +270 -144
  145. package/src/daemon/handlers/sessions.ts +107 -24
  146. package/src/daemon/handlers/subagents.ts +3 -3
  147. package/src/daemon/handlers/work-items.ts +10 -7
  148. package/src/daemon/ipc-contract/integrations.ts +9 -1
  149. package/src/daemon/ipc-contract/messages.ts +4 -0
  150. package/src/daemon/ipc-contract/sessions.ts +1 -1
  151. package/src/daemon/ipc-contract/settings.ts +26 -0
  152. package/src/daemon/ipc-contract/shared.ts +2 -0
  153. package/src/daemon/ipc-contract/work-items.ts +1 -7
  154. package/src/daemon/ipc-contract-inventory.json +5 -1
  155. package/src/daemon/ipc-contract.ts +5 -1
  156. package/src/daemon/lifecycle.ts +306 -266
  157. package/src/daemon/recording-executor.ts +1 -1
  158. package/src/daemon/recording-intent.ts +0 -41
  159. package/src/daemon/response-tier.ts +2 -2
  160. package/src/daemon/server.ts +6 -6
  161. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  162. package/src/daemon/session-agent-loop.ts +15 -8
  163. package/src/daemon/session-history.ts +3 -2
  164. package/src/daemon/session-media-retry.ts +3 -0
  165. package/src/daemon/session-messaging.ts +38 -4
  166. package/src/daemon/session-notifiers.ts +2 -2
  167. package/src/daemon/session-process.ts +256 -23
  168. package/src/daemon/session-queue-manager.ts +2 -0
  169. package/src/daemon/session-runtime-assembly.ts +39 -0
  170. package/src/daemon/session-skill-tools.ts +13 -4
  171. package/src/daemon/session-tool-setup.ts +6 -7
  172. package/src/daemon/session.ts +19 -8
  173. package/src/daemon/tls-certs.ts +55 -13
  174. package/src/daemon/tool-side-effects.ts +13 -5
  175. package/src/gallery/default-gallery.ts +32 -9
  176. package/src/influencer/client.ts +2 -1
  177. package/src/memory/channel-delivery-store.ts +37 -567
  178. package/src/memory/channel-guardian-store.ts +66 -1317
  179. package/src/memory/conflict-store.ts +4 -4
  180. package/src/memory/conversation-attention-store.ts +4 -7
  181. package/src/memory/conversation-crud.ts +668 -0
  182. package/src/memory/conversation-queries.ts +361 -0
  183. package/src/memory/conversation-store.ts +45 -983
  184. package/src/memory/db-connection.ts +3 -0
  185. package/src/memory/db-init.ts +25 -0
  186. package/src/memory/delivery-channels.ts +175 -0
  187. package/src/memory/delivery-crud.ts +211 -0
  188. package/src/memory/delivery-status.ts +199 -0
  189. package/src/memory/embedding-backend.ts +70 -4
  190. package/src/memory/embedding-local.ts +12 -2
  191. package/src/memory/entity-extractor.ts +3 -8
  192. package/src/memory/fts-reconciler.ts +121 -0
  193. package/src/memory/guardian-action-store.ts +366 -3
  194. package/src/memory/guardian-approvals.ts +569 -0
  195. package/src/memory/guardian-bindings.ts +130 -0
  196. package/src/memory/guardian-rate-limits.ts +196 -0
  197. package/src/memory/guardian-verification.ts +520 -0
  198. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  199. package/src/memory/job-utils.ts +8 -5
  200. package/src/memory/jobs-store.ts +66 -6
  201. package/src/memory/jobs-worker.ts +23 -1
  202. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  203. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  204. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  205. package/src/memory/migrations/100-core-tables.ts +1 -1
  206. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  207. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  208. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  209. package/src/memory/migrations/113-late-migrations.ts +1 -1
  210. package/src/memory/migrations/116-messages-fts.ts +13 -0
  211. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  212. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  213. package/src/memory/migrations/index.ts +8 -3
  214. package/src/memory/migrations/validate-migration-state.ts +114 -15
  215. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  216. package/src/memory/retriever.ts +46 -13
  217. package/src/memory/schema-migration.ts +3 -0
  218. package/src/memory/schema.ts +25 -7
  219. package/src/memory/search/semantic.ts +8 -90
  220. package/src/notifications/README.md +1 -1
  221. package/src/notifications/broadcaster.ts +20 -2
  222. package/src/notifications/conversation-pairing.ts +3 -3
  223. package/src/notifications/decision-engine.ts +173 -8
  224. package/src/notifications/deliveries-store.ts +27 -8
  225. package/src/notifications/preferences-store.ts +7 -7
  226. package/src/notifications/thread-candidates.ts +234 -0
  227. package/src/notifications/types.ts +18 -0
  228. package/src/permissions/defaults.ts +11 -1
  229. package/src/permissions/prompter.ts +17 -0
  230. package/src/permissions/trust-store.ts +2 -0
  231. package/src/providers/failover.ts +19 -0
  232. package/src/providers/registry.ts +46 -1
  233. package/src/runtime/approval-message-composer.ts +1 -1
  234. package/src/runtime/channel-guardian-service.ts +15 -3
  235. package/src/runtime/channel-retry-sweep.ts +7 -2
  236. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  237. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  238. package/src/runtime/guardian-action-message-composer.ts +245 -0
  239. package/src/runtime/guardian-outbound-actions.ts +35 -15
  240. package/src/runtime/guardian-verification-templates.ts +15 -9
  241. package/src/runtime/http-errors.ts +93 -0
  242. package/src/runtime/http-server.ts +140 -51
  243. package/src/runtime/http-types.ts +53 -0
  244. package/src/runtime/ingress-service.ts +237 -0
  245. package/src/runtime/middleware/error-handler.ts +4 -3
  246. package/src/runtime/middleware/rate-limiter.ts +160 -0
  247. package/src/runtime/middleware/request-logger.ts +71 -0
  248. package/src/runtime/middleware/twilio-validation.ts +7 -6
  249. package/src/runtime/pending-interactions.ts +12 -0
  250. package/src/runtime/routes/access-request-decision.ts +215 -0
  251. package/src/runtime/routes/app-routes.ts +25 -18
  252. package/src/runtime/routes/approval-routes.ts +18 -47
  253. package/src/runtime/routes/attachment-routes.ts +15 -41
  254. package/src/runtime/routes/call-routes.ts +20 -20
  255. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  256. package/src/runtime/routes/contact-routes.ts +4 -9
  257. package/src/runtime/routes/conversation-attention-routes.ts +5 -4
  258. package/src/runtime/routes/conversation-routes.ts +26 -57
  259. package/src/runtime/routes/debug-routes.ts +71 -0
  260. package/src/runtime/routes/events-routes.ts +3 -2
  261. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  262. package/src/runtime/routes/identity-routes.ts +14 -10
  263. package/src/runtime/routes/inbound-conversation.ts +3 -2
  264. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  265. package/src/runtime/routes/ingress-routes.ts +174 -0
  266. package/src/runtime/routes/integration-routes.ts +82 -20
  267. package/src/runtime/routes/pairing-routes.ts +11 -10
  268. package/src/runtime/routes/secret-routes.ts +10 -18
  269. package/src/runtime/verification-rate-limiter.ts +83 -0
  270. package/src/schedule/schedule-store.ts +13 -1
  271. package/src/schedule/scheduler.ts +2 -2
  272. package/src/security/secret-ingress.ts +5 -2
  273. package/src/security/secret-scanner.ts +72 -6
  274. package/src/subagent/manager.ts +6 -4
  275. package/src/swarm/plan-validator.ts +4 -1
  276. package/src/tasks/task-runner.ts +3 -1
  277. package/src/tools/browser/api-map.ts +9 -6
  278. package/src/tools/calls/call-start.ts +20 -0
  279. package/src/tools/executor.ts +50 -568
  280. package/src/tools/permission-checker.ts +272 -0
  281. package/src/tools/registry.ts +14 -6
  282. package/src/tools/reminder/reminder-store.ts +7 -7
  283. package/src/tools/reminder/reminder.ts +6 -3
  284. package/src/tools/secret-detection-handler.ts +301 -0
  285. package/src/tools/subagent/message.ts +1 -1
  286. package/src/tools/system/voice-config.ts +62 -0
  287. package/src/tools/tasks/index.ts +3 -3
  288. package/src/tools/tasks/work-item-list.ts +3 -3
  289. package/src/tools/tasks/work-item-update.ts +4 -5
  290. package/src/tools/tool-approval-handler.ts +192 -0
  291. package/src/tools/tool-manifest.ts +2 -0
  292. package/src/watcher/watcher-store.ts +9 -9
  293. package/src/work-items/work-item-runner.ts +9 -6
  294. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  295. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -23,7 +23,7 @@ export async function executeSubagentMessage(
23
23
  };
24
24
  }
25
25
 
26
- const result = manager.sendMessage(subagentId, content);
26
+ const result = await manager.sendMessage(subagentId, content);
27
27
 
28
28
  if (result === 'queue_full') {
29
29
  return {
@@ -0,0 +1,62 @@
1
+ import { normalizeActivationKey } from '../../daemon/handlers/config-voice.js';
2
+ import { RiskLevel } from '../../permissions/types.js';
3
+ import type { ToolDefinition } from '../../providers/types.js';
4
+ import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
5
+
6
+ const TOOL_NAME = 'voice_config_update';
7
+
8
+ export const voiceConfigUpdateTool: Tool = {
9
+ name: TOOL_NAME,
10
+ description:
11
+ 'Change the push-to-talk activation key. Valid keys: fn (Fn/Globe key), ctrl (Control key), fn_shift (Fn+Shift), none (disable PTT).',
12
+ category: 'system',
13
+ defaultRiskLevel: RiskLevel.Low,
14
+
15
+ getDefinition(): ToolDefinition {
16
+ return {
17
+ name: TOOL_NAME,
18
+ description: this.description,
19
+ input_schema: {
20
+ type: 'object',
21
+ properties: {
22
+ activation_key: {
23
+ type: 'string',
24
+ description:
25
+ 'The activation key to set. Accepts enum values (fn, ctrl, fn_shift, none) or natural language (e.g. "Control", "Fn+Shift", "Off").',
26
+ },
27
+ },
28
+ required: ['activation_key'],
29
+ },
30
+ };
31
+ },
32
+
33
+ async execute(
34
+ input: Record<string, unknown>,
35
+ context: ToolContext,
36
+ ): Promise<ToolExecutionResult> {
37
+ const rawKey = input.activation_key;
38
+ if (typeof rawKey !== 'string' || rawKey.trim() === '') {
39
+ return {
40
+ content: 'Error: activation_key is required and must be a non-empty string.',
41
+ isError: true,
42
+ };
43
+ }
44
+
45
+ const result = normalizeActivationKey(rawKey);
46
+ if (!result.ok) {
47
+ return { content: result.reason, isError: true };
48
+ }
49
+
50
+ const labels: Record<string, string> = {
51
+ fn: 'Fn/Globe key',
52
+ ctrl: 'Control key',
53
+ fn_shift: 'Fn+Shift',
54
+ none: 'disabled',
55
+ };
56
+
57
+ return {
58
+ content: `Push-to-talk activation key updated to ${labels[result.value]} (${result.value}).`,
59
+ isError: false,
60
+ };
61
+ },
62
+ };
@@ -2,9 +2,9 @@
2
2
  * UX Terminology Glossary
3
3
  * ──────────────────────────────────────────────────────────────────────
4
4
  *
5
- * "Task" (user-facing) — A work item in the Tasks panel (backed by the
6
- * `work_items` table). This is what users see, create, run, and track.
7
- * The user-facing surface is simply called "Tasks".
5
+ * "Task" (user-facing) — A work item in the task queue (backed by the
6
+ * `work_items` table). This is what users create, run, and track
7
+ * through conversation.
8
8
  *
9
9
  * "Task template" / "task definition" (internal) — A reusable template
10
10
  * saved from a conversation (backed by the `tasks` table). Templates
@@ -37,8 +37,8 @@ export async function executeTaskListShow(
37
37
  const filtered = statusFilter !== undefined;
38
38
 
39
39
  if (count === 0) {
40
- const suffix = filtered ? 'no items matching filter.' : 'no tasks queued.';
41
- return { content: `Opened Tasks window \u2014 ${suffix}`, isError: false };
40
+ const suffix = filtered ? 'No items matching that filter.' : 'No tasks queued.';
41
+ return { content: suffix, isError: false };
42
42
  }
43
43
 
44
44
  const label = filtered
@@ -47,7 +47,7 @@ export async function executeTaskListShow(
47
47
 
48
48
  const taskList = formatTaskList(items);
49
49
 
50
- return { content: `Opened Tasks window (${label}).\n\nCurrent tasks:\n${taskList}`, isError: false };
50
+ return { content: `Task queue (${label}):\n${taskList}`, isError: false };
51
51
  } catch (err) {
52
52
  const msg = err instanceof Error ? err.message : String(err);
53
53
  return { content: `Error: ${msg}`, isError: true };
@@ -53,12 +53,11 @@ export async function executeTaskListUpdate(
53
53
 
54
54
  const item = result.workItem;
55
55
 
56
- // Block direct transitions to 'done' — the only path to done is
57
- // through the Review action (handleWorkItemComplete in the daemon).
58
- if (input.status === 'done') {
59
- log.warn({ selectorType, resolvedWorkItemId: item.id }, 'rejected attempt to set status to done directly');
56
+ // Allow direct transitions to 'done' only from 'awaiting_review'
57
+ if (input.status === 'done' && item.status !== 'awaiting_review') {
58
+ log.warn({ selectorType, resolvedWorkItemId: item.id, currentStatus: item.status }, 'rejected attempt to set status to done from non-review state');
60
59
  return {
61
- content: 'Error: Cannot set status to \'done\' directly. Use the Review action in the Tasks window.',
60
+ content: `Error: Cannot mark as done from '${item.status}'. Tasks must reach 'awaiting_review' status first (typically after a task run completes). You can set the status to 'awaiting_review' if the task is ready to be completed.`,
62
61
  isError: true,
63
62
  };
64
63
  }
@@ -0,0 +1,192 @@
1
+ import { isToolBlocked } from '../security/parental-control-store.js';
2
+ import { getTaskRunRules } from '../tasks/ephemeral-permissions.js';
3
+ import { getLogger } from '../util/logger.js';
4
+ import { enforceGuardianOnlyPolicy } from './guardian-control-plane-policy.js';
5
+ import { getAllTools, getTool } from './registry.js';
6
+ import type { ExecutionTarget, Tool, ToolContext, ToolExecutionResult, ToolLifecycleEvent } from './types.js';
7
+
8
+ const log = getLogger('tool-approval-handler');
9
+
10
+ export type PreExecutionGateResult =
11
+ | { allowed: true; tool: Tool }
12
+ | { allowed: false; result: ToolExecutionResult };
13
+
14
+ /**
15
+ * Handles pre-execution approval gates: abort checks, parental controls,
16
+ * guardian policy, allowed-tool-set gating, and task-run preflight checks.
17
+ * These run before the interactive permission prompt flow.
18
+ */
19
+ export class ToolApprovalHandler {
20
+ /**
21
+ * Evaluate all pre-execution approval gates for a tool invocation.
22
+ * Returns the resolved Tool if all gates pass, or an early-return
23
+ * ToolExecutionResult if any gate blocks execution.
24
+ */
25
+ checkPreExecutionGates(
26
+ name: string,
27
+ input: Record<string, unknown>,
28
+ context: ToolContext,
29
+ executionTarget: ExecutionTarget,
30
+ riskLevel: string,
31
+ startTime: number,
32
+ emitLifecycleEvent: (event: ToolLifecycleEvent) => void,
33
+ ): PreExecutionGateResult {
34
+ // Bail out immediately if the session was aborted before this tool started.
35
+ if (context.signal?.aborted) {
36
+ const durationMs = Date.now() - startTime;
37
+ emitLifecycleEvent({
38
+ type: 'error',
39
+ toolName: name,
40
+ executionTarget,
41
+ input,
42
+ workingDir: context.workingDir,
43
+ sessionId: context.sessionId,
44
+ conversationId: context.conversationId,
45
+ requestId: context.requestId,
46
+ riskLevel,
47
+ decision: 'error',
48
+ durationMs,
49
+ errorMessage: 'Cancelled',
50
+ isExpected: true,
51
+ errorCategory: 'tool_failure',
52
+ });
53
+ return { allowed: false, result: { content: 'Cancelled', isError: true } };
54
+ }
55
+
56
+ // Reject tools blocked by parental control settings before any permission check.
57
+ if (isToolBlocked(name)) {
58
+ log.warn(
59
+ {
60
+ toolName: name,
61
+ sessionId: context.sessionId,
62
+ conversationId: context.conversationId,
63
+ principal: context.principal,
64
+ reason: 'blocked_by_parental_controls',
65
+ },
66
+ 'Parental control blocked tool invocation',
67
+ );
68
+ const durationMs = Date.now() - startTime;
69
+ emitLifecycleEvent({
70
+ type: 'permission_denied',
71
+ toolName: name,
72
+ executionTarget,
73
+ input,
74
+ workingDir: context.workingDir,
75
+ sessionId: context.sessionId,
76
+ conversationId: context.conversationId,
77
+ requestId: context.requestId,
78
+ riskLevel,
79
+ decision: 'deny',
80
+ reason: 'Blocked by parental control settings',
81
+ durationMs,
82
+ });
83
+ return { allowed: false, result: { content: 'This tool is blocked by parental control settings.', isError: true } };
84
+ }
85
+
86
+ // Reject tool invocations targeting guardian control-plane endpoints from non-guardian actors.
87
+ const guardianCheck = enforceGuardianOnlyPolicy(name, input, context.guardianActorRole);
88
+ if (guardianCheck.denied) {
89
+ log.warn({
90
+ toolName: name,
91
+ sessionId: context.sessionId,
92
+ conversationId: context.conversationId,
93
+ actorRole: context.guardianActorRole,
94
+ reason: 'guardian_only_policy',
95
+ }, 'Guardian-only policy blocked tool invocation');
96
+ const durationMs = Date.now() - startTime;
97
+ emitLifecycleEvent({
98
+ type: 'permission_denied',
99
+ toolName: name,
100
+ executionTarget,
101
+ input,
102
+ workingDir: context.workingDir,
103
+ sessionId: context.sessionId,
104
+ conversationId: context.conversationId,
105
+ requestId: context.requestId,
106
+ riskLevel,
107
+ decision: 'deny',
108
+ reason: guardianCheck.reason!,
109
+ durationMs,
110
+ });
111
+ return { allowed: false, result: { content: guardianCheck.reason!, isError: true } };
112
+ }
113
+
114
+ // Gate tools not active for the current turn
115
+ if (context.allowedToolNames && !context.allowedToolNames.has(name)) {
116
+ const msg = `Tool "${name}" is not currently active. Load the skill that provides this tool first.`;
117
+ const durationMs = Date.now() - startTime;
118
+ emitLifecycleEvent({
119
+ type: 'error',
120
+ toolName: name,
121
+ executionTarget,
122
+ input,
123
+ workingDir: context.workingDir,
124
+ sessionId: context.sessionId,
125
+ conversationId: context.conversationId,
126
+ requestId: context.requestId,
127
+ riskLevel,
128
+ decision: 'error',
129
+ durationMs,
130
+ errorMessage: msg,
131
+ isExpected: true,
132
+ errorCategory: 'tool_failure',
133
+ });
134
+ return { allowed: false, result: { content: msg, isError: true } };
135
+ }
136
+
137
+ // Belt-and-suspenders guard for task runs: only preflight-approved tools
138
+ // may execute. This catches cases where ephemeral rules might not cover
139
+ // a tool, ensuring unapproved calls fail deterministically instead of
140
+ // falling through to the interactive prompter.
141
+ if (context.taskRunId) {
142
+ const taskRules = getTaskRunRules(context.taskRunId);
143
+ const approvedToolNames = new Set(taskRules.map((r) => r.tool));
144
+ if (approvedToolNames.size > 0 && !approvedToolNames.has(name)) {
145
+ const msg = `Tool '${name}' was not approved in the task's preflight. Add it to required tools and re-approve.`;
146
+ const durationMs = Date.now() - startTime;
147
+ emitLifecycleEvent({
148
+ type: 'permission_denied',
149
+ toolName: name,
150
+ executionTarget,
151
+ input,
152
+ workingDir: context.workingDir,
153
+ sessionId: context.sessionId,
154
+ conversationId: context.conversationId,
155
+ requestId: context.requestId,
156
+ riskLevel,
157
+ decision: 'deny',
158
+ reason: msg,
159
+ durationMs,
160
+ });
161
+ return { allowed: false, result: { content: msg, isError: true } };
162
+ }
163
+ }
164
+
165
+ // Resolve the tool from the registry
166
+ const tool = getTool(name);
167
+ if (!tool) {
168
+ const available = getAllTools().filter((t) => t.executionMode !== 'proxy' || context.proxyToolResolver).map((t) => t.name).sort().join(', ');
169
+ const msg = `Unknown tool: ${name}. Available tools: ${available}`;
170
+ const durationMs = Date.now() - startTime;
171
+ emitLifecycleEvent({
172
+ type: 'error',
173
+ toolName: name,
174
+ executionTarget,
175
+ input,
176
+ workingDir: context.workingDir,
177
+ sessionId: context.sessionId,
178
+ conversationId: context.conversationId,
179
+ requestId: context.requestId,
180
+ riskLevel,
181
+ decision: 'error',
182
+ durationMs,
183
+ errorMessage: msg,
184
+ isExpected: true,
185
+ errorCategory: 'tool_failure',
186
+ });
187
+ return { allowed: false, result: { content: msg, isError: true } };
188
+ }
189
+
190
+ return { allowed: true, tool };
191
+ }
192
+ }
@@ -12,6 +12,7 @@ import { credentialStoreTool } from './credentials/vault.js';
12
12
  import { memorySaveTool, memorySearchTool, memoryUpdateTool } from './memory/register.js';
13
13
  import type { LazyToolDescriptor } from './registry.js';
14
14
  import { vellumSkillsCatalogTool } from './skills/vellum-catalog.js';
15
+ import { voiceConfigUpdateTool } from './system/voice-config.js';
15
16
  import type { Tool } from './types.js';
16
17
  import { screenWatchTool } from './watch/screen-watch.js';
17
18
 
@@ -66,6 +67,7 @@ export const explicitTools: Tool[] = [
66
67
  accountManageTool,
67
68
  screenWatchTool,
68
69
  vellumSkillsCatalogTool,
70
+ voiceConfigUpdateTool,
69
71
  ];
70
72
 
71
73
  // ── Lazy tool descriptors ───────────────────────────────────────────
@@ -1,7 +1,7 @@
1
1
  import { and, asc, desc, eq, gte, lte } from 'drizzle-orm';
2
2
  import { v4 as uuid } from 'uuid';
3
3
 
4
- import { getDb } from '../memory/db.js';
4
+ import { getDb, rawChanges } from '../memory/db.js';
5
5
  import { watcherEvents,watchers } from '../memory/schema.js';
6
6
  import { truncate } from '../util/truncate.js';
7
7
  import { DEFAULT_POLL_INTERVAL_MS } from './constants.js';
@@ -142,8 +142,8 @@ export function updateWatcher(
142
142
 
143
143
  export function deleteWatcher(id: string): boolean {
144
144
  const db = getDb();
145
- const result = db.delete(watchers).where(eq(watchers.id, id)).run() as unknown as { changes?: number };
146
- return (result.changes ?? 0) > 0;
145
+ db.delete(watchers).where(eq(watchers.id, id)).run();
146
+ return rawChanges() > 0;
147
147
  }
148
148
 
149
149
  // ── Claim / Complete ────────────────────────────────────────────────
@@ -167,7 +167,7 @@ export function claimDueWatchers(now: number): Watcher[] {
167
167
 
168
168
  const claimed: Watcher[] = [];
169
169
  for (const row of candidates) {
170
- const result = db
170
+ db
171
171
  .update(watchers)
172
172
  .set({ status: 'polling', updatedAt: now })
173
173
  .where(and(
@@ -175,9 +175,9 @@ export function claimDueWatchers(now: number): Watcher[] {
175
175
  eq(watchers.nextPollAt, row.nextPollAt),
176
176
  eq(watchers.status, 'idle'),
177
177
  ))
178
- .run() as unknown as { changes?: number };
178
+ .run();
179
179
 
180
- if ((result.changes ?? 0) === 0) continue;
180
+ if (rawChanges() === 0) continue;
181
181
  claimed.push(parseWatcherRow({ ...row, status: 'polling', updatedAt: now }));
182
182
  }
183
183
  return claimed;
@@ -271,12 +271,12 @@ export function setWatcherConversationId(id: string, conversationId: string): vo
271
271
  */
272
272
  export function resetStuckWatchers(): number {
273
273
  const db = getDb();
274
- const result = db
274
+ db
275
275
  .update(watchers)
276
276
  .set({ status: 'idle', updatedAt: Date.now() })
277
277
  .where(eq(watchers.status, 'polling'))
278
- .run() as unknown as { changes?: number };
279
- return result.changes ?? 0;
278
+ .run();
279
+ return rawChanges();
280
280
  }
281
281
 
282
282
  // ── Watcher Events ──────────────────────────────────────────────────
@@ -127,8 +127,8 @@ export function runWorkItemInBackground(workItemId: string): RunWorkItemResult {
127
127
  workItemId,
128
128
  title: workItem.title,
129
129
  } as ServerMessage);
130
- (session as unknown as { taskRunId?: string }).taskRunId = taskRunId;
131
- (session as unknown as { headlessLock: boolean }).headlessLock = true;
130
+ session.taskRunId = taskRunId;
131
+ session.headlessLock = true;
132
132
  }
133
133
  await session.processMessage(message, [], (event) => {
134
134
  broadcast(event);
@@ -136,8 +136,10 @@ export function runWorkItemInBackground(workItemId: string): RunWorkItemResult {
136
136
  },
137
137
  );
138
138
 
139
- if (session) {
140
- (session as unknown as { headlessLock: boolean }).headlessLock = false;
139
+ // TS can't track that session is mutated inside the closure above
140
+ const doneSession = session as { headlessLock: boolean } | null;
141
+ if (doneSession) {
142
+ doneSession.headlessLock = false;
141
143
  }
142
144
 
143
145
  const current = getWorkItem(workItemId);
@@ -154,8 +156,9 @@ export function runWorkItemInBackground(workItemId: string): RunWorkItemResult {
154
156
  broadcastWorkItemStatus(broadcast, workItemId);
155
157
  broadcast({ type: 'tasks_changed' } as ServerMessage);
156
158
  } catch (err) {
157
- if (session) {
158
- (session as unknown as { headlessLock: boolean }).headlessLock = false;
159
+ const errSession = session as { headlessLock: boolean } | null;
160
+ if (errSession) {
161
+ errSession.headlessLock = false;
159
162
  }
160
163
  log.error({ err, workItemId }, 'work item background run failed');
161
164
  updateWorkItem(workItemId, {