@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
@@ -11,6 +11,7 @@
11
11
  import { answerCall, cancelCall, getCallStatus, relayInstruction,startCall } from '../../calls/call-domain.js';
12
12
  import { getConfig } from '../../config/loader.js';
13
13
  import { VALID_CALLER_IDENTITY_MODES } from '../../config/schema.js';
14
+ import { httpError, httpErrorCodeFromStatus } from '../http-errors.js';
14
15
 
15
16
  // ── Idempotency cache ─────────────────────────────────────────────────────────
16
17
  // Stores serialized 201 responses keyed by idempotencyKey for 5 minutes so
@@ -42,10 +43,7 @@ function pruneIdempotencyCache(): void {
42
43
  */
43
44
  export async function handleStartCall(req: Request, assistantId: string = 'self'): Promise<Response> {
44
45
  if (!getConfig().calls.enabled) {
45
- return Response.json(
46
- { error: 'Calls feature is disabled via configuration. Set calls.enabled to true to use this feature.' },
47
- { status: 403 },
48
- );
46
+ return httpError('FORBIDDEN', 'Calls feature is disabled via configuration. Set calls.enabled to true to use this feature.', 403);
49
47
  }
50
48
 
51
49
  let body: {
@@ -59,23 +57,20 @@ export async function handleStartCall(req: Request, assistantId: string = 'self'
59
57
  try {
60
58
  body = await req.json() as typeof body;
61
59
  } catch {
62
- return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
60
+ return httpError('BAD_REQUEST', 'Invalid JSON in request body', 400);
63
61
  }
64
62
 
65
63
  if (typeof body !== 'object' || body == null || Array.isArray(body)) {
66
- return Response.json({ error: 'Request body must be a JSON object' }, { status: 400 });
64
+ return httpError('BAD_REQUEST', 'Request body must be a JSON object', 400);
67
65
  }
68
66
 
69
67
  if (!body.conversationId) {
70
- return Response.json({ error: 'conversationId is required' }, { status: 400 });
68
+ return httpError('BAD_REQUEST', 'conversationId is required', 400);
71
69
  }
72
70
 
73
71
  if (body.callerIdentityMode != null &&
74
72
  !(VALID_CALLER_IDENTITY_MODES as readonly string[]).includes(body.callerIdentityMode as string)) {
75
- return Response.json(
76
- { error: `Invalid callerIdentityMode: "${body.callerIdentityMode}". Must be one of: ${VALID_CALLER_IDENTITY_MODES.join(', ')}` },
77
- { status: 400 },
78
- );
73
+ return httpError('BAD_REQUEST', `Invalid callerIdentityMode: "${body.callerIdentityMode}". Must be one of: ${VALID_CALLER_IDENTITY_MODES.join(', ')}`, 400);
79
74
  }
80
75
 
81
76
  // Idempotency check: return cached response for duplicate requests
@@ -101,7 +96,8 @@ export async function handleStartCall(req: Request, assistantId: string = 'self'
101
96
  });
102
97
 
103
98
  if (!result.ok) {
104
- return Response.json({ error: result.error }, { status: result.status ?? 500 });
99
+ const status = result.status ?? 500;
100
+ return httpError(httpErrorCodeFromStatus(status), result.error, status);
105
101
  }
106
102
 
107
103
  const responseBody = {
@@ -127,7 +123,8 @@ export function handleGetCallStatus(callSessionId: string): Response {
127
123
  const result = getCallStatus(callSessionId);
128
124
 
129
125
  if (!result.ok) {
130
- return Response.json({ error: result.error }, { status: result.status ?? 500 });
126
+ const status = result.status ?? 500;
127
+ return httpError(httpErrorCodeFromStatus(status), result.error, status);
131
128
  }
132
129
 
133
130
  const { session } = result;
@@ -166,7 +163,8 @@ export async function handleCancelCall(req: Request, callSessionId: string): Pro
166
163
  const result = await cancelCall({ callSessionId, reason });
167
164
 
168
165
  if (!result.ok) {
169
- return Response.json({ error: result.error }, { status: result.status ?? 500 });
166
+ const status = result.status ?? 500;
167
+ return httpError(httpErrorCodeFromStatus(status), result.error, status);
170
168
  }
171
169
 
172
170
  return Response.json({
@@ -185,11 +183,11 @@ export async function handleAnswerCall(req: Request, callSessionId: string): Pro
185
183
  try {
186
184
  body = await req.json() as typeof body;
187
185
  } catch {
188
- return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
186
+ return httpError('BAD_REQUEST', 'Invalid JSON in request body', 400);
189
187
  }
190
188
 
191
189
  if (typeof body !== 'object' || body == null || Array.isArray(body)) {
192
- return Response.json({ error: 'Request body must be a JSON object' }, { status: 400 });
190
+ return httpError('BAD_REQUEST', 'Request body must be a JSON object', 400);
193
191
  }
194
192
 
195
193
  const result = await answerCall({
@@ -198,7 +196,8 @@ export async function handleAnswerCall(req: Request, callSessionId: string): Pro
198
196
  });
199
197
 
200
198
  if (!result.ok) {
201
- return Response.json({ error: result.error }, { status: result.status ?? 500 });
199
+ const status = result.status ?? 500;
200
+ return httpError(httpErrorCodeFromStatus(status), result.error, status);
202
201
  }
203
202
 
204
203
  return Response.json({ ok: true, questionId: result.questionId });
@@ -214,11 +213,11 @@ export async function handleInstructionCall(req: Request, callSessionId: string)
214
213
  try {
215
214
  body = await req.json() as typeof body;
216
215
  } catch {
217
- return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
216
+ return httpError('BAD_REQUEST', 'Invalid JSON in request body', 400);
218
217
  }
219
218
 
220
219
  if (typeof body !== 'object' || body == null || Array.isArray(body)) {
221
- return Response.json({ error: 'Request body must be a JSON object' }, { status: 400 });
220
+ return httpError('BAD_REQUEST', 'Request body must be a JSON object', 400);
222
221
  }
223
222
 
224
223
  const result = await relayInstruction({
@@ -227,7 +226,8 @@ export async function handleInstructionCall(req: Request, callSessionId: string)
227
226
  });
228
227
 
229
228
  if (!result.ok) {
230
- return Response.json({ error: result.error }, { status: result.status ?? 500 });
229
+ const status = result.status ?? 500;
230
+ return httpError(httpErrorCodeFromStatus(status), result.error, status);
231
231
  }
232
232
 
233
233
  return Response.json({ ok: true });
@@ -3,6 +3,7 @@
3
3
  * and post-decision delivery scheduling.
4
4
  */
5
5
  import * as channelDeliveryStore from '../../memory/channel-delivery-store.js';
6
+ import { httpError } from '../http-errors.js';
6
7
  export { type DeliverReplyOptions,deliverReplyViaCallback } from '../channel-reply-delivery.js';
7
8
 
8
9
  // ---------------------------------------------------------------------------
@@ -19,7 +20,7 @@ export async function handleReplayDeadLetters(req: Request): Promise<Response> {
19
20
  const eventIds = body.eventIds;
20
21
 
21
22
  if (!Array.isArray(eventIds) || eventIds.length === 0) {
22
- return Response.json({ error: 'eventIds array is required' }, { status: 400 });
23
+ return httpError('BAD_REQUEST', 'eventIds array is required', 400);
23
24
  }
24
25
 
25
26
  const replayed = channelDeliveryStore.replayDeadLetters(eventIds);
@@ -40,13 +41,13 @@ export async function handleChannelDeliveryAck(req: Request): Promise<Response>
40
41
  const { sourceChannel, externalChatId, externalMessageId } = body;
41
42
 
42
43
  if (!sourceChannel || typeof sourceChannel !== 'string') {
43
- return Response.json({ error: 'sourceChannel is required' }, { status: 400 });
44
+ return httpError('BAD_REQUEST', 'sourceChannel is required', 400);
44
45
  }
45
46
  if (!externalChatId || typeof externalChatId !== 'string') {
46
- return Response.json({ error: 'externalChatId is required' }, { status: 400 });
47
+ return httpError('BAD_REQUEST', 'externalChatId is required', 400);
47
48
  }
48
49
  if (!externalMessageId || typeof externalMessageId !== 'string') {
49
- return Response.json({ error: 'externalMessageId is required' }, { status: 400 });
50
+ return httpError('BAD_REQUEST', 'externalMessageId is required', 400);
50
51
  }
51
52
 
52
53
  const acked = channelDeliveryStore.acknowledgeDelivery(
@@ -56,7 +57,7 @@ export async function handleChannelDeliveryAck(req: Request): Promise<Response>
56
57
  );
57
58
 
58
59
  if (!acked) {
59
- return Response.json({ error: 'Inbound event not found' }, { status: 404 });
60
+ return httpError('NOT_FOUND', 'Inbound event not found', 404);
60
61
  }
61
62
 
62
63
  return new Response(null, { status: 204 });
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { getContact, listContacts, mergeContacts } from '../../contacts/contact-store.js';
10
+ import { httpError } from '../http-errors.js';
10
11
 
11
12
  /**
12
13
  * GET /v1/contacts?limit=50
@@ -23,10 +24,7 @@ export function handleListContacts(url: URL): Response {
23
24
  export function handleGetContact(contactId: string): Response {
24
25
  const contact = getContact(contactId);
25
26
  if (!contact) {
26
- return Response.json(
27
- { ok: false, error: `Contact "${contactId}" not found` },
28
- { status: 404 },
29
- );
27
+ return httpError('NOT_FOUND', `Contact "${contactId}" not found`, 404);
30
28
  }
31
29
  return Response.json({ ok: true, contact });
32
30
  }
@@ -38,10 +36,7 @@ export async function handleMergeContacts(req: Request): Promise<Response> {
38
36
  const body = (await req.json()) as { keepId?: string; mergeId?: string };
39
37
 
40
38
  if (!body.keepId || !body.mergeId) {
41
- return Response.json(
42
- { ok: false, error: 'keepId and mergeId are required' },
43
- { status: 400 },
44
- );
39
+ return httpError('BAD_REQUEST', 'keepId and mergeId are required', 400);
45
40
  }
46
41
 
47
42
  try {
@@ -49,6 +44,6 @@ export async function handleMergeContacts(req: Request): Promise<Response> {
49
44
  return Response.json({ ok: true, contact });
50
45
  } catch (err) {
51
46
  const message = err instanceof Error ? err.message : String(err);
52
- return Response.json({ ok: false, error: message }, { status: 400 });
47
+ return httpError('BAD_REQUEST', message, 400);
53
48
  }
54
49
  }
@@ -10,6 +10,7 @@ import {
10
10
  } from '../../memory/conversation-attention-store.js';
11
11
  import * as conversationStore from '../../memory/conversation-store.js';
12
12
  import { truncate } from '../../util/truncate.js';
13
+ import { httpError } from '../http-errors.js';
13
14
 
14
15
  export function handleListConversationAttention(url: URL): Response {
15
16
  const stateParam = url.searchParams.get('state') ?? 'all';
@@ -22,7 +23,7 @@ export function handleListConversationAttention(url: URL): Response {
22
23
  const before = rawBefore !== undefined && Number.isFinite(rawBefore) ? rawBefore : undefined;
23
24
 
24
25
  if (!['seen', 'unseen', 'all'].includes(stateParam)) {
25
- return Response.json({ error: 'Invalid state parameter. Must be seen, unseen, or all.' }, { status: 400 });
26
+ return httpError('BAD_REQUEST', 'Invalid state parameter. Must be seen, unseen, or all.', 400);
26
27
  }
27
28
 
28
29
  const attentionStates = listConversationAttention({
@@ -61,9 +62,9 @@ export function handleListConversationAttention(url: URL): Response {
61
62
  const results = pageStates.map((attn) => {
62
63
  const conv = conversationMap.get(attn.conversationId);
63
64
  const convSource = conv?.source ?? 'user';
64
- const hasUnseen = attn.latestAssistantMessageAt !== null &&
65
- (attn.lastSeenAssistantMessageAt === null || attn.lastSeenAssistantMessageAt < attn.latestAssistantMessageAt);
66
- const state: 'seen' | 'unseen' | 'no_assistant_message' = attn.latestAssistantMessageAt === null
65
+ const hasUnseen = attn.latestAssistantMessageAt != null &&
66
+ (attn.lastSeenAssistantMessageAt == null || attn.lastSeenAssistantMessageAt < attn.latestAssistantMessageAt);
67
+ const state: 'seen' | 'unseen' | 'no_assistant_message' = attn.latestAssistantMessageAt == null
67
68
  ? 'no_assistant_message'
68
69
  : hasUnseen ? 'unseen' : 'seen';
69
70
 
@@ -17,6 +17,7 @@ import { getConfiguredProvider } from '../../providers/provider-send-message.js'
17
17
  import type { Provider } from '../../providers/types.js';
18
18
  import { getLogger } from '../../util/logger.js';
19
19
  import { buildAssistantEvent } from '../assistant-event.js';
20
+ import { httpError } from '../http-errors.js';
20
21
  import type {
21
22
  MessageProcessor,
22
23
  NonBlockingMessageProcessor,
@@ -64,10 +65,7 @@ export function handleListMessages(
64
65
  const mapping = getConversationByKey(conversationKey);
65
66
  resolvedConversationId = mapping?.conversationId;
66
67
  } else {
67
- return Response.json(
68
- { error: 'conversationKey or conversationId query parameter is required' },
69
- { status: 400 },
70
- );
68
+ return httpError('BAD_REQUEST', 'conversationKey or conversationId query parameter is required', 400);
71
69
  }
72
70
 
73
71
  if (!resolvedConversationId) {
@@ -181,10 +179,10 @@ function makeHubPublisher(
181
179
  });
182
180
  }
183
181
 
184
- const msgRecord = msg as unknown as Record<string, unknown>;
182
+ // ServerMessage is a large union; sessionId exists on most but not all variants.
185
183
  const msgSessionId =
186
- 'sessionId' in msg && typeof msgRecord.sessionId === 'string'
187
- ? (msgRecord.sessionId as string)
184
+ 'sessionId' in msg && typeof (msg as { sessionId?: unknown }).sessionId === 'string'
185
+ ? (msg as { sessionId: string }).sessionId
188
186
  : undefined;
189
187
  const resolvedSessionId = msgSessionId ?? conversationId;
190
188
  const event = buildAssistantEvent('self', msg, resolvedSessionId);
@@ -217,57 +215,36 @@ export async function handleSendMessage(
217
215
 
218
216
  const { conversationKey, content, attachmentIds } = body;
219
217
  if (!body.sourceChannel || typeof body.sourceChannel !== 'string') {
220
- return Response.json(
221
- { error: 'sourceChannel is required' },
222
- { status: 400 },
223
- );
218
+ return httpError('BAD_REQUEST', 'sourceChannel is required', 400);
224
219
  }
225
220
  const sourceChannel = parseChannelId(body.sourceChannel);
226
221
 
227
222
  if (!sourceChannel) {
228
- return Response.json(
229
- { error: `Invalid sourceChannel: ${body.sourceChannel}. Valid values: ${CHANNEL_IDS.join(', ')}` },
230
- { status: 400 },
231
- );
223
+ return httpError('BAD_REQUEST', `Invalid sourceChannel: ${body.sourceChannel}. Valid values: ${CHANNEL_IDS.join(', ')}`, 400);
232
224
  }
233
225
 
234
226
  if (!body.interface || typeof body.interface !== 'string') {
235
- return Response.json(
236
- { error: 'interface is required' },
237
- { status: 400 },
238
- );
227
+ return httpError('BAD_REQUEST', 'interface is required', 400);
239
228
  }
240
229
  const sourceInterface = parseInterfaceId(body.interface);
241
230
  if (!sourceInterface) {
242
- return Response.json(
243
- { error: `Invalid interface: ${body.interface}. Valid values: ${INTERFACE_IDS.join(', ')}` },
244
- { status: 400 },
245
- );
231
+ return httpError('BAD_REQUEST', `Invalid interface: ${body.interface}. Valid values: ${INTERFACE_IDS.join(', ')}`, 400);
246
232
  }
247
233
 
248
234
  if (!conversationKey) {
249
- return Response.json(
250
- { error: 'conversationKey is required' },
251
- { status: 400 },
252
- );
235
+ return httpError('BAD_REQUEST', 'conversationKey is required', 400);
253
236
  }
254
237
 
255
238
  // Reject non-string content values (numbers, objects, etc.)
256
239
  if (content != null && typeof content !== 'string') {
257
- return Response.json(
258
- { error: 'content must be a string' },
259
- { status: 400 },
260
- );
240
+ return httpError('BAD_REQUEST', 'content must be a string', 400);
261
241
  }
262
242
 
263
243
  const trimmedContent = typeof content === 'string' ? content.trim() : '';
264
244
  const hasAttachments = Array.isArray(attachmentIds) && attachmentIds.length > 0;
265
245
 
266
246
  if (trimmedContent.length === 0 && !hasAttachments) {
267
- return Response.json(
268
- { error: 'content or attachmentIds is required' },
269
- { status: 400 },
270
- );
247
+ return httpError('BAD_REQUEST', 'content or attachmentIds is required', 400);
271
248
  }
272
249
 
273
250
  // Validate that all attachment IDs resolve
@@ -276,10 +253,7 @@ export async function handleSendMessage(
276
253
  if (resolved.length !== attachmentIds.length) {
277
254
  const resolvedIds = new Set(resolved.map((a) => a.id));
278
255
  const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
279
- return Response.json(
280
- { error: `Attachment IDs not found: ${missing.join(', ')}` },
281
- { status: 400 },
282
- );
256
+ return httpError('BAD_REQUEST', `Attachment IDs not found: ${missing.join(', ')}`, 400);
283
257
  }
284
258
  }
285
259
 
@@ -299,6 +273,13 @@ export async function handleSendMessage(
299
273
  : [];
300
274
 
301
275
  if (session.isProcessing()) {
276
+ // If a tool confirmation is pending, auto-deny it so the agent
277
+ // can finish the current turn and process this queued message.
278
+ if (session.hasAnyPendingConfirmation()) {
279
+ session.denyAllPendingConfirmations();
280
+ pendingInteractions.removeBySession(session);
281
+ }
282
+
302
283
  // Queue the message so it's processed when the current turn completes
303
284
  const requestId = crypto.randomUUID();
304
285
  const result = session.enqueueMessage(
@@ -317,10 +298,7 @@ export async function handleSendMessage(
317
298
  { isInteractive: false },
318
299
  );
319
300
  if (result.rejected) {
320
- return Response.json(
321
- { error: 'Message queue is full. Please retry later.' },
322
- { status: 429 },
323
- );
301
+ return httpError('RATE_LIMITED', 'Message queue is full. Please retry later.', 429);
324
302
  }
325
303
  return Response.json({ accepted: true, queued: true }, { status: 202 });
326
304
  }
@@ -335,7 +313,7 @@ export async function handleSendMessage(
335
313
  assistantMessageInterface: sourceInterface,
336
314
  });
337
315
  const requestId = crypto.randomUUID();
338
- const messageId = session.persistUserMessage(content ?? '', attachments, requestId);
316
+ const messageId = await session.persistUserMessage(content ?? '', attachments, requestId);
339
317
 
340
318
  // Fire-and-forget the agent loop; events flow to the hub via onEvent.
341
319
  // Mark non-interactive so conflict clarification doesn't block the turn.
@@ -349,7 +327,7 @@ export async function handleSendMessage(
349
327
  // ── Legacy path (fallback when sendMessageDeps not wired) ───────────
350
328
  const processor = deps.persistAndProcessMessage ?? deps.processMessage;
351
329
  if (!processor) {
352
- return Response.json({ error: 'Message processing not configured' }, { status: 503 });
330
+ return httpError('SERVICE_UNAVAILABLE', 'Message processing not configured', 503);
353
331
  }
354
332
 
355
333
  try {
@@ -364,10 +342,7 @@ export async function handleSendMessage(
364
342
  return Response.json({ accepted: true, messageId: result.messageId }, { status: 202 });
365
343
  } catch (err) {
366
344
  if (err instanceof Error && err.message === 'Session is already processing a message') {
367
- return Response.json(
368
- { error: 'Session is busy processing another message. Please retry.' },
369
- { status: 409 },
370
- );
345
+ return httpError('CONFLICT', 'Session is busy processing another message. Please retry.', 409);
371
346
  }
372
347
  throw err;
373
348
  }
@@ -406,10 +381,7 @@ export async function handleGetSuggestion(
406
381
  ): Promise<Response> {
407
382
  const conversationKey = url.searchParams.get('conversationKey');
408
383
  if (!conversationKey) {
409
- return Response.json(
410
- { error: 'conversationKey query parameter is required' },
411
- { status: 400 },
412
- );
384
+ return httpError('BAD_REQUEST', 'conversationKey query parameter is required', 400);
413
385
  }
414
386
 
415
387
  const mapping = getConversationByKey(conversationKey);
@@ -517,10 +489,7 @@ export async function handleGetSuggestion(
517
489
  export function handleSearchConversations(url: URL): Response {
518
490
  const query = url.searchParams.get('q') ?? '';
519
491
  if (!query.trim()) {
520
- return Response.json(
521
- { error: 'q query parameter is required' },
522
- { status: 400 },
523
- );
492
+ return httpError('BAD_REQUEST', 'q query parameter is required', 400);
524
493
  }
525
494
 
526
495
  const limit = url.searchParams.has('limit')
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Debug introspection endpoint for monitoring and troubleshooting.
3
+ */
4
+
5
+ import { statSync } from 'node:fs';
6
+
7
+ import { getDbPath } from '../../util/platform.js';
8
+ import { countConversations } from '../../memory/conversation-store.js';
9
+ import { getMemoryJobCounts } from '../../memory/jobs-store.js';
10
+ import { countSchedules } from '../../schedule/schedule-store.js';
11
+ import { rawAll } from '../../memory/db.js';
12
+ import { getConfig } from '../../config/loader.js';
13
+ import { getProviderDebugStatus } from '../../providers/registry.js';
14
+
15
+ /** Process start time — used to calculate uptime. */
16
+ const startedAt = Date.now();
17
+
18
+ function getDatabaseSizeBytes(): number | null {
19
+ try {
20
+ return statSync(getDbPath()).size;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function getMemoryItemCount(): number {
27
+ try {
28
+ const rows = rawAll<{ c: number }>('SELECT COUNT(*) AS c FROM memory_items');
29
+ return rows[0]?.c ?? 0;
30
+ } catch {
31
+ return 0;
32
+ }
33
+ }
34
+
35
+ export function handleDebug(): Response {
36
+ const now = Date.now();
37
+ const uptimeSeconds = Math.floor((now - startedAt) / 1000);
38
+
39
+ const conversationCount = countConversations();
40
+ const memoryItemCount = getMemoryItemCount();
41
+ const dbSizeBytes = getDatabaseSizeBytes();
42
+
43
+ const memoryJobCounts = getMemoryJobCounts();
44
+
45
+ const scheduleCounts = countSchedules();
46
+
47
+ const config = getConfig();
48
+ const providerOrder = Array.isArray(config.providerOrder) ? config.providerOrder : [];
49
+ const providerStatus = getProviderDebugStatus(config.provider, providerOrder);
50
+
51
+ return Response.json({
52
+ session: {
53
+ uptimeSeconds,
54
+ startedAt: new Date(startedAt).toISOString(),
55
+ },
56
+ provider: providerStatus,
57
+ memory: {
58
+ conversationCount,
59
+ memoryItemCount,
60
+ ...(dbSizeBytes != null ? { databaseSizeBytes: dbSizeBytes } : {}),
61
+ },
62
+ jobs: {
63
+ memory: memoryJobCounts,
64
+ },
65
+ schedules: {
66
+ total: scheduleCounts.total,
67
+ enabled: scheduleCounts.enabled,
68
+ },
69
+ timestamp: new Date(now).toISOString(),
70
+ });
71
+ }
@@ -11,6 +11,7 @@ import { getOrCreateConversation } from '../../memory/conversation-key-store.js'
11
11
  import { formatSseFrame, formatSseHeartbeat } from '../assistant-event.js';
12
12
  import type { AssistantEventSubscription } from '../assistant-event-hub.js';
13
13
  import { AssistantEventHub,assistantEventHub } from '../assistant-event-hub.js';
14
+ import { httpError } from '../http-errors.js';
14
15
 
15
16
  /** Keep-alive comment sent to idle clients every 30 s by default. */
16
17
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
@@ -35,7 +36,7 @@ export function handleSubscribeAssistantEvents(
35
36
  ): Response {
36
37
  const conversationKey = url.searchParams.get('conversationKey');
37
38
  if (!conversationKey) {
38
- return Response.json({ error: 'conversationKey is required' }, { status: 400 });
39
+ return httpError('BAD_REQUEST', 'conversationKey is required', 400);
39
40
  }
40
41
 
41
42
  const hub = options?.hub ?? assistantEventHub;
@@ -88,7 +89,7 @@ export function handleSubscribeAssistantEvents(
88
89
  );
89
90
  } catch (err) {
90
91
  if (err instanceof RangeError) {
91
- return Response.json({ error: 'Too many concurrent connections' }, { status: 503 });
92
+ return httpError('SERVICE_UNAVAILABLE', 'Too many concurrent connections', 503);
92
93
  }
93
94
  throw err;
94
95
  }