@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
@@ -42,17 +42,21 @@ import {
42
42
  getInterfacesDir,
43
43
  getRootDir,
44
44
  getSocketPath,
45
+ removeSocketFile,
45
46
  } from '../util/platform.js';
46
47
  import { listWorkItems, updateWorkItem } from '../work-items/work-item-store.js';
47
48
  import { WorkspaceHeartbeatService } from '../workspace/heartbeat-service.js';
48
49
  import { createApprovalConversationGenerator,createApprovalCopyGenerator } from './approval-generators.js';
49
- import { cleanupPidFile,writePid } from './daemon-control.js';
50
+ import { hasNoAuthOverride, hasUngatedNoAuthOverride } from './connection-policy.js';
51
+ import { cleanupPidFile, cleanupPidFileIfOwner, writePid } from './daemon-control.js';
52
+ import { createGuardianActionCopyGenerator, createGuardianFollowUpConversationGenerator } from './guardian-action-generators.js';
50
53
  import { initPairingHandlers } from './handlers/pairing.js';
51
54
  import { installCliLaunchers } from './install-cli-launchers.js';
52
55
  import type { ServerMessage } from './ipc-protocol.js';
53
56
  import { initializeProvidersAndTools, registerMessagingProviders,registerWatcherProviders } from './providers-setup.js';
54
57
  import { seedInterfaceFiles } from './seed-files.js';
55
58
  import { DaemonServer } from './server.js';
59
+ import { setGuardianActionCopyGenerator, setGuardianFollowUpConversationGenerator } from './session-process.js';
56
60
  import { initSlashPairingContext } from './session-slash.js';
57
61
  import { installShutdownHandlers } from './shutdown-handlers.js';
58
62
 
@@ -76,216 +80,255 @@ function loadDotEnv(): void {
76
80
  export async function runDaemon(): Promise<void> {
77
81
  loadDotEnv();
78
82
  validateEnv();
79
- initSentry();
80
- await initLogfire();
81
83
 
82
- // Migration order matters: first move legacy flat files into the data dir
83
- // structure, then relocate the data dir into the active workspace, and
84
- // finally create any directories that don't yet exist.
85
- migrateToDataLayout();
86
- migrateToWorkspaceLayout();
87
- ensureDataDir();
88
-
89
- log.info('Daemon startup: migrations complete');
90
-
91
- seedInterfaceFiles();
84
+ if (hasUngatedNoAuthOverride()) {
85
+ log.warn('VELLUM_DAEMON_NOAUTH is set but VELLUM_UNSAFE_AUTH_BYPASS=1 is not auth bypass is IGNORED and authentication remains enabled. Set VELLUM_UNSAFE_AUTH_BYPASS=1 to confirm the bypass.');
86
+ } else if (hasNoAuthOverride()) {
87
+ log.warn('VELLUM_DAEMON_NOAUTH is set — IPC authentication is DISABLED. This should only be used for development or SSH-forwarded sockets. Do not use in production.');
88
+ }
92
89
 
93
- log.info('Daemon startup: installing templates and initializing DB');
94
- installTemplates();
95
- ensurePromptFiles();
90
+ // Track whether the IPC socket has been created so we can clean it up
91
+ // if init crashes after the socket is bound but before shutdown handlers
92
+ // are installed.
93
+ let socketCreated = false;
96
94
 
97
95
  try {
98
- installCliLaunchers();
99
- } catch (err) {
100
- log.warn({ err }, 'CLI launcher installation failed — continuing startup');
101
- }
102
- initializeDb();
103
- log.info('Daemon startup: DB initialized');
104
-
105
- // Recover orphaned work items that were left in 'running' state when the
106
- // daemon previously crashed or was killed mid-task.
107
- const orphanedRunning = listWorkItems({ status: 'running' });
108
- if (orphanedRunning.length > 0) {
109
- for (const item of orphanedRunning) {
110
- updateWorkItem(item.id, { status: 'failed', lastRunStatus: 'interrupted' });
111
- log.info({ workItemId: item.id, title: item.title }, 'Recovered orphaned running work item → failed (interrupted)');
96
+ initSentry();
97
+ await initLogfire();
98
+
99
+ // Migration order matters: first move legacy flat files into the data dir
100
+ // structure, then relocate the data dir into the active workspace, and
101
+ // finally create any directories that don't yet exist.
102
+ migrateToDataLayout();
103
+ migrateToWorkspaceLayout();
104
+ ensureDataDir();
105
+
106
+ // Resolve and write the bearer token as early as possible so the CLI
107
+ // (which polls for this file during gateway startup) doesn't time out
108
+ // waiting for Qdrant or other slow init steps to finish.
109
+ const httpTokenPath = getHttpTokenPath();
110
+ let bearerToken = getRuntimeProxyBearerToken();
111
+ if (!bearerToken) {
112
+ try {
113
+ const existing = readFileSync(httpTokenPath, 'utf-8').trim();
114
+ if (existing) bearerToken = existing;
115
+ } catch {
116
+ // File doesn't exist or can't be read — will generate below
117
+ }
112
118
  }
113
- log.info({ count: orphanedRunning.length }, 'Recovered orphaned running work items');
114
- }
119
+ if (!bearerToken) {
120
+ bearerToken = randomBytes(32).toString('hex');
121
+ }
122
+ writeFileSync(httpTokenPath, bearerToken, { mode: 0o600 });
123
+ chmodSync(httpTokenPath, 0o600);
124
+ log.info('Daemon startup: bearer token written');
115
125
 
116
- try {
117
- const twilioProvider = new TwilioConversationRelayProvider();
118
- await reconcileCallsOnStartup(twilioProvider, log);
119
- } catch (err) {
120
- log.warn({ err }, 'Call recovery failed — continuing startup');
121
- }
126
+ log.info('Daemon startup: migrations complete');
122
127
 
123
- log.info('Daemon startup: loading config');
124
- const config = loadConfig();
128
+ seedInterfaceFiles();
125
129
 
126
- if (config.logFile.dir) {
127
- initLogger({ dir: config.logFile.dir, retentionDays: config.logFile.retentionDays });
128
- }
130
+ log.info('Daemon startup: installing templates and initializing DB');
131
+ installTemplates();
132
+ ensurePromptFiles();
129
133
 
130
- await initializeProvidersAndTools(config);
134
+ try {
135
+ installCliLaunchers();
136
+ } catch (err) {
137
+ log.warn({ err }, 'CLI launcher installation failed — continuing startup');
138
+ }
139
+ initializeDb();
140
+ log.info('Daemon startup: DB initialized');
141
+
142
+ // Recover orphaned work items that were left in 'running' state when the
143
+ // daemon previously crashed or was killed mid-task.
144
+ const orphanedRunning = listWorkItems({ status: 'running' });
145
+ if (orphanedRunning.length > 0) {
146
+ for (const item of orphanedRunning) {
147
+ updateWorkItem(item.id, { status: 'failed', lastRunStatus: 'interrupted' });
148
+ log.info({ workItemId: item.id, title: item.title }, 'Recovered orphaned running work item → failed (interrupted)');
149
+ }
150
+ log.info({ count: orphanedRunning.length }, 'Recovered orphaned running work items');
151
+ }
131
152
 
132
- // Start the IPC socket BEFORE Qdrant so that clients can connect
133
- // immediately. Qdrant startup can take 30+ seconds (binary download,
134
- // /readyz polling) which previously blocked the socket from appearing.
135
- log.info('Daemon startup: starting DaemonServer (IPC socket)');
136
- const server = new DaemonServer();
137
- await server.start();
138
- log.info('Daemon startup: DaemonServer started');
153
+ try {
154
+ const twilioProvider = new TwilioConversationRelayProvider();
155
+ await reconcileCallsOnStartup(twilioProvider, log);
156
+ } catch (err) {
157
+ log.warn({ err }, 'Call recovery failed — continuing startup');
158
+ }
139
159
 
140
- // Initialize Qdrant vector store — non-fatal so the daemon stays up without it
141
- const qdrantUrl = getQdrantUrlEnv() || config.memory.qdrant.url;
142
- log.info({ qdrantUrl }, 'Daemon startup: initializing Qdrant');
143
- const qdrantManager = new QdrantManager({ url: qdrantUrl });
144
- try {
145
- await qdrantManager.start();
146
- initQdrantClient({
147
- url: qdrantUrl,
148
- collection: config.memory.qdrant.collection,
149
- vectorSize: config.memory.qdrant.vectorSize,
150
- onDisk: config.memory.qdrant.onDisk,
151
- quantization: config.memory.qdrant.quantization,
152
- });
153
- log.info('Qdrant vector store initialized');
154
- } catch (err) {
155
- log.warn({ err }, 'Qdrant failed to start — memory features will be unavailable');
156
- }
160
+ log.info('Daemon startup: loading config');
161
+ const config = loadConfig();
157
162
 
158
- log.info('Daemon startup: starting memory worker');
159
- const memoryWorker = startMemoryJobsWorker();
160
-
161
- registerWatcherProviders();
162
- registerMessagingProviders();
163
-
164
- // Register the IPC broadcast function for the notification signal pipeline's
165
- // macOS adapter so it can deliver notification_intent messages to desktop clients.
166
- registerBroadcastFn((msg) => server.broadcast(msg));
167
-
168
- const scheduler = startScheduler(
169
- async (conversationId, message) => {
170
- await server.processMessage(conversationId, message);
171
- },
172
- (reminder) => {
173
- void emitNotificationSignal({
174
- sourceEventName: 'reminder.fired',
175
- sourceChannel: 'scheduler',
176
- sourceSessionId: reminder.id,
177
- attentionHints: {
178
- requiresAction: true,
179
- urgency: 'high',
180
- isAsyncBackground: false,
181
- visibleInSourceNow: false,
182
- },
183
- contextPayload: {
184
- reminderId: reminder.id,
185
- label: reminder.label,
186
- message: reminder.message,
187
- },
188
- routingIntent: reminder.routingIntent,
189
- routingHints: reminder.routingHints,
190
- dedupeKey: `reminder:${reminder.id}`,
191
- });
192
- },
193
- (schedule) => {
194
- void emitNotificationSignal({
195
- sourceEventName: 'schedule.complete',
196
- sourceChannel: 'scheduler',
197
- sourceSessionId: schedule.id,
198
- attentionHints: {
199
- requiresAction: false,
200
- urgency: 'medium',
201
- isAsyncBackground: true,
202
- visibleInSourceNow: false,
203
- },
204
- contextPayload: {
205
- scheduleId: schedule.id,
206
- name: schedule.name,
207
- },
208
- });
209
- },
210
- (notification) => {
211
- void emitNotificationSignal({
212
- sourceEventName: 'watcher.notification',
213
- sourceChannel: 'watcher',
214
- sourceSessionId: `watcher-${Date.now()}`,
215
- attentionHints: {
216
- requiresAction: false,
217
- urgency: 'low',
218
- isAsyncBackground: true,
219
- visibleInSourceNow: false,
220
- },
221
- contextPayload: {
222
- title: notification.title,
223
- body: notification.body,
224
- },
225
- });
226
- },
227
- (params) => {
228
- void emitNotificationSignal({
229
- sourceEventName: 'watcher.escalation',
230
- sourceChannel: 'watcher',
231
- sourceSessionId: `watcher-escalation-${Date.now()}`,
232
- attentionHints: {
233
- requiresAction: true,
234
- urgency: 'high',
235
- isAsyncBackground: false,
236
- visibleInSourceNow: false,
237
- },
238
- contextPayload: {
239
- title: params.title,
240
- body: params.body,
241
- },
242
- });
243
- },
244
- );
245
-
246
- // Start the runtime HTTP server. Required for iOS pairing (gateway proxies
247
- // to it) and optional REST API access. Defaults to port 7821.
248
- let runtimeHttp: RuntimeHttpServer | null = null;
249
- const httpPort = getRuntimeHttpPort();
250
- log.info({ httpPort }, 'Daemon startup: starting runtime HTTP server');
251
-
252
- // Resolve the bearer token in priority order:
253
- // 1. Explicit env var (e.g. cloud deploys)
254
- // 2. Existing token file on disk (preserves QR-paired iOS devices across restarts)
255
- // 3. Fresh random token (first-time startup)
256
- const httpTokenPath = getHttpTokenPath();
257
- let bearerToken = getRuntimeProxyBearerToken();
258
- if (!bearerToken) {
163
+ if (config.logFile.dir) {
164
+ initLogger({ dir: config.logFile.dir, retentionDays: config.logFile.retentionDays });
165
+ }
166
+
167
+ await initializeProvidersAndTools(config);
168
+
169
+ // Start the IPC socket BEFORE Qdrant so that clients can connect
170
+ // immediately. Qdrant startup can take 30+ seconds (binary download,
171
+ // /readyz polling) which previously blocked the socket from appearing.
172
+ log.info('Daemon startup: starting DaemonServer (IPC socket)');
173
+ const server = new DaemonServer();
174
+ await server.start();
175
+ socketCreated = true;
176
+ log.info('Daemon startup: DaemonServer started');
177
+
178
+ // Initialize Qdrant vector store — non-fatal so the daemon stays up without it
179
+ const qdrantUrl = getQdrantUrlEnv() || config.memory.qdrant.url;
180
+ log.info({ qdrantUrl }, 'Daemon startup: initializing Qdrant');
181
+ const qdrantManager = new QdrantManager({ url: qdrantUrl });
259
182
  try {
260
- const existing = readFileSync(httpTokenPath, 'utf-8').trim();
261
- if (existing) bearerToken = existing;
262
- } catch {
263
- // File doesn't exist or can't be read — will generate below
183
+ await qdrantManager.start();
184
+ initQdrantClient({
185
+ url: qdrantUrl,
186
+ collection: config.memory.qdrant.collection,
187
+ vectorSize: config.memory.qdrant.vectorSize,
188
+ onDisk: config.memory.qdrant.onDisk,
189
+ quantization: config.memory.qdrant.quantization,
190
+ });
191
+ log.info('Qdrant vector store initialized');
192
+ } catch (err) {
193
+ log.warn({ err }, 'Qdrant failed to start — memory features will be unavailable');
264
194
  }
265
- }
266
- if (!bearerToken) {
267
- bearerToken = randomBytes(32).toString('hex');
268
- }
269
- writeFileSync(httpTokenPath, bearerToken, { mode: 0o600 });
270
- chmodSync(httpTokenPath, 0o600);
271
-
272
- const hostname = getRuntimeHttpHost();
273
-
274
- runtimeHttp = new RuntimeHttpServer({
275
- port: httpPort,
276
- hostname,
277
- bearerToken,
278
- processMessage: (conversationId, content, attachmentIds, options, sourceChannel, sourceInterface) =>
279
- server.processMessage(conversationId, content, attachmentIds, options, sourceChannel, sourceInterface),
280
- persistAndProcessMessage: (conversationId, content, attachmentIds, options, sourceChannel, sourceInterface) =>
281
- server.persistAndProcessMessage(conversationId, content, attachmentIds, options, sourceChannel, sourceInterface),
282
- interfacesDir: getInterfacesDir(),
283
- approvalCopyGenerator: createApprovalCopyGenerator(),
284
- approvalConversationGenerator: createApprovalConversationGenerator(),
285
- sendMessageDeps: {
286
- getOrCreateSession: (conversationId) =>
195
+
196
+ log.info('Daemon startup: starting memory worker');
197
+ const memoryWorker = startMemoryJobsWorker();
198
+
199
+ registerWatcherProviders();
200
+ registerMessagingProviders();
201
+
202
+ // Register the IPC broadcast function for the notification signal pipeline's
203
+ // macOS adapter so it can deliver notification_intent messages to desktop clients.
204
+ registerBroadcastFn((msg) => server.broadcast(msg));
205
+
206
+ const scheduler = startScheduler(
207
+ async (conversationId, message) => {
208
+ await server.processMessage(conversationId, message);
209
+ },
210
+ (reminder) => {
211
+ void emitNotificationSignal({
212
+ sourceEventName: 'reminder.fired',
213
+ sourceChannel: 'scheduler',
214
+ sourceSessionId: reminder.id,
215
+ attentionHints: {
216
+ requiresAction: true,
217
+ urgency: 'high',
218
+ isAsyncBackground: false,
219
+ visibleInSourceNow: false,
220
+ },
221
+ contextPayload: {
222
+ reminderId: reminder.id,
223
+ label: reminder.label,
224
+ message: reminder.message,
225
+ },
226
+ routingIntent: reminder.routingIntent,
227
+ routingHints: reminder.routingHints,
228
+ dedupeKey: `reminder:${reminder.id}`,
229
+ });
230
+ },
231
+ (schedule) => {
232
+ void emitNotificationSignal({
233
+ sourceEventName: 'schedule.complete',
234
+ sourceChannel: 'scheduler',
235
+ sourceSessionId: schedule.id,
236
+ attentionHints: {
237
+ requiresAction: false,
238
+ urgency: 'medium',
239
+ isAsyncBackground: true,
240
+ visibleInSourceNow: false,
241
+ },
242
+ contextPayload: {
243
+ scheduleId: schedule.id,
244
+ name: schedule.name,
245
+ },
246
+ });
247
+ },
248
+ (notification) => {
249
+ void emitNotificationSignal({
250
+ sourceEventName: 'watcher.notification',
251
+ sourceChannel: 'watcher',
252
+ sourceSessionId: `watcher-${Date.now()}`,
253
+ attentionHints: {
254
+ requiresAction: false,
255
+ urgency: 'low',
256
+ isAsyncBackground: true,
257
+ visibleInSourceNow: false,
258
+ },
259
+ contextPayload: {
260
+ title: notification.title,
261
+ body: notification.body,
262
+ },
263
+ });
264
+ },
265
+ (params) => {
266
+ void emitNotificationSignal({
267
+ sourceEventName: 'watcher.escalation',
268
+ sourceChannel: 'watcher',
269
+ sourceSessionId: `watcher-escalation-${Date.now()}`,
270
+ attentionHints: {
271
+ requiresAction: true,
272
+ urgency: 'high',
273
+ isAsyncBackground: false,
274
+ visibleInSourceNow: false,
275
+ },
276
+ contextPayload: {
277
+ title: params.title,
278
+ body: params.body,
279
+ },
280
+ });
281
+ },
282
+ );
283
+
284
+ // Start the runtime HTTP server. Required for iOS pairing (gateway proxies
285
+ // to it) and optional REST API access. Defaults to port 7821.
286
+ let runtimeHttp: RuntimeHttpServer | null = null;
287
+ const httpPort = getRuntimeHttpPort();
288
+ log.info({ httpPort }, 'Daemon startup: starting runtime HTTP server');
289
+
290
+ const hostname = getRuntimeHttpHost();
291
+
292
+ runtimeHttp = new RuntimeHttpServer({
293
+ port: httpPort,
294
+ hostname,
295
+ bearerToken,
296
+ processMessage: (conversationId, content, attachmentIds, options, sourceChannel, sourceInterface) =>
297
+ server.processMessage(conversationId, content, attachmentIds, options, sourceChannel, sourceInterface),
298
+ persistAndProcessMessage: (conversationId, content, attachmentIds, options, sourceChannel, sourceInterface) =>
299
+ server.persistAndProcessMessage(conversationId, content, attachmentIds, options, sourceChannel, sourceInterface),
300
+ interfacesDir: getInterfacesDir(),
301
+ approvalCopyGenerator: createApprovalCopyGenerator(),
302
+ approvalConversationGenerator: createApprovalConversationGenerator(),
303
+ guardianActionCopyGenerator: (() => {
304
+ const gen = createGuardianActionCopyGenerator();
305
+ setGuardianActionCopyGenerator(gen);
306
+ return gen;
307
+ })(),
308
+ guardianFollowUpConversationGenerator: (() => {
309
+ const gen = createGuardianFollowUpConversationGenerator();
310
+ setGuardianFollowUpConversationGenerator(gen);
311
+ return gen;
312
+ })(),
313
+ sendMessageDeps: {
314
+ getOrCreateSession: (conversationId) =>
315
+ server.getSessionForMessages(conversationId),
316
+ assistantEventHub,
317
+ resolveAttachments: (attachmentIds) =>
318
+ attachmentsStore.getAttachmentsByIds(attachmentIds).map((a) => ({
319
+ id: a.id,
320
+ filename: a.originalFilename,
321
+ mimeType: a.mimeType,
322
+ data: a.dataBase64,
323
+ })),
324
+ },
325
+ });
326
+
327
+ // Inject voice bridge deps BEFORE attempting to start the HTTP server.
328
+ // The bridge must be available even when the HTTP server fails to bind.
329
+ setVoiceBridgeDeps({
330
+ getOrCreateSession: (conversationId, _transport) =>
287
331
  server.getSessionForMessages(conversationId),
288
- assistantEventHub,
289
332
  resolveAttachments: (attachmentIds) =>
290
333
  attachmentsStore.getAttachmentsByIds(attachmentIds).map((a) => ({
291
334
  id: a.id,
@@ -293,80 +336,77 @@ export async function runDaemon(): Promise<void> {
293
336
  mimeType: a.mimeType,
294
337
  data: a.dataBase64,
295
338
  })),
296
- },
297
- });
298
-
299
- // Inject voice bridge deps BEFORE attempting to start the HTTP server.
300
- // The bridge must be available even when the HTTP server fails to bind.
301
- setVoiceBridgeDeps({
302
- getOrCreateSession: (conversationId, _transport) =>
303
- server.getSessionForMessages(conversationId),
304
- resolveAttachments: (attachmentIds) =>
305
- attachmentsStore.getAttachmentsByIds(attachmentIds).map((a) => ({
306
- id: a.id,
307
- filename: a.originalFilename,
308
- mimeType: a.mimeType,
309
- data: a.dataBase64,
310
- })),
311
- deriveDefaultStrictSideEffects: (conversationId) => {
312
- const threadType = conversationStore.getConversationThreadType(conversationId);
313
- return threadType === 'private';
314
- },
315
- });
316
- try {
317
- await runtimeHttp.start();
318
- setRelayBroadcast((msg) => server.broadcast(msg));
319
- runtimeHttp.setPairingBroadcast((msg) => server.broadcast(msg as ServerMessage));
320
- initPairingHandlers(runtimeHttp.getPairingStore(), bearerToken);
321
- initSlashPairingContext(runtimeHttp.getPairingStore());
322
- server.setHttpPort(httpPort);
323
- log.info({ port: httpPort, hostname }, 'Daemon startup: runtime HTTP server listening');
324
- } catch (err) {
325
- log.warn({ err, port: httpPort }, 'Failed to start runtime HTTP server, continuing without it');
326
- runtimeHttp = null;
327
- }
339
+ deriveDefaultStrictSideEffects: (conversationId) => {
340
+ const threadType = conversationStore.getConversationThreadType(conversationId);
341
+ return threadType === 'private';
342
+ },
343
+ });
344
+ try {
345
+ await runtimeHttp.start();
346
+ setRelayBroadcast((msg) => server.broadcast(msg));
347
+ runtimeHttp.setPairingBroadcast((msg) => server.broadcast(msg as ServerMessage));
348
+ initPairingHandlers(runtimeHttp.getPairingStore(), bearerToken);
349
+ initSlashPairingContext(runtimeHttp.getPairingStore());
350
+ server.setHttpPort(httpPort);
351
+ log.info({ port: httpPort, hostname }, 'Daemon startup: runtime HTTP server listening');
352
+ } catch (err) {
353
+ log.warn({ err, port: httpPort }, 'Failed to start runtime HTTP server, continuing without it');
354
+ runtimeHttp = null;
355
+ }
328
356
 
329
- writePid(process.pid);
330
- log.info({ pid: process.pid }, 'Daemon started');
357
+ writePid(process.pid);
358
+ log.info({ pid: process.pid }, 'Daemon started');
331
359
 
332
- const hookManager = getHookManager();
333
- hookManager.watch();
360
+ const hookManager = getHookManager();
361
+ hookManager.watch();
334
362
 
335
- void hookManager.trigger('daemon-start', {
336
- pid: process.pid,
337
- socketPath: getSocketPath(),
338
- });
363
+ void hookManager.trigger('daemon-start', {
364
+ pid: process.pid,
365
+ socketPath: getSocketPath(),
366
+ });
339
367
 
340
- if (config.auditLog.retentionDays > 0) {
341
- try {
342
- rotateToolInvocations(config.auditLog.retentionDays);
343
- } catch (err) {
344
- log.warn({ err }, 'Audit log rotation failed');
368
+ if (config.auditLog.retentionDays > 0) {
369
+ try {
370
+ rotateToolInvocations(config.auditLog.retentionDays);
371
+ } catch (err) {
372
+ log.warn({ err }, 'Audit log rotation failed');
373
+ }
345
374
  }
346
- }
347
375
 
348
- const workspaceHeartbeat = new WorkspaceHeartbeatService();
349
- workspaceHeartbeat.start();
350
-
351
- const heartbeatConfig = config.heartbeat;
352
- const heartbeat = new HeartbeatService({
353
- processMessage: (conversationId, content) =>
354
- server.processMessage(conversationId, content),
355
- alerter: (alert) => server.broadcast(alert),
356
- });
357
- heartbeat.start();
358
- server.setHeartbeatService(heartbeat);
359
- log.info({ enabled: heartbeatConfig.enabled, intervalMs: heartbeatConfig.intervalMs }, 'Heartbeat service configured');
360
-
361
- installShutdownHandlers({
362
- server,
363
- workspaceHeartbeat,
364
- heartbeat,
365
- hookManager,
366
- runtimeHttp,
367
- scheduler,
368
- memoryWorker,
369
- qdrantManager,
370
- cleanupPidFile,
371
- });
376
+ const workspaceHeartbeat = new WorkspaceHeartbeatService();
377
+ workspaceHeartbeat.start();
378
+
379
+ const heartbeatConfig = config.heartbeat;
380
+ const heartbeat = new HeartbeatService({
381
+ processMessage: (conversationId, content) =>
382
+ server.processMessage(conversationId, content),
383
+ alerter: (alert) => server.broadcast(alert),
384
+ });
385
+ heartbeat.start();
386
+ server.setHeartbeatService(heartbeat);
387
+ log.info({ enabled: heartbeatConfig.enabled, intervalMs: heartbeatConfig.intervalMs }, 'Heartbeat service configured');
388
+
389
+ installShutdownHandlers({
390
+ server,
391
+ workspaceHeartbeat,
392
+ heartbeat,
393
+ hookManager,
394
+ runtimeHttp,
395
+ scheduler,
396
+ memoryWorker,
397
+ qdrantManager,
398
+ cleanupPidFile,
399
+ });
400
+ } catch (err) {
401
+ log.error({ err }, 'Daemon startup failed — cleaning up');
402
+ cleanupPidFileIfOwner(process.pid);
403
+ if (socketCreated) {
404
+ try {
405
+ removeSocketFile(getSocketPath());
406
+ } catch {
407
+ // Best-effort socket cleanup
408
+ }
409
+ }
410
+ throw err;
411
+ }
372
412
  }
@@ -5,7 +5,6 @@
5
5
 
6
6
  import type * as net from 'node:net';
7
7
 
8
- import type { HandlerContext } from './handlers/shared.js';
9
8
  import {
10
9
  handleRecordingPause,
11
10
  handleRecordingRestart,
@@ -14,6 +13,7 @@ import {
14
13
  handleRecordingStop,
15
14
  isRecordingIdle,
16
15
  } from './handlers/recording.js';
16
+ import type { HandlerContext } from './handlers/shared.js';
17
17
  import type { RecordingIntentResult } from './recording-intent.js';
18
18
 
19
19
  export interface RecordingExecutionContext {