@vellumai/assistant 0.4.11 → 0.4.13

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 (111) hide show
  1. package/ARCHITECTURE.md +401 -385
  2. package/package.json +1 -1
  3. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
  4. package/src/__tests__/registry.test.ts +235 -187
  5. package/src/__tests__/secure-keys.test.ts +27 -0
  6. package/src/__tests__/session-agent-loop.test.ts +521 -256
  7. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
  8. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  9. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  10. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  11. package/src/__tests__/skills.test.ts +334 -276
  12. package/src/__tests__/slack-skill.test.ts +124 -0
  13. package/src/__tests__/starter-task-flow.test.ts +7 -17
  14. package/src/agent/loop.ts +10 -3
  15. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
  16. package/src/config/bundled-skills/doordash/SKILL.md +171 -0
  17. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
  18. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
  19. package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
  20. package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
  21. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
  22. package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
  23. package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
  24. package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
  25. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
  26. package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
  27. package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
  28. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
  29. package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
  30. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
  31. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
  32. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
  33. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
  34. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
  35. package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
  36. package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
  37. package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
  38. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
  39. package/src/config/bundled-skills/messaging/SKILL.md +59 -42
  40. package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
  41. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
  42. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
  43. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
  44. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
  45. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
  46. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
  47. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
  48. package/src/config/bundled-skills/notion/SKILL.md +240 -0
  49. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
  50. package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
  51. package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
  52. package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
  53. package/src/config/bundled-skills/slack/SKILL.md +49 -0
  54. package/src/config/bundled-skills/slack/TOOLS.json +167 -0
  55. package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
  56. package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
  57. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
  58. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
  59. package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
  60. package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
  61. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
  62. package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
  63. package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
  64. package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
  65. package/src/config/bundled-tool-registry.ts +292 -267
  66. package/src/config/schema.ts +1 -1
  67. package/src/daemon/handlers/skills.ts +334 -234
  68. package/src/daemon/ipc-contract/messages.ts +2 -0
  69. package/src/daemon/ipc-contract/surfaces.ts +2 -0
  70. package/src/daemon/lifecycle.ts +358 -221
  71. package/src/daemon/response-tier.ts +2 -0
  72. package/src/daemon/server.ts +453 -193
  73. package/src/daemon/session-agent-loop-handlers.ts +43 -2
  74. package/src/daemon/session-agent-loop.ts +3 -0
  75. package/src/daemon/session-lifecycle.ts +3 -0
  76. package/src/daemon/session-process.ts +1 -0
  77. package/src/daemon/session-surfaces.ts +22 -20
  78. package/src/daemon/session-tool-setup.ts +1 -0
  79. package/src/daemon/session.ts +5 -2
  80. package/src/messaging/outreach-classifier.ts +12 -5
  81. package/src/messaging/provider-types.ts +5 -0
  82. package/src/messaging/provider.ts +1 -1
  83. package/src/messaging/providers/gmail/adapter.ts +11 -5
  84. package/src/messaging/providers/gmail/client.ts +2 -0
  85. package/src/messaging/providers/slack/adapter.ts +1 -0
  86. package/src/messaging/providers/slack/client.ts +8 -0
  87. package/src/messaging/providers/slack/types.ts +5 -0
  88. package/src/runtime/http-errors.ts +33 -20
  89. package/src/runtime/http-server.ts +706 -291
  90. package/src/runtime/http-types.ts +26 -16
  91. package/src/runtime/routes/secret-routes.ts +57 -2
  92. package/src/runtime/routes/surface-action-routes.ts +66 -0
  93. package/src/runtime/routes/trust-rules-routes.ts +140 -0
  94. package/src/security/keychain-to-encrypted-migration.ts +59 -0
  95. package/src/security/secure-keys.ts +17 -0
  96. package/src/skills/frontmatter.ts +9 -7
  97. package/src/tools/apps/executors.ts +2 -1
  98. package/src/tools/tool-manifest.ts +44 -42
  99. package/src/tools/types.ts +9 -0
  100. package/src/__tests__/skill-mirror-parity.test.ts +0 -176
  101. package/src/config/vellum-skills/catalog.json +0 -63
  102. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
  103. package/src/skills/vellum-catalog-remote.ts +0 -166
  104. package/src/tools/skills/vellum-catalog.ts +0 -168
  105. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
  106. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
  107. /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
  108. /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
  109. /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
  110. /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
  111. /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
@@ -1,47 +1,54 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { chmodSync,readFileSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
-
5
- import { config as dotenvConfig } from 'dotenv';
6
-
7
- import { setPointerMessageProcessor } from '../calls/call-pointer-messages.js';
8
- import { reconcileCallsOnStartup } from '../calls/call-recovery.js';
9
- import { setRelayBroadcast } from '../calls/relay-server.js';
10
- import { TwilioConversationRelayProvider } from '../calls/twilio-provider.js';
11
- import { setVoiceBridgeDeps } from '../calls/voice-session-bridge.js';
12
- import { isAssistantFeatureFlagEnabled } from '../config/assistant-feature-flags.js';
1
+ import { randomBytes } from "node:crypto";
2
+ import { chmodSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { config as dotenvConfig } from "dotenv";
6
+
7
+ import { setPointerMessageProcessor } from "../calls/call-pointer-messages.js";
8
+ import { reconcileCallsOnStartup } from "../calls/call-recovery.js";
9
+ import { setRelayBroadcast } from "../calls/relay-server.js";
10
+ import { TwilioConversationRelayProvider } from "../calls/twilio-provider.js";
11
+ import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
12
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
13
13
  import {
14
14
  getQdrantUrlEnv,
15
15
  getRuntimeHttpHost,
16
16
  getRuntimeHttpPort,
17
17
  getRuntimeProxyBearerToken,
18
18
  validateEnv,
19
- } from '../config/env.js';
20
- import { loadConfig } from '../config/loader.js';
21
- import { ensurePromptFiles } from '../config/system-prompt.js';
22
- import { syncUpdateBulletinOnStartup } from '../config/update-bulletin.js';
23
- import { HeartbeatService } from '../heartbeat/heartbeat-service.js';
24
- import { getHookManager } from '../hooks/manager.js';
25
- import { installTemplates } from '../hooks/templates.js';
26
- import { closeSentry, initSentry } from '../instrument.js';
27
- import { initLogfire } from '../logfire.js';
28
- import { getMcpServerManager } from '../mcp/manager.js';
29
- import * as attachmentsStore from '../memory/attachments-store.js';
30
- import * as conversationStore from '../memory/conversation-store.js';
31
- import { initializeDb } from '../memory/db.js';
32
- import { startMemoryJobsWorker } from '../memory/jobs-worker.js';
33
- import { initQdrantClient } from '../memory/qdrant-client.js';
34
- import { QdrantManager } from '../memory/qdrant-manager.js';
35
- import { rotateToolInvocations } from '../memory/tool-usage-store.js';
36
- import { migrateToDataLayout } from '../migrations/data-layout.js';
37
- import { migrateToWorkspaceLayout } from '../migrations/workspace-layout.js';
38
- import { emitNotificationSignal, registerBroadcastFn } from '../notifications/emit-signal.js';
39
- import { initSigningKey, loadOrCreateSigningKey } from '../runtime/actor-token-service.js';
40
- import { assistantEventHub } from '../runtime/assistant-event-hub.js';
41
- import { ensureVellumGuardianBinding } from '../runtime/guardian-vellum-migration.js';
42
- import { RuntimeHttpServer } from '../runtime/http-server.js';
43
- import { startScheduler } from '../schedule/scheduler.js';
44
- import { getLogger, initLogger } from '../util/logger.js';
19
+ } from "../config/env.js";
20
+ import { loadConfig } from "../config/loader.js";
21
+ import { ensurePromptFiles } from "../config/system-prompt.js";
22
+ import { syncUpdateBulletinOnStartup } from "../config/update-bulletin.js";
23
+ import { HeartbeatService } from "../heartbeat/heartbeat-service.js";
24
+ import { getHookManager } from "../hooks/manager.js";
25
+ import { installTemplates } from "../hooks/templates.js";
26
+ import { closeSentry, initSentry } from "../instrument.js";
27
+ import { initLogfire } from "../logfire.js";
28
+ import { getMcpServerManager } from "../mcp/manager.js";
29
+ import * as attachmentsStore from "../memory/attachments-store.js";
30
+ import * as conversationStore from "../memory/conversation-store.js";
31
+ import { initializeDb } from "../memory/db.js";
32
+ import { startMemoryJobsWorker } from "../memory/jobs-worker.js";
33
+ import { initQdrantClient } from "../memory/qdrant-client.js";
34
+ import { QdrantManager } from "../memory/qdrant-manager.js";
35
+ import { rotateToolInvocations } from "../memory/tool-usage-store.js";
36
+ import { migrateToDataLayout } from "../migrations/data-layout.js";
37
+ import { migrateToWorkspaceLayout } from "../migrations/workspace-layout.js";
38
+ import {
39
+ emitNotificationSignal,
40
+ registerBroadcastFn,
41
+ } from "../notifications/emit-signal.js";
42
+ import {
43
+ initSigningKey,
44
+ loadOrCreateSigningKey,
45
+ } from "../runtime/actor-token-service.js";
46
+ import { assistantEventHub } from "../runtime/assistant-event-hub.js";
47
+ import { ensureVellumGuardianBinding } from "../runtime/guardian-vellum-migration.js";
48
+ import { RuntimeHttpServer } from "../runtime/http-server.js";
49
+ import { startScheduler } from "../schedule/scheduler.js";
50
+ import { migrateKeychainToEncrypted } from "../security/keychain-to-encrypted-migration.js";
51
+ import { getLogger, initLogger } from "../util/logger.js";
45
52
  import {
46
53
  ensureDataDir,
47
54
  getHttpTokenPath,
@@ -49,24 +56,44 @@ import {
49
56
  getRootDir,
50
57
  getSocketPath,
51
58
  removeSocketFile,
52
- } from '../util/platform.js';
53
- import { listWorkItems, updateWorkItem } from '../work-items/work-item-store.js';
54
- import { WorkspaceHeartbeatService } from '../workspace/heartbeat-service.js';
55
- import { createApprovalConversationGenerator,createApprovalCopyGenerator } from './approval-generators.js';
56
- import { hasNoAuthOverride, hasUngatedNoAuthOverride } from './connection-policy.js';
57
- import { cleanupPidFile, cleanupPidFileIfOwner, writePid } from './daemon-control.js';
58
- import { createGuardianActionCopyGenerator, createGuardianFollowUpConversationGenerator } from './guardian-action-generators.js';
59
- import { initPairingHandlers } from './handlers/pairing.js';
60
- import { installCliLaunchers } from './install-cli-launchers.js';
61
- import type { ServerMessage } from './ipc-protocol.js';
62
- import { initializeProvidersAndTools, registerMessagingProviders,registerWatcherProviders } from './providers-setup.js';
63
- import { seedInterfaceFiles } from './seed-files.js';
64
- import { DaemonServer } from './server.js';
65
- import { initSlashPairingContext } from './session-slash.js';
66
- import { installShutdownHandlers } from './shutdown-handlers.js';
59
+ } from "../util/platform.js";
60
+ import {
61
+ listWorkItems,
62
+ updateWorkItem,
63
+ } from "../work-items/work-item-store.js";
64
+ import { WorkspaceHeartbeatService } from "../workspace/heartbeat-service.js";
65
+ import {
66
+ createApprovalConversationGenerator,
67
+ createApprovalCopyGenerator,
68
+ } from "./approval-generators.js";
69
+ import {
70
+ hasNoAuthOverride,
71
+ hasUngatedNoAuthOverride,
72
+ } from "./connection-policy.js";
73
+ import {
74
+ cleanupPidFile,
75
+ cleanupPidFileIfOwner,
76
+ writePid,
77
+ } from "./daemon-control.js";
78
+ import {
79
+ createGuardianActionCopyGenerator,
80
+ createGuardianFollowUpConversationGenerator,
81
+ } from "./guardian-action-generators.js";
82
+ import { initPairingHandlers } from "./handlers/pairing.js";
83
+ import { installCliLaunchers } from "./install-cli-launchers.js";
84
+ import type { ServerMessage } from "./ipc-protocol.js";
85
+ import {
86
+ initializeProvidersAndTools,
87
+ registerMessagingProviders,
88
+ registerWatcherProviders,
89
+ } from "./providers-setup.js";
90
+ import { seedInterfaceFiles } from "./seed-files.js";
91
+ import { DaemonServer } from "./server.js";
92
+ import { initSlashPairingContext } from "./session-slash.js";
93
+ import { installShutdownHandlers } from "./shutdown-handlers.js";
67
94
 
68
95
  // Re-export public API so existing consumers don't need to change imports
69
- export type { StopResult } from './daemon-control.js';
96
+ export type { StopResult } from "./daemon-control.js";
70
97
  export {
71
98
  cleanupPidFile,
72
99
  ensureDaemonRunning,
@@ -74,12 +101,12 @@ export {
74
101
  isDaemonRunning,
75
102
  startDaemon,
76
103
  stopDaemon,
77
- } from './daemon-control.js';
104
+ } from "./daemon-control.js";
78
105
 
79
- const log = getLogger('lifecycle');
106
+ const log = getLogger("lifecycle");
80
107
 
81
108
  function loadDotEnv(): void {
82
- dotenvConfig({ path: join(getRootDir(), '.env'), quiet: true });
109
+ dotenvConfig({ path: join(getRootDir(), ".env"), quiet: true });
83
110
  }
84
111
 
85
112
  // Entry point for the daemon process itself
@@ -88,9 +115,13 @@ export async function runDaemon(): Promise<void> {
88
115
  validateEnv();
89
116
 
90
117
  if (hasUngatedNoAuthOverride()) {
91
- 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.');
118
+ log.warn(
119
+ "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.",
120
+ );
92
121
  } else if (hasNoAuthOverride()) {
93
- 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.');
122
+ log.warn(
123
+ "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.",
124
+ );
94
125
  }
95
126
 
96
127
  // Track whether the IPC socket has been created so we can clean it up
@@ -113,6 +144,10 @@ export async function runDaemon(): Promise<void> {
113
144
  migrateToWorkspaceLayout();
114
145
  ensureDataDir();
115
146
 
147
+ // Copy any existing macOS keychain secrets into the encrypted file store
148
+ // before config loads, so the new encrypted-store-first read path sees them.
149
+ migrateKeychainToEncrypted();
150
+
116
151
  // Resolve and write the bearer token as early as possible so the CLI
117
152
  // (which polls for this file during gateway startup) doesn't time out
118
153
  // waiting for Qdrant or other slow init steps to finish.
@@ -120,81 +155,102 @@ export async function runDaemon(): Promise<void> {
120
155
  let bearerToken = getRuntimeProxyBearerToken();
121
156
  if (!bearerToken) {
122
157
  try {
123
- const existing = readFileSync(httpTokenPath, 'utf-8').trim();
158
+ const existing = readFileSync(httpTokenPath, "utf-8").trim();
124
159
  if (existing) bearerToken = existing;
125
160
  } catch {
126
161
  // File doesn't exist or can't be read — will generate below
127
162
  }
128
163
  }
129
164
  if (!bearerToken) {
130
- bearerToken = randomBytes(32).toString('hex');
165
+ bearerToken = randomBytes(32).toString("hex");
131
166
  }
132
167
  writeFileSync(httpTokenPath, bearerToken, { mode: 0o600 });
133
168
  chmodSync(httpTokenPath, 0o600);
134
- log.info('Daemon startup: bearer token written');
169
+ log.info("Daemon startup: bearer token written");
135
170
 
136
171
  // Load (or generate + persist) the actor-token signing key so tokens
137
172
  // survive daemon restarts. Must happen after ensureDataDir() creates
138
173
  // the protected directory.
139
174
  initSigningKey(loadOrCreateSigningKey());
140
175
 
141
- log.info('Daemon startup: migrations complete');
176
+ log.info("Daemon startup: migrations complete");
142
177
 
143
178
  seedInterfaceFiles();
144
179
 
145
- log.info('Daemon startup: installing templates and initializing DB');
180
+ log.info("Daemon startup: installing templates and initializing DB");
146
181
  installTemplates();
147
182
  ensurePromptFiles();
148
183
 
149
184
  try {
150
185
  installCliLaunchers();
151
186
  } catch (err) {
152
- log.warn({ err }, 'CLI launcher installation failed — continuing startup');
187
+ log.warn(
188
+ { err },
189
+ "CLI launcher installation failed — continuing startup",
190
+ );
153
191
  }
154
192
  initializeDb();
155
- log.info('Daemon startup: DB initialized');
193
+ log.info("Daemon startup: DB initialized");
156
194
 
157
195
  // Backfill vellum guardian binding for existing installations
158
196
  try {
159
- ensureVellumGuardianBinding('self');
197
+ ensureVellumGuardianBinding("self");
160
198
  } catch (err) {
161
- log.warn({ err }, 'Vellum guardian binding backfill failed — continuing startup');
199
+ log.warn(
200
+ { err },
201
+ "Vellum guardian binding backfill failed — continuing startup",
202
+ );
162
203
  }
163
204
 
164
205
  try {
165
206
  syncUpdateBulletinOnStartup();
166
207
  } catch (err) {
167
- log.warn({ err }, 'Bulletin sync failed — continuing startup');
208
+ log.warn({ err }, "Bulletin sync failed — continuing startup");
168
209
  }
169
210
 
170
211
  // Recover orphaned work items that were left in 'running' state when the
171
212
  // daemon previously crashed or was killed mid-task.
172
- const orphanedRunning = listWorkItems({ status: 'running' });
213
+ const orphanedRunning = listWorkItems({ status: "running" });
173
214
  if (orphanedRunning.length > 0) {
174
215
  for (const item of orphanedRunning) {
175
- updateWorkItem(item.id, { status: 'failed', lastRunStatus: 'interrupted' });
176
- log.info({ workItemId: item.id, title: item.title }, 'Recovered orphaned running work item → failed (interrupted)');
216
+ updateWorkItem(item.id, {
217
+ status: "failed",
218
+ lastRunStatus: "interrupted",
219
+ });
220
+ log.info(
221
+ { workItemId: item.id, title: item.title },
222
+ "Recovered orphaned running work item → failed (interrupted)",
223
+ );
177
224
  }
178
- log.info({ count: orphanedRunning.length }, 'Recovered orphaned running work items');
225
+ log.info(
226
+ { count: orphanedRunning.length },
227
+ "Recovered orphaned running work items",
228
+ );
179
229
  }
180
230
 
181
231
  try {
182
232
  const twilioProvider = new TwilioConversationRelayProvider();
183
233
  await reconcileCallsOnStartup(twilioProvider, log);
184
234
  } catch (err) {
185
- log.warn({ err }, 'Call recovery failed — continuing startup');
235
+ log.warn({ err }, "Call recovery failed — continuing startup");
186
236
  }
187
237
 
188
- log.info('Daemon startup: loading config');
238
+ log.info("Daemon startup: loading config");
189
239
  const config = loadConfig();
190
240
 
191
241
  if (config.logFile.dir) {
192
- initLogger({ dir: config.logFile.dir, retentionDays: config.logFile.retentionDays });
242
+ initLogger({
243
+ dir: config.logFile.dir,
244
+ retentionDays: config.logFile.retentionDays,
245
+ });
193
246
  }
194
247
 
195
248
  // If the user has opted out of crash reporting, stop Sentry from capturing
196
249
  // future events. Early-startup crashes before this point are still captured.
197
- const collectUsageData = isAssistantFeatureFlagEnabled('feature_flags.collect-usage-data.enabled', config);
250
+ const collectUsageData = isAssistantFeatureFlagEnabled(
251
+ "feature_flags.collect-usage-data.enabled",
252
+ config,
253
+ );
198
254
  if (!collectUsageData) {
199
255
  await closeSentry();
200
256
  }
@@ -204,15 +260,15 @@ export async function runDaemon(): Promise<void> {
204
260
  // Start the IPC socket BEFORE Qdrant so that clients can connect
205
261
  // immediately. Qdrant startup can take 30+ seconds (binary download,
206
262
  // /readyz polling) which previously blocked the socket from appearing.
207
- log.info('Daemon startup: starting DaemonServer (IPC socket)');
263
+ log.info("Daemon startup: starting DaemonServer (IPC socket)");
208
264
  const server = new DaemonServer();
209
265
  await server.start();
210
266
  socketCreated = true;
211
- log.info('Daemon startup: DaemonServer started');
267
+ log.info("Daemon startup: DaemonServer started");
212
268
 
213
269
  // Initialize Qdrant vector store — non-fatal so the daemon stays up without it
214
270
  const qdrantUrl = getQdrantUrlEnv() || config.memory.qdrant.url;
215
- log.info({ qdrantUrl }, 'Daemon startup: initializing Qdrant');
271
+ log.info({ qdrantUrl }, "Daemon startup: initializing Qdrant");
216
272
  const qdrantManager = new QdrantManager({ url: qdrantUrl });
217
273
  try {
218
274
  await qdrantManager.start();
@@ -223,12 +279,15 @@ export async function runDaemon(): Promise<void> {
223
279
  onDisk: config.memory.qdrant.onDisk,
224
280
  quantization: config.memory.qdrant.quantization,
225
281
  });
226
- log.info('Qdrant vector store initialized');
282
+ log.info("Qdrant vector store initialized");
227
283
  } catch (err) {
228
- log.warn({ err }, 'Qdrant failed to start — memory features will be unavailable');
284
+ log.warn(
285
+ { err },
286
+ "Qdrant failed to start — memory features will be unavailable",
287
+ );
229
288
  }
230
289
 
231
- log.info('Daemon startup: starting memory worker');
290
+ log.info("Daemon startup: starting memory worker");
232
291
  const memoryWorker = startMemoryJobsWorker();
233
292
 
234
293
  registerWatcherProviders();
@@ -244,12 +303,12 @@ export async function runDaemon(): Promise<void> {
244
303
  },
245
304
  (reminder) => {
246
305
  void emitNotificationSignal({
247
- sourceEventName: 'reminder.fired',
248
- sourceChannel: 'scheduler',
306
+ sourceEventName: "reminder.fired",
307
+ sourceChannel: "scheduler",
249
308
  sourceSessionId: reminder.id,
250
309
  attentionHints: {
251
310
  requiresAction: true,
252
- urgency: 'high',
311
+ urgency: "high",
253
312
  isAsyncBackground: false,
254
313
  visibleInSourceNow: false,
255
314
  },
@@ -265,12 +324,12 @@ export async function runDaemon(): Promise<void> {
265
324
  },
266
325
  (schedule) => {
267
326
  void emitNotificationSignal({
268
- sourceEventName: 'schedule.complete',
269
- sourceChannel: 'scheduler',
327
+ sourceEventName: "schedule.complete",
328
+ sourceChannel: "scheduler",
270
329
  sourceSessionId: schedule.id,
271
330
  attentionHints: {
272
331
  requiresAction: false,
273
- urgency: 'medium',
332
+ urgency: "medium",
274
333
  isAsyncBackground: true,
275
334
  visibleInSourceNow: false,
276
335
  },
@@ -282,12 +341,12 @@ export async function runDaemon(): Promise<void> {
282
341
  },
283
342
  (notification) => {
284
343
  void emitNotificationSignal({
285
- sourceEventName: 'watcher.notification',
286
- sourceChannel: 'watcher',
344
+ sourceEventName: "watcher.notification",
345
+ sourceChannel: "watcher",
287
346
  sourceSessionId: `watcher-${Date.now()}`,
288
347
  attentionHints: {
289
348
  requiresAction: false,
290
- urgency: 'low',
349
+ urgency: "low",
291
350
  isAsyncBackground: true,
292
351
  visibleInSourceNow: false,
293
352
  },
@@ -299,12 +358,12 @@ export async function runDaemon(): Promise<void> {
299
358
  },
300
359
  (params) => {
301
360
  void emitNotificationSignal({
302
- sourceEventName: 'watcher.escalation',
303
- sourceChannel: 'watcher',
361
+ sourceEventName: "watcher.escalation",
362
+ sourceChannel: "watcher",
304
363
  sourceSessionId: `watcher-escalation-${Date.now()}`,
305
364
  attentionHints: {
306
365
  requiresAction: true,
307
- urgency: 'high',
366
+ urgency: "high",
308
367
  isAsyncBackground: false,
309
368
  visibleInSourceNow: false,
310
369
  },
@@ -320,7 +379,7 @@ export async function runDaemon(): Promise<void> {
320
379
  // to it) and optional REST API access. Defaults to port 7821.
321
380
  let runtimeHttp: RuntimeHttpServer | null = null;
322
381
  const httpPort = getRuntimeHttpPort();
323
- log.info({ httpPort }, 'Daemon startup: starting runtime HTTP server');
382
+ log.info({ httpPort }, "Daemon startup: starting runtime HTTP server");
324
383
 
325
384
  const hostname = getRuntimeHttpHost();
326
385
 
@@ -328,15 +387,44 @@ export async function runDaemon(): Promise<void> {
328
387
  port: httpPort,
329
388
  hostname,
330
389
  bearerToken,
331
- processMessage: (conversationId, content, attachmentIds, options, sourceChannel, sourceInterface) =>
332
- server.processMessage(conversationId, content, attachmentIds, options, sourceChannel, sourceInterface),
333
- persistAndProcessMessage: (conversationId, content, attachmentIds, options, sourceChannel, sourceInterface) =>
334
- server.persistAndProcessMessage(conversationId, content, attachmentIds, options, sourceChannel, sourceInterface),
390
+ processMessage: (
391
+ conversationId,
392
+ content,
393
+ attachmentIds,
394
+ options,
395
+ sourceChannel,
396
+ sourceInterface,
397
+ ) =>
398
+ server.processMessage(
399
+ conversationId,
400
+ content,
401
+ attachmentIds,
402
+ options,
403
+ sourceChannel,
404
+ sourceInterface,
405
+ ),
406
+ persistAndProcessMessage: (
407
+ conversationId,
408
+ content,
409
+ attachmentIds,
410
+ options,
411
+ sourceChannel,
412
+ sourceInterface,
413
+ ) =>
414
+ server.persistAndProcessMessage(
415
+ conversationId,
416
+ content,
417
+ attachmentIds,
418
+ options,
419
+ sourceChannel,
420
+ sourceInterface,
421
+ ),
335
422
  interfacesDir: getInterfacesDir(),
336
423
  approvalCopyGenerator: createApprovalCopyGenerator(),
337
424
  approvalConversationGenerator: createApprovalConversationGenerator(),
338
425
  guardianActionCopyGenerator: createGuardianActionCopyGenerator(),
339
- guardianFollowUpConversationGenerator: createGuardianFollowUpConversationGenerator(),
426
+ guardianFollowUpConversationGenerator:
427
+ createGuardianFollowUpConversationGenerator(),
340
428
  sendMessageDeps: {
341
429
  getOrCreateSession: (conversationId) =>
342
430
  server.getSessionForMessages(conversationId),
@@ -349,6 +437,7 @@ export async function runDaemon(): Promise<void> {
349
437
  data: a.dataBase64,
350
438
  })),
351
439
  },
440
+ findSession: (sessionId) => server.findSession(sessionId),
352
441
  });
353
442
 
354
443
  // Inject voice bridge deps BEFORE attempting to start the HTTP server.
@@ -364,138 +453,174 @@ export async function runDaemon(): Promise<void> {
364
453
  data: a.dataBase64,
365
454
  })),
366
455
  deriveDefaultStrictSideEffects: (conversationId) => {
367
- const threadType = conversationStore.getConversationThreadType(conversationId);
368
- return threadType === 'private';
456
+ const threadType =
457
+ conversationStore.getConversationThreadType(conversationId);
458
+ return threadType === "private";
369
459
  },
370
460
  });
371
461
  try {
372
462
  await runtimeHttp.start();
373
463
  setRelayBroadcast((msg) => server.broadcast(msg));
374
- setPointerMessageProcessor(async (conversationId, instruction, requiredFacts) => {
375
- const session = await server.getSessionForMessages(conversationId);
376
-
377
- // Constrain pointer generation to a tool-disabled path so call-
378
- // status events cannot trigger unintended side-effect tools.
379
- // Incrementing toolsDisabledDepth causes the resolveTools callback
380
- // to return an empty tool list, preventing the LLM from seeing or
381
- // invoking any tools during the pointer agent loop.
382
- //
383
- // A depth counter (rather than a boolean) ensures that overlapping
384
- // pointer requests on the same session don't clear each other's
385
- // constraint — each caller increments on entry and decrements in
386
- // its own finally block.
387
- session.toolsDisabledDepth++;
388
- try {
389
- const messageId = await session.persistUserMessage(
390
- instruction,
391
- [],
392
- undefined,
393
- { pointerInstruction: true },
394
- '[Call status event]',
395
- );
396
-
397
- // Helper: roll back persisted messages on failure, then reload
398
- // in-memory history from the (now cleaned) DB. Reloading avoids
399
- // stale-index issues when context compaction reassigns the
400
- // messages array during runAgentLoop.
401
- const rollback = async (extraMessageIds?: string[]) => {
402
- try { conversationStore.deleteMessageById(messageId); } catch { /* best effort */ }
403
- for (const id of extraMessageIds ?? []) {
404
- try { conversationStore.deleteMessageById(id); } catch { /* best effort */ }
405
- }
406
- try { await session.loadFromDb(); } catch { /* best effort */ }
407
- };
408
-
409
- // Snapshot message IDs before the agent loop so we can diff
410
- // afterwards to find exactly which messages this run created,
411
- // avoiding positional heuristics that break under concurrency.
464
+ setPointerMessageProcessor(
465
+ async (conversationId, instruction, requiredFacts) => {
466
+ const session = await server.getSessionForMessages(conversationId);
467
+
468
+ // Constrain pointer generation to a tool-disabled path so call-
469
+ // status events cannot trigger unintended side-effect tools.
470
+ // Incrementing toolsDisabledDepth causes the resolveTools callback
471
+ // to return an empty tool list, preventing the LLM from seeing or
472
+ // invoking any tools during the pointer agent loop.
412
473
  //
413
- // Caveat: the diff captures *all* new messages in the
414
- // conversation during the loop window, not just those from
415
- // this specific agent loop. If a concurrent pointer event
416
- // falls back to a deterministic addMessage() while our loop
417
- // is in flight, that message lands in our diff. The race
418
- // requires two pointer events for the same conversation
419
- // within the agent loop window *and* this run must fail or
420
- // fail fact-check — narrow enough to accept. A future
421
- // improvement could tag messages with a per-run correlation
422
- // ID so rollback only targets its own output.
423
- const preRunMessageIds = new Set(
424
- conversationStore.getMessages(conversationId).map((m) => m.id),
425
- );
426
-
427
- let agentLoopError: string | undefined;
428
- let generatedText = '';
429
- await session.runAgentLoop(instruction, messageId, (msg) => {
430
- if ('type' in msg && msg.type === 'assistant_text_delta' && 'text' in msg) {
431
- generatedText += (msg as { text: string }).text;
432
- }
433
- if ('type' in msg && (msg.type === 'error' || msg.type === 'session_error')) {
434
- agentLoopError = 'message' in msg
435
- ? (msg as { message: string }).message
436
- : 'userMessage' in msg
437
- ? (msg as { userMessage: string }).userMessage
438
- : 'Agent loop failed';
474
+ // A depth counter (rather than a boolean) ensures that overlapping
475
+ // pointer requests on the same session don't clear each other's
476
+ // constraint each caller increments on entry and decrements in
477
+ // its own finally block.
478
+ session.toolsDisabledDepth++;
479
+ try {
480
+ const messageId = await session.persistUserMessage(
481
+ instruction,
482
+ [],
483
+ undefined,
484
+ { pointerInstruction: true },
485
+ "[Call status event]",
486
+ );
487
+
488
+ // Helper: roll back persisted messages on failure, then reload
489
+ // in-memory history from the (now cleaned) DB. Reloading avoids
490
+ // stale-index issues when context compaction reassigns the
491
+ // messages array during runAgentLoop.
492
+ const rollback = async (extraMessageIds?: string[]) => {
493
+ try {
494
+ conversationStore.deleteMessageById(messageId);
495
+ } catch {
496
+ /* best effort */
497
+ }
498
+ for (const id of extraMessageIds ?? []) {
499
+ try {
500
+ conversationStore.deleteMessageById(id);
501
+ } catch {
502
+ /* best effort */
503
+ }
504
+ }
505
+ try {
506
+ await session.loadFromDb();
507
+ } catch {
508
+ /* best effort */
509
+ }
510
+ };
511
+
512
+ // Snapshot message IDs before the agent loop so we can diff
513
+ // afterwards to find exactly which messages this run created,
514
+ // avoiding positional heuristics that break under concurrency.
515
+ //
516
+ // Caveat: the diff captures *all* new messages in the
517
+ // conversation during the loop window, not just those from
518
+ // this specific agent loop. If a concurrent pointer event
519
+ // falls back to a deterministic addMessage() while our loop
520
+ // is in flight, that message lands in our diff. The race
521
+ // requires two pointer events for the same conversation
522
+ // within the agent loop window *and* this run must fail or
523
+ // fail fact-check — narrow enough to accept. A future
524
+ // improvement could tag messages with a per-run correlation
525
+ // ID so rollback only targets its own output.
526
+ const preRunMessageIds = new Set(
527
+ conversationStore.getMessages(conversationId).map((m) => m.id),
528
+ );
529
+
530
+ let agentLoopError: string | undefined;
531
+ let generatedText = "";
532
+ await session.runAgentLoop(instruction, messageId, (msg) => {
533
+ if (
534
+ "type" in msg &&
535
+ msg.type === "assistant_text_delta" &&
536
+ "text" in msg
537
+ ) {
538
+ generatedText += (msg as { text: string }).text;
539
+ }
540
+ if (
541
+ "type" in msg &&
542
+ (msg.type === "error" || msg.type === "session_error")
543
+ ) {
544
+ agentLoopError =
545
+ "message" in msg
546
+ ? (msg as { message: string }).message
547
+ : "userMessage" in msg
548
+ ? (msg as { userMessage: string }).userMessage
549
+ : "Agent loop failed";
550
+ }
551
+ });
552
+
553
+ // Identify messages created during this run by diffing against
554
+ // the pre-run snapshot. This captures all messages added to the
555
+ // conversation during the loop window, which may include messages
556
+ // from concurrent pointer events (see over-capture caveat above).
557
+ const postRunMessages =
558
+ conversationStore.getMessages(conversationId);
559
+ const createdMessageIds = postRunMessages
560
+ .filter((m) => !preRunMessageIds.has(m.id) && m.id !== messageId)
561
+ .map((m) => m.id);
562
+
563
+ if (agentLoopError) {
564
+ await rollback(createdMessageIds);
565
+ throw new Error(agentLoopError);
439
566
  }
440
- });
441
-
442
- // Identify messages created during this run by diffing against
443
- // the pre-run snapshot. This captures all messages added to the
444
- // conversation during the loop window, which may include messages
445
- // from concurrent pointer events (see over-capture caveat above).
446
- const postRunMessages = conversationStore.getMessages(conversationId);
447
- const createdMessageIds = postRunMessages
448
- .filter((m) => !preRunMessageIds.has(m.id) && m.id !== messageId)
449
- .map((m) => m.id);
450
-
451
- if (agentLoopError) {
452
- await rollback(createdMessageIds);
453
- throw new Error(agentLoopError);
454
- }
455
567
 
456
- // Post-generation fact check: verify the assistant's response
457
- // includes all required factual details (phone number, duration,
458
- // outcome keyword, etc.). If the model omitted or rewrote them,
459
- // remove both the instruction and generated messages and throw so
460
- // the deterministic fallback fires.
461
- //
462
- // Validation uses text accumulated from assistant_text_delta
463
- // events during the agent loop rather than a DB lookup, avoiding
464
- // any positional ambiguity when concurrent pointer events
465
- // interleave messages in the conversation.
466
- if (requiredFacts && requiredFacts.length > 0) {
467
- const missingFacts = requiredFacts.filter((fact) => !generatedText.includes(fact));
468
- if (missingFacts.length > 0) {
469
- log.warn(
470
- { conversationId, missingFacts },
471
- 'Generated pointer text failed fact validation — falling back to deterministic',
568
+ // Post-generation fact check: verify the assistant's response
569
+ // includes all required factual details (phone number, duration,
570
+ // outcome keyword, etc.). If the model omitted or rewrote them,
571
+ // remove both the instruction and generated messages and throw so
572
+ // the deterministic fallback fires.
573
+ //
574
+ // Validation uses text accumulated from assistant_text_delta
575
+ // events during the agent loop rather than a DB lookup, avoiding
576
+ // any positional ambiguity when concurrent pointer events
577
+ // interleave messages in the conversation.
578
+ if (requiredFacts && requiredFacts.length > 0) {
579
+ const missingFacts = requiredFacts.filter(
580
+ (fact) => !generatedText.includes(fact),
472
581
  );
473
- await rollback(createdMessageIds);
474
- throw new Error('Generated pointer text failed fact validation');
582
+ if (missingFacts.length > 0) {
583
+ log.warn(
584
+ { conversationId, missingFacts },
585
+ "Generated pointer text failed fact validation — falling back to deterministic",
586
+ );
587
+ await rollback(createdMessageIds);
588
+ throw new Error(
589
+ "Generated pointer text failed fact validation",
590
+ );
591
+ }
475
592
  }
593
+ } finally {
594
+ // Restore tool availability so subsequent turns aren't affected.
595
+ session.toolsDisabledDepth--;
476
596
  }
477
- } finally {
478
- // Restore tool availability so subsequent turns aren't affected.
479
- session.toolsDisabledDepth--;
480
- }
481
- });
482
- runtimeHttp.setPairingBroadcast((msg) => server.broadcast(msg as ServerMessage));
597
+ },
598
+ );
599
+ runtimeHttp.setPairingBroadcast((msg) =>
600
+ server.broadcast(msg as ServerMessage),
601
+ );
483
602
  initPairingHandlers(runtimeHttp.getPairingStore(), bearerToken);
484
603
  initSlashPairingContext(runtimeHttp.getPairingStore());
485
604
  server.setHttpPort(httpPort);
486
- log.info({ port: httpPort, hostname }, 'Daemon startup: runtime HTTP server listening');
605
+ log.info(
606
+ { port: httpPort, hostname },
607
+ "Daemon startup: runtime HTTP server listening",
608
+ );
487
609
  } catch (err) {
488
- log.warn({ err, port: httpPort }, 'Failed to start runtime HTTP server, continuing without it');
610
+ log.warn(
611
+ { err, port: httpPort },
612
+ "Failed to start runtime HTTP server, continuing without it",
613
+ );
489
614
  runtimeHttp = null;
490
615
  }
491
616
 
492
617
  writePid(process.pid);
493
- log.info({ pid: process.pid }, 'Daemon started');
618
+ log.info({ pid: process.pid }, "Daemon started");
494
619
 
495
620
  const hookManager = getHookManager();
496
621
  hookManager.watch();
497
622
 
498
- void hookManager.trigger('daemon-start', {
623
+ void hookManager.trigger("daemon-start", {
499
624
  pid: process.pid,
500
625
  socketPath: getSocketPath(),
501
626
  });
@@ -504,18 +629,23 @@ export async function runDaemon(): Promise<void> {
504
629
  // If download fails, local embeddings gracefully fall back to cloud backends.
505
630
  void (async () => {
506
631
  try {
507
- const { EmbeddingRuntimeManager } = await import('../memory/embedding-runtime-manager.js');
632
+ const { EmbeddingRuntimeManager } =
633
+ await import("../memory/embedding-runtime-manager.js");
508
634
  const runtimeManager = new EmbeddingRuntimeManager();
509
635
  if (!runtimeManager.isReady()) {
510
- log.info('Downloading embedding runtime in background...');
636
+ log.info("Downloading embedding runtime in background...");
511
637
  await runtimeManager.ensureInstalled();
512
638
  // Reset the localBackendBroken flag so auto mode retries local embeddings
513
- const { clearEmbeddingBackendCache } = await import('../memory/embedding-backend.js');
639
+ const { clearEmbeddingBackendCache } =
640
+ await import("../memory/embedding-backend.js");
514
641
  clearEmbeddingBackendCache();
515
- log.info('Embedding runtime download complete');
642
+ log.info("Embedding runtime download complete");
516
643
  }
517
644
  } catch (err) {
518
- log.warn({ err }, 'Embedding runtime download failed — local embeddings will use cloud fallback');
645
+ log.warn(
646
+ { err },
647
+ "Embedding runtime download failed — local embeddings will use cloud fallback",
648
+ );
519
649
  }
520
650
  })();
521
651
 
@@ -523,7 +653,7 @@ export async function runDaemon(): Promise<void> {
523
653
  try {
524
654
  rotateToolInvocations(config.auditLog.retentionDays);
525
655
  } catch (err) {
526
- log.warn({ err }, 'Audit log rotation failed');
656
+ log.warn({ err }, "Audit log rotation failed");
527
657
  }
528
658
  }
529
659
 
@@ -538,13 +668,20 @@ export async function runDaemon(): Promise<void> {
538
668
  });
539
669
  heartbeat.start();
540
670
  server.setHeartbeatService(heartbeat);
541
- log.info({ enabled: heartbeatConfig.enabled, intervalMs: heartbeatConfig.intervalMs }, 'Heartbeat service configured');
671
+ log.info(
672
+ {
673
+ enabled: heartbeatConfig.enabled,
674
+ intervalMs: heartbeatConfig.intervalMs,
675
+ },
676
+ "Heartbeat service configured",
677
+ );
542
678
 
543
679
  // Retrieve the MCP manager if MCP servers were configured.
544
680
  // The manager is a singleton created during initializeProvidersAndTools().
545
- const mcpManager = config.mcp?.servers && Object.keys(config.mcp.servers).length > 0
546
- ? getMcpServerManager()
547
- : null;
681
+ const mcpManager =
682
+ config.mcp?.servers && Object.keys(config.mcp.servers).length > 0
683
+ ? getMcpServerManager()
684
+ : null;
548
685
 
549
686
  installShutdownHandlers({
550
687
  server,
@@ -559,7 +696,7 @@ export async function runDaemon(): Promise<void> {
559
696
  cleanupPidFile,
560
697
  });
561
698
  } catch (err) {
562
- log.error({ err }, 'Daemon startup failed — cleaning up');
699
+ log.error({ err }, "Daemon startup failed — cleaning up");
563
700
  cleanupPidFileIfOwner(process.pid);
564
701
  if (socketCreated) {
565
702
  try {