@vellumai/assistant 0.11.8 → 0.11.9-staging.2

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 (326) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +8 -48
  3. package/docker-entrypoint.sh +5 -1
  4. package/docker-kata-apt-env.sh +3 -0
  5. package/docker-kata-apt-shims.sh +127 -0
  6. package/docker-kata-apt-wrapper.sh +45 -0
  7. package/docker-kata-chroot-exec.sh +35 -0
  8. package/docker-kata-pip.sh +8 -2
  9. package/docs/architecture/memory.md +8 -4
  10. package/docs/guardian-request-flow.md +18 -14
  11. package/docs/trusted-contact-access.md +1 -7
  12. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  13. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  25. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  26. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  28. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  29. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  30. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  31. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  32. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  35. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  36. package/openapi.yaml +193 -4
  37. package/package.json +2 -2
  38. package/src/__tests__/access-request-card-view.test.ts +6 -5
  39. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  40. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  42. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  43. package/src/__tests__/attachments-store.test.ts +27 -0
  44. package/src/__tests__/attachments.test.ts +43 -0
  45. package/src/__tests__/canned-reply-release.test.ts +48 -5
  46. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  47. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  48. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  50. package/src/__tests__/conversation-error.test.ts +15 -0
  51. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  52. package/src/__tests__/conversation-pairing.test.ts +141 -1
  53. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  54. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  56. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  57. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  58. package/src/__tests__/credential-health-service.test.ts +98 -10
  59. package/src/__tests__/delete-propagation.test.ts +469 -0
  60. package/src/__tests__/dm-persistence.test.ts +16 -0
  61. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  62. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  63. package/src/__tests__/gemini-provider.test.ts +46 -0
  64. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  65. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  66. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  67. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  68. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  69. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  70. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  71. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  72. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  73. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  74. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  75. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  76. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  77. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  78. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  79. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  80. package/src/__tests__/openai-provider.test.ts +5 -5
  81. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  82. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  83. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  84. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  85. package/src/__tests__/pricing.test.ts +11 -0
  86. package/src/__tests__/process-message-display-content.test.ts +20 -9
  87. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  88. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  89. package/src/__tests__/reaction-persistence.test.ts +22 -198
  90. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  91. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  92. package/src/__tests__/skill-load-tool.test.ts +27 -0
  93. package/src/__tests__/skills.test.ts +34 -1
  94. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  95. package/src/__tests__/terminal-tools.test.ts +9 -0
  96. package/src/__tests__/thread-backfill.test.ts +5 -3
  97. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  98. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  99. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  100. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  101. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  102. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  103. package/src/agent/attachments.ts +13 -2
  104. package/src/agent/loop.ts +3 -19
  105. package/src/api/README.md +9 -5
  106. package/src/api/index.ts +9 -8
  107. package/src/api/package.json +1 -0
  108. package/src/api/responses/conversation-message.ts +8 -0
  109. package/src/api/responses/home.ts +7 -18
  110. package/src/api/surfaces.ts +114 -7
  111. package/src/approvals/AGENTS.md +1 -1
  112. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  113. package/src/channels/__tests__/types.test.ts +22 -1
  114. package/src/channels/gateway-guardian-requests.ts +0 -22
  115. package/src/channels/types.ts +8 -6
  116. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  117. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  118. package/src/cli/commands/channels/index.help.ts +19 -6
  119. package/src/cli/commands/channels/index.ts +6 -2
  120. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  121. package/src/cli/commands/db/index.help.ts +1 -1
  122. package/src/cli/commands/db/status.ts +172 -1
  123. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  124. package/src/cli/commands/platform/connect.ts +14 -5
  125. package/src/cli/commands/plugins.help.ts +6 -5
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  127. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  128. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  129. package/src/cli/lib/bundled-marketplace.json +14 -0
  130. package/src/cli/lib/install-from-github.ts +9 -5
  131. package/src/cli/lib/install-from-platform.ts +12 -1
  132. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  133. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  134. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  135. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  136. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  137. package/src/config/call-site-defaults.ts +4 -1
  138. package/src/config/feature-flag-registry.json +21 -4
  139. package/src/config/schemas/memory-v3.ts +4 -3
  140. package/src/context/strip-injections.ts +17 -56
  141. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  142. package/src/conversations/message-consolidation.ts +4 -3
  143. package/src/credential-health/credential-health-service.ts +130 -0
  144. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  145. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  146. package/src/daemon/conversation-error.ts +2 -0
  147. package/src/daemon/conversation-messaging.ts +195 -47
  148. package/src/daemon/conversation-process.ts +20 -5
  149. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  150. package/src/daemon/conversation-store.ts +159 -11
  151. package/src/daemon/conversation-tool-setup.ts +9 -2
  152. package/src/daemon/conversation.ts +290 -19
  153. package/src/daemon/dictation-text-processing.ts +2 -7
  154. package/src/daemon/handlers/config-channels.ts +33 -3
  155. package/src/daemon/handlers/config-model.test.ts +1 -0
  156. package/src/daemon/handlers/shared.ts +9 -0
  157. package/src/daemon/port-oversized-content.test.ts +117 -0
  158. package/src/daemon/port-oversized-content.ts +110 -0
  159. package/src/daemon/process-message.ts +87 -16
  160. package/src/daemon/reaction-record.test.ts +23 -11
  161. package/src/daemon/reaction-record.ts +6 -9
  162. package/src/documents/document-store.ts +1 -5
  163. package/src/home/conversation-starter-validation.ts +1 -5
  164. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  165. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  166. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  167. package/src/live-voice/live-voice-photo.ts +544 -61
  168. package/src/live-voice/live-voice-session.ts +6 -2
  169. package/src/live-voice/protocol.ts +20 -1
  170. package/src/messaging/provider-message-metadata.ts +40 -1
  171. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  172. package/src/messaging/providers/channel-transport.ts +28 -0
  173. package/src/messaging/providers/discord/send.ts +3 -2
  174. package/src/messaging/providers/slack/api.ts +11 -1
  175. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  176. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  177. package/src/messaging/providers/slack/send.test.ts +58 -0
  178. package/src/messaging/providers/slack/send.ts +4 -4
  179. package/src/messaging/providers/slack/transport.ts +28 -2
  180. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  181. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  182. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  183. package/src/messaging/reaction-envelopes.test.ts +73 -0
  184. package/src/messaging/reaction-envelopes.ts +33 -5
  185. package/src/messaging/read-provider-metadata.ts +4 -3
  186. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  187. package/src/monitoring/recovery/db.ts +35 -0
  188. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  189. package/src/monitoring/recovery/run-recovery.ts +6 -2
  190. package/src/monitoring/recovery/stale-processing.ts +3 -20
  191. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  192. package/src/notifications/AGENTS.md +2 -2
  193. package/src/notifications/README.md +2 -2
  194. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  195. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  196. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  197. package/src/notifications/access-request-copy.ts +126 -114
  198. package/src/notifications/adapters/discord.ts +9 -5
  199. package/src/notifications/adapters/shared.ts +17 -4
  200. package/src/notifications/adapters/slack.ts +14 -4
  201. package/src/notifications/adapters/telegram.ts +8 -4
  202. package/src/notifications/approval-card-data.ts +5 -2
  203. package/src/notifications/assistant-reply-producer.ts +7 -7
  204. package/src/notifications/broadcaster.ts +57 -25
  205. package/src/notifications/conversation-pairing.ts +68 -2
  206. package/src/notifications/copy-composer.ts +13 -32
  207. package/src/notifications/decision-engine.ts +56 -237
  208. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  209. package/src/notifications/guardian-feed-projection.ts +0 -15
  210. package/src/notifications/guardian-question-mode.ts +147 -6
  211. package/src/notifications/notification-utils.ts +117 -1
  212. package/src/permissions/confirmation-guardian-request.ts +2 -2
  213. package/src/permissions/prompter.ts +1 -1
  214. package/src/persistence/conversation-crud.ts +93 -14
  215. package/src/persistence/conversation-queries.ts +144 -17
  216. package/src/persistence/conversation-types.ts +39 -5
  217. package/src/persistence/delivery-crud.ts +224 -28
  218. package/src/persistence/delivery-status.ts +21 -2
  219. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  220. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  221. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  222. package/src/persistence/schema/conversations.ts +71 -2
  223. package/src/persistence/schema-contract.test.ts +78 -0
  224. package/src/persistence/schema-contract.ts +96 -0
  225. package/src/persistence/steps.ts +4 -0
  226. package/src/platform/client.ts +55 -0
  227. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  228. package/src/plugins/defaults/injector-order.ts +2 -1
  229. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  230. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  231. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  232. package/src/plugins/defaults/memory/injectors.ts +1 -1
  233. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  234. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  235. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  236. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  237. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  238. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  239. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  240. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  241. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  242. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  243. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  244. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  245. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  246. package/src/plugins/mcp-servers.ts +14 -2
  247. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  248. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  249. package/src/providers/anthropic/client.ts +21 -24
  250. package/src/providers/call-site-routing.ts +5 -4
  251. package/src/providers/connection-resolution.ts +29 -1
  252. package/src/providers/content-block-size.test.ts +82 -0
  253. package/src/providers/content-block-size.ts +99 -0
  254. package/src/providers/file-block-text.test.ts +64 -0
  255. package/src/providers/file-block-text.ts +28 -0
  256. package/src/providers/gemini/client.ts +15 -9
  257. package/src/providers/inference/auth.ts +6 -6
  258. package/src/providers/media-resolve.ts +14 -0
  259. package/src/providers/model-catalog.ts +47 -34
  260. package/src/providers/openai/chat-completions-provider.ts +9 -25
  261. package/src/providers/openai/responses-provider.ts +11 -18
  262. package/src/providers/registry.ts +10 -7
  263. package/src/providers/routing-identity.ts +2 -1
  264. package/src/providers/types.ts +1 -4
  265. package/src/providers/vellum-model-routing.ts +3 -2
  266. package/src/runtime/AGENTS.md +40 -15
  267. package/src/runtime/approval-message-composer.ts +1 -6
  268. package/src/runtime/assistant-event-hub.ts +2 -39
  269. package/src/runtime/channel-approval-types.ts +1 -5
  270. package/src/runtime/channel-reply-delivery.ts +182 -115
  271. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  272. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  273. package/src/runtime/channel-retry-sweep.ts +102 -1
  274. package/src/runtime/finalize-event-delivery.ts +4 -4
  275. package/src/runtime/guardian-action-message-composer.ts +1 -4
  276. package/src/runtime/guardian-reply-router.ts +4 -75
  277. package/src/runtime/http-router.ts +1 -5
  278. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  279. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  280. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  281. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  282. package/src/runtime/routes/acp-routes.ts +1 -1
  283. package/src/runtime/routes/canned-reply-release.ts +19 -9
  284. package/src/runtime/routes/channel-route-shared.ts +0 -39
  285. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  286. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  287. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  288. package/src/runtime/routes/conversation-routes.ts +138 -59
  289. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  290. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  291. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  292. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  294. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  295. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  296. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  297. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  298. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  299. package/src/runtime/routes/index.ts +2 -0
  300. package/src/runtime/routes/platform-routes.ts +99 -10
  301. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  302. package/src/runtime/sync/resource-sync-events.ts +14 -37
  303. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  304. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  305. package/src/tools/client-os.ts +11 -1
  306. package/src/tools/host-filesystem/edit.ts +2 -2
  307. package/src/tools/host-filesystem/read.ts +2 -2
  308. package/src/tools/host-filesystem/transfer.ts +2 -2
  309. package/src/tools/host-filesystem/write.ts +2 -2
  310. package/src/tools/host-terminal/host-shell.ts +2 -2
  311. package/src/tools/skills/load.ts +1 -1
  312. package/src/tools/terminal/safe-env.ts +4 -0
  313. package/src/tools/tool-input-schemas.ts +2 -0
  314. package/src/tools/tool-manifest.ts +2 -0
  315. package/src/tools/ui-surface/definitions.ts +4 -3
  316. package/src/tools/watch/watch-retro-report.ts +205 -0
  317. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  318. package/src/watch/watch-retro.ts +190 -77
  319. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  320. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  321. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  322. package/src/workspace/migrations/registry.ts +4 -0
  323. package/docker-kata-pip-chroot.sh +0 -22
  324. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  325. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  326. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -7,7 +7,7 @@ The end-to-end lifecycle of interactive guardian requests (approvals, questions)
7
7
  - **Rich delivery failures must degrade gracefully.** If delivering a rich approval prompt (e.g., Telegram inline buttons) fails, fall back to plain text with instructions (e.g., `Reply "yes" to approve`) — never auto-deny.
8
8
  - **Non-rich channels** (http-api) receive plain-text approval prompts. The conversational approval engine handles free-text responses.
9
9
  - **Race conditions:** Always check whether a decision has already been resolved before delivering the engine's optimistic reply. If `handleChannelDecision` returns `applied: false`, deliver an "already resolved" notice and return `stale_ignored`.
10
- - **Unified guardian decision primitive:** All guardian decision paths (callback buttons, conversational engine, channel reactions and text) must route through `applyGuardianDecision()` in `assistant/src/approvals/guardian-decision-primitive.ts`. Do not inline decision logic (CAS resolution, resolver dispatch, grant minting) at individual callsites.
10
+ - **Unified guardian decision primitive:** All guardian decision paths (callback buttons, conversational engine, channel text) must route through `applyGuardianDecision()` in `assistant/src/approvals/guardian-decision-primitive.ts`. Do not inline decision logic (CAS resolution, resolver dispatch, grant minting) at individual callsites.
11
11
 
12
12
  ## Single-Guardian Invariant
13
13
 
@@ -327,21 +327,6 @@ describe("request lookups", () => {
327
327
  });
328
328
  });
329
329
 
330
- test("getPendingRequestByDestinationMessageOrNull maps the destination triple", async () => {
331
- ipcResponse = null;
332
- expect(
333
- await client.getPendingRequestByDestinationMessageOrNull(
334
- "telegram",
335
- "chat-456",
336
- "msg-1",
337
- ),
338
- ).toBeNull();
339
- expect(ipcCalls[0]).toEqual({
340
- method: "guardian_requests_get_by_destination_message",
341
- params: { channel: "telegram", chatId: "chat-456", messageId: "msg-1" },
342
- });
343
- });
344
-
345
330
  test("getPendingRequestByCallSession and getRequestByPendingQuestionOrNull map params", async () => {
346
331
  const request = makeWireRequest({ callSessionId: "call-1" });
347
332
  ipcResponse = request;
@@ -378,18 +363,11 @@ describe("request lookups", () => {
378
363
 
379
364
  ipcError = new Error("gateway unavailable");
380
365
  expect(await client.getGuardianRequestByCodeOrNull("AB12")).toBeNull();
381
- expect(
382
- await client.getPendingRequestByDestinationMessageOrNull(
383
- "telegram",
384
- "chat-456",
385
- "msg-1",
386
- ),
387
- ).toBeNull();
388
366
  expect(
389
367
  await client.getPendingRequestByCallSessionOrNull("call-1"),
390
368
  ).toBeNull();
391
369
  expect(await client.getRequestByPendingQuestionOrNull("pq-1")).toBeNull();
392
- expect(warnCalls).toHaveLength(5);
370
+ expect(warnCalls).toHaveLength(4);
393
371
  });
394
372
 
395
373
  test("OrNull variants pass successful reads through untouched", async () => {
@@ -21,6 +21,7 @@ describe("INTERFACE_IDS", () => {
21
21
  for (const id of [
22
22
  "macos",
23
23
  "windows",
24
+ "linux",
24
25
  "ios",
25
26
  "cli",
26
27
  "telegram",
@@ -45,6 +46,7 @@ describe("INTERACTIVE_INTERFACES", () => {
45
46
  test("includes desktop clients", () => {
46
47
  expect(INTERACTIVE_INTERFACES.has("macos")).toBe(true);
47
48
  expect(INTERACTIVE_INTERFACES.has("windows")).toBe(true);
49
+ expect(INTERACTIVE_INTERFACES.has("linux")).toBe(true);
48
50
  });
49
51
  });
50
52
 
@@ -56,6 +58,7 @@ describe("isInterfaceId", () => {
56
58
  test("returns true for desktop interfaces", () => {
57
59
  expect(isInterfaceId("macos")).toBe(true);
58
60
  expect(isInterfaceId("windows")).toBe(true);
61
+ expect(isInterfaceId("linux")).toBe(true);
59
62
  });
60
63
 
61
64
  test("returns false for unknown interface", () => {
@@ -72,6 +75,7 @@ describe("parseInterfaceId", () => {
72
75
  expect(parseInterfaceId("web")).toBe("web");
73
76
  expect(parseInterfaceId("macos")).toBe("macos");
74
77
  expect(parseInterfaceId("windows")).toBe("windows");
78
+ expect(parseInterfaceId("linux")).toBe("linux");
75
79
  });
76
80
 
77
81
  test("normalizes legacy 'vellum' alias to 'web'", () => {
@@ -117,6 +121,16 @@ describe("supportsHostProxy", () => {
117
121
  expect(supportsHostProxy("windows", "host_app_control")).toBe(false);
118
122
  });
119
123
 
124
+ test("linux exposes the same host capabilities as windows", () => {
125
+ expect(supportsHostProxy("linux")).toBe(true);
126
+ expect(supportsHostProxy("linux", "host_bash")).toBe(true);
127
+ expect(supportsHostProxy("linux", "host_file")).toBe(true);
128
+ expect(supportsHostProxy("linux", "host_cu")).toBe(true);
129
+ expect(supportsHostProxy("linux", "host_browser")).toBe(true);
130
+ expect(supportsHostProxy("linux", "host_ui_snapshot")).toBe(true);
131
+ expect(supportsHostProxy("linux", "host_app_control")).toBe(false);
132
+ });
133
+
120
134
  // ── chrome-extension: only host_browser. ──
121
135
  test("chrome-extension returns false (no capability)", () => {
122
136
  // Chrome extension does not support "any host proxy at all" — it only
@@ -185,13 +199,20 @@ describe("parseClientOs", () => {
185
199
  );
186
200
  });
187
201
 
202
+ test("includes linux (Electron desktop client)", () => {
203
+ expect(parseClientOs("linux")).toBe("linux");
204
+ expect((CLIENT_OS_VALUES as readonly string[]).includes("linux")).toBe(
205
+ true,
206
+ );
207
+ });
208
+
188
209
  test("rejects transport-only interface ids and unknown values", () => {
189
210
  // `clientOs` is an OS surface, not a transport interface — values like
190
211
  // "cli"/"telegram" (real interfaces) are not valid OS surfaces.
191
212
  expect(parseClientOs("cli")).toBeNull();
192
213
  expect(parseClientOs("telegram")).toBeNull();
193
214
  expect(parseClientOs("vellum")).toBeNull();
194
- expect(parseClientOs("linux")).toBeNull();
215
+ expect(parseClientOs("plan9")).toBeNull();
195
216
  expect(parseClientOs(undefined)).toBeNull();
196
217
  expect(parseClientOs(42)).toBeNull();
197
218
  });
@@ -301,28 +301,6 @@ export async function listGuardianRequestDeliveriesByChat(
301
301
  );
302
302
  }
303
303
 
304
- /**
305
- * Reaction routing: the pending request whose delivered card is the
306
- * reacted-to message. Throws on transport failure.
307
- */
308
- async function getPendingRequestByDestinationMessage(
309
- channel: string,
310
- chatId: string,
311
- messageId: string,
312
- ): Promise<GuardianRequestWire | null> {
313
- return callGateway(
314
- GUARDIAN_REQUESTS_IPC_METHODS.getByDestinationMessage,
315
- { channel, chatId, messageId },
316
- GuardianRequestLookupIpcResponseSchema,
317
- );
318
- }
319
-
320
- /** `getPendingRequestByDestinationMessage` deny-path variant: degrades to null. */
321
- export const getPendingRequestByDestinationMessageOrNull = degradeOnFailure(
322
- getPendingRequestByDestinationMessage,
323
- null,
324
- );
325
-
326
304
  /**
327
305
  * Reply routing: pending requests delivered to a destination conversation
328
306
  * (`conversationId`, optionally narrowed by `channel`) or chat
@@ -187,6 +187,7 @@ export const CHANNEL_METADATA: Partial<Record<ChannelId, ChannelInfo>> = {
187
187
  export const INTERFACE_IDS = [
188
188
  "macos",
189
189
  "windows",
190
+ "linux",
190
191
  "ios",
191
192
  "cli",
192
193
  "telegram",
@@ -263,6 +264,7 @@ export const CLIENT_OS_VALUES = [
263
264
  "ios",
264
265
  "macos",
265
266
  "windows",
267
+ "linux",
266
268
  "android",
267
269
  ] as const;
268
270
 
@@ -286,6 +288,7 @@ export function parseClientOs(value: unknown): ClientOs | null {
286
288
  export const INTERACTIVE_INTERFACES: ReadonlySet<InterfaceId> = new Set([
287
289
  "macos",
288
290
  "windows",
291
+ "linux",
289
292
  "ios",
290
293
  "cli",
291
294
  "web",
@@ -297,8 +300,8 @@ export function isInteractiveInterface(id: InterfaceId): boolean {
297
300
 
298
301
  /**
299
302
  * Host proxy capabilities that an interface can support. macOS supports all
300
- * of them, Windows withholds app control, and chrome-extension supports only
301
- * host_browser through the Chrome DevTools Protocol proxy.
303
+ * of them, Windows and Linux withhold app control, and chrome-extension
304
+ * supports only host_browser through the Chrome DevTools Protocol proxy.
302
305
  */
303
306
  export const HOST_PROXY_CAPABILITIES = [
304
307
  "host_bash",
@@ -317,10 +320,9 @@ export type HostProxyCapability = (typeof HOST_PROXY_CAPABILITIES)[number];
317
320
  * `supportsHostProxy(id)` type predicate.
318
321
  *
319
322
  * Extend this literal type AND the `supportsHostProxy` implementation
320
- * below in lock-step when adding a new host-capable client such as native
321
- * Linux.
323
+ * below in lock-step when adding a new host-capable client.
322
324
  */
323
- export type HostProxyInterfaceId = "macos" | "windows";
325
+ export type HostProxyInterfaceId = "macos" | "windows" | "linux";
324
326
 
325
327
  /**
326
328
  * Whether the interface supports a host proxy capability.
@@ -352,7 +354,7 @@ export function supportsHostProxy(
352
354
  if (id === "macos") {
353
355
  return true;
354
356
  }
355
- if (id === "windows") {
357
+ if (id === "windows" || id === "linux") {
356
358
  return capability == null || capability !== "host_app_control";
357
359
  }
358
360
  if (id === "chrome-extension" && capability === "host_browser") {
@@ -9,6 +9,7 @@
9
9
  import { describe, expect, test } from "bun:test";
10
10
 
11
11
  import { buildCliCommandHelpContent } from "../../plugins/defaults/memory/substrate/cli-command-content.js";
12
+ import { channelsHelp } from "../commands/channels/index.help.js";
12
13
  import { pluginsHelp } from "../commands/plugins.help.js";
13
14
  import { skillsHelp } from "../commands/skills.help.js";
14
15
 
@@ -33,6 +34,8 @@ describe("catalog search help for setup-intent retrieval", () => {
33
34
  expect(indexed.toLowerCase()).toContain("before searching the web");
34
35
  expect(indexed).toContain("assistant plugins search");
35
36
  expect(indexed).toContain("assistant skills search");
37
+ expect(indexed.toLowerCase()).toContain("channels");
38
+ expect(pluginsHelp.description).toContain("channels");
36
39
  expect(indexed.toLowerCase()).not.toContain("empty query");
37
40
  });
38
41
 
@@ -47,4 +50,19 @@ describe("catalog search help for setup-intent retrieval", () => {
47
50
  expect(indexed).toContain("assistant skills search");
48
51
  expect(indexed).toContain("try web search");
49
52
  });
53
+
54
+ test("channels help points missing channels at plugin search", () => {
55
+ const indexed = buildCliCommandHelpContent(channelsHelp);
56
+ const list = channelsHelp.subcommands?.find((sub) => sub.name === "list");
57
+
58
+ expect(list?.helpText).toBeDefined();
59
+ expect(indexed).toContain("assistant plugins search <name>");
60
+ expect(indexed).toContain("not listed");
61
+ expect(channelsHelp.description).toBe(
62
+ "Inspect and repair messaging channels",
63
+ );
64
+ expect(channelsHelp.description.toLowerCase()).not.toContain("slack");
65
+ expect(channelsHelp.description.toLowerCase()).not.toContain("telegram");
66
+ expect(channelsHelp.description.toLowerCase()).not.toContain("email");
67
+ });
50
68
  });
@@ -89,6 +89,32 @@ describe("assistant channels", () => {
89
89
  queryParams: { includeRemote: "true" },
90
90
  });
91
91
  });
92
+
93
+ test("human list output points missing channels at plugin search", async () => {
94
+ mockResponses = [
95
+ {
96
+ ok: true,
97
+ result: { success: true, snapshots: [emptySnapshot("slack")] },
98
+ },
99
+ ];
100
+ const out = await runCli("channels", "list");
101
+ expect(out).toContain("assistant plugins search <name>");
102
+ expect(out).toContain("not listed");
103
+ });
104
+
105
+ test("json list output omits the plugin-search hint", async () => {
106
+ mockResponses = [
107
+ {
108
+ ok: true,
109
+ result: { success: true, snapshots: [emptySnapshot("slack")] },
110
+ },
111
+ ];
112
+ const out = await runCli("channels", "list", "--json");
113
+ expect(out).not.toContain("assistant plugins search");
114
+ expect(JSON.parse(out)).toEqual({
115
+ snapshots: [emptySnapshot("slack")],
116
+ });
117
+ });
92
118
  });
93
119
 
94
120
  describe("two-axis rendering", () => {
@@ -14,14 +14,19 @@ export const KNOWN_CHANNELS = [
14
14
  "a2a",
15
15
  ] as const;
16
16
 
17
+ export const CHANNELS_PLUGIN_SEARCH_HINT =
18
+ "If the channel you are looking for is not listed, search the plugin marketplace with 'assistant plugins search <name>'.";
19
+
17
20
  export const channelsHelp: CliCommandHelp = {
18
21
  name: "channels",
19
- description:
20
- "Inspect and repair messaging channels (slack, telegram, email, etc.)",
22
+ description: "Inspect and repair messaging channels",
21
23
  helpText: `
22
- Channels are the messaging surfaces the assistant talks over — slack,
23
- telegram, whatsapp, email, phone, vellum, platform, a2a. Each channel
24
- has a probe that reports whether it's configured and reachable.
24
+ Channels are the messaging surfaces the assistant talks over. Built-in
25
+ readiness probes cover slack, telegram, whatsapp, email, phone, vellum,
26
+ platform, and a2a.
27
+
28
+ ${CHANNELS_PLUGIN_SEARCH_HINT} Plugins can bundle additional channels
29
+ from other Vellum users.
25
30
 
26
31
  list Overview of every channel + ready state
27
32
  get <channel> Live snapshot of one channel (always re-probes)
@@ -45,6 +50,14 @@ Examples:
45
50
  defaultValue: false,
46
51
  },
47
52
  ],
53
+ helpText: `
54
+ Shows readiness for every built-in channel.
55
+
56
+ ${CHANNELS_PLUGIN_SEARCH_HINT}
57
+
58
+ Examples:
59
+ $ assistant channels list
60
+ $ assistant channels list --json`,
48
61
  },
49
62
  {
50
63
  name: "get",
@@ -53,7 +66,7 @@ Examples:
53
66
  arguments: [
54
67
  {
55
68
  name: "<channel>",
56
- description: `Channel id: ${KNOWN_CHANNELS.join(", ")}`,
69
+ description: `Channel id: ${KNOWN_CHANNELS.join(", ")}. If the channel is not in this list, run 'assistant plugins search <name>'.`,
57
70
  },
58
71
  ],
59
72
  options: [
@@ -20,7 +20,7 @@ import { applyCommandHelp, subcommand } from "../../lib/cli-command-help.js";
20
20
  import { registerCommand } from "../../lib/register-command.js";
21
21
  import { log } from "../../logger.js";
22
22
  import { shouldOutputJson, writeOutput } from "../../output.js";
23
- import { channelsHelp } from "./index.help.js";
23
+ import { CHANNELS_PLUGIN_SEARCH_HINT, channelsHelp } from "./index.help.js";
24
24
 
25
25
  // ---------------------------------------------------------------------------
26
26
  // Snapshot shape
@@ -95,6 +95,8 @@ function renderList(snapshots: ChannelSnapshot[]): void {
95
95
  for (const s of sorted) {
96
96
  log.info(`${statusGlyph(s)} ${s.channel.padEnd(12)} ${statusState(s)}`);
97
97
  }
98
+ log.info("");
99
+ log.info(CHANNELS_PLUGIN_SEARCH_HINT);
98
100
  }
99
101
 
100
102
  function renderSnapshot(s: ChannelSnapshot): void {
@@ -185,7 +187,9 @@ export function registerChannelsCommand(program: Command): void {
185
187
  (s) => s.channel === channel,
186
188
  );
187
189
  if (!snapshot) {
188
- log.error(`No readiness probe registered for channel: ${channel}`);
190
+ log.error(
191
+ `No readiness probe registered for channel: ${channel}. If you are looking for a community channel, run 'assistant plugins search ${channel}'.`,
192
+ );
189
193
  process.exitCode = 1;
190
194
  return;
191
195
  }
@@ -169,6 +169,8 @@ describe("assistant db status — happy path", () => {
169
169
  // (memory_checkpoints has 3 rows.)
170
170
  expect(stdout).toContain("Largest tables");
171
171
  expect(stdout.indexOf("messages")).toBeLessThan(stdout.indexOf("small"));
172
+ expect(stdout).toContain("Schema contract");
173
+ expect(stdout).toContain("missing");
172
174
  });
173
175
 
174
176
  test("reports the latest 'completed' migration, ignoring 'started'", async () => {
@@ -212,6 +214,26 @@ describe("assistant db status — happy path", () => {
212
214
  expect(parsed.db.largest.length).toBeGreaterThan(0);
213
215
  // Either 'bytes' (if dbstat is available) or 'rows' fallback.
214
216
  expect(["bytes", "rows"]).toContain(parsed.db.sizingMethod);
217
+ expect(Array.isArray(parsed.schemaContract.missingTables)).toBe(true);
218
+ expect(Array.isArray(parsed.schemaContract.missingColumns)).toBe(true);
219
+ // The seed is a 3-table stub, so the drizzle catalog reports misses.
220
+ expect(parsed.schemaContract.missingTables.length).toBeGreaterThan(0);
221
+ });
222
+
223
+ test("reports conversations.fork_strategy when the table exists without that column", async () => {
224
+ seedDb();
225
+ const db = new Database(dbPath);
226
+ try {
227
+ db.exec(`CREATE TABLE conversations (id TEXT PRIMARY KEY, title TEXT)`);
228
+ } finally {
229
+ db.close();
230
+ }
231
+
232
+ const { stdout, exitCode } = await runStatus(["status"]);
233
+
234
+ expect(exitCode).toBe(0);
235
+ expect(stdout).toContain("Schema contract");
236
+ expect(stdout).toContain("conversations.fork_strategy");
215
237
  });
216
238
  });
217
239
 
@@ -12,7 +12,7 @@ export const dbHelp: CliCommandHelp = {
12
12
  {
13
13
  name: "status",
14
14
  description:
15
- "Show database path, size, key pragmas, and the 5 largest tables",
15
+ "Show database path, size, key pragmas, largest tables, and drizzle schema misses",
16
16
  },
17
17
  {
18
18
  name: "repair",
@@ -16,6 +16,8 @@
16
16
  * callers will see the row-count path today)
17
17
  * - latest applied workspace migration (proxy for schema version, read
18
18
  * from the `memory_checkpoints` rows the migration runner writes)
19
+ * - drizzle-vs-SQLite schema contract (columns the current binary
20
+ * selects that no live database file has)
19
21
  * - file mtime + age, for "is this DB live?" triage at a glance
20
22
  *
21
23
  * If the DB file is missing, exits with code 1 after a loud error. If the file
@@ -28,6 +30,7 @@ import { Database } from "bun:sqlite";
28
30
 
29
31
  import type { Command } from "commander";
30
32
 
33
+ import type { SchemaContractReport } from "../../../persistence/schema-contract.js";
31
34
  import { getLogsDbPath } from "../../../util/logs-db-path.js";
32
35
  import { getMemoryDbPath } from "../../../util/memory-db-path.js";
33
36
  import { getDbPath } from "../../../util/platform.js";
@@ -110,6 +113,12 @@ interface StatusReport {
110
113
  */
111
114
  telemetryFile?: FileFacts;
112
115
  telemetryDb?: DbFacts | null;
116
+ /**
117
+ * Drizzle source catalog vs the union of columns in every live SQLite
118
+ * file (main, logs, memory, telemetry). Extra SQLite-only columns are
119
+ * ignored. Omitted when the main file could not be opened.
120
+ */
121
+ schemaContract?: SchemaContractReport;
113
122
  }
114
123
 
115
124
  interface MigrationSummary {
@@ -260,6 +269,83 @@ function readDbFacts(path: string): DbFacts {
260
269
  }
261
270
  }
262
271
 
272
+ /** Quote a SQLite identifier that came from sqlite_master (never user input). */
273
+ function quoteIdent(name: string): string {
274
+ return `"${name.replaceAll('"', '""')}"`;
275
+ }
276
+
277
+ /**
278
+ * Read table → column names from one SQLite file. Skips `sqlite_*`
279
+ * internals and tables whose `PRAGMA table_info` fails.
280
+ */
281
+ function readSqliteColumns(path: string): Map<string, Set<string>> {
282
+ const db = new Database(path, { readonly: true });
283
+ try {
284
+ const tables = db
285
+ .query<{ name: string }, []>(
286
+ `SELECT name FROM sqlite_master
287
+ WHERE type='table' AND name NOT LIKE 'sqlite_%'`,
288
+ )
289
+ .all();
290
+ const out = new Map<string, Set<string>>();
291
+ for (const table of tables) {
292
+ try {
293
+ const columns = db
294
+ .query<{ name: string }, []>(
295
+ `PRAGMA table_info(${quoteIdent(table.name)})`,
296
+ )
297
+ .all();
298
+ out.set(table.name, new Set(columns.map((column) => column.name)));
299
+ } catch {
300
+ // Virtual or unreadable table. Skip so one failure does not hide
301
+ // the rest of the contract.
302
+ }
303
+ }
304
+ return out;
305
+ } finally {
306
+ db.close();
307
+ }
308
+ }
309
+
310
+ /** Union column sets from several files. Same table name keeps every column. */
311
+ function mergeSqliteColumns(
312
+ files: Array<Map<string, Set<string>>>,
313
+ ): Map<string, Set<string>> {
314
+ const merged = new Map<string, Set<string>>();
315
+ for (const file of files) {
316
+ for (const [table, columns] of file) {
317
+ const existing = merged.get(table);
318
+ if (!existing) {
319
+ merged.set(table, new Set(columns));
320
+ continue;
321
+ }
322
+ for (const column of columns) {
323
+ existing.add(column);
324
+ }
325
+ }
326
+ }
327
+ return merged;
328
+ }
329
+
330
+ async function readSchemaContract(
331
+ paths: string[],
332
+ ): Promise<SchemaContractReport> {
333
+ // Lazy so the drizzle catalog stays out of the CLI's static graph
334
+ // (cli/no-daemon-internals).
335
+ const { diffDrizzleSchemaContract } =
336
+ await import("../../../persistence/schema-contract.js");
337
+ const live: Array<Map<string, Set<string>>> = [];
338
+ for (const path of paths) {
339
+ try {
340
+ live.push(readSqliteColumns(path));
341
+ } catch {
342
+ // File vanished or is unreadable. The main-file open already
343
+ // succeeded; skip this sidecar rather than fail the report.
344
+ }
345
+ }
346
+ return diffDrizzleSchemaContract(mergeSqliteColumns(live));
347
+ }
348
+
263
349
  // ---------------------------------------------------------------------------
264
350
  // Rendering
265
351
  // ---------------------------------------------------------------------------
@@ -350,6 +436,7 @@ function renderHuman(report: StatusReport): string {
350
436
  // Cross-database migration summary — the checkpoint ledger lives only in
351
437
  // the main DB, so this is a single block at the end covering all three.
352
438
  out += renderSummary(report.migration);
439
+ out += renderSchemaContract(report.schemaContract);
353
440
 
354
441
  return out;
355
442
  }
@@ -469,6 +556,72 @@ function renderSummary(migration: MigrationSummary | undefined): string {
469
556
  return out;
470
557
  }
471
558
 
559
+ const HUMAN_COLUMN_MISS_LIMIT = 40;
560
+ const HUMAN_TABLE_MISS_LIMIT = 15;
561
+
562
+ function schemaContractIsClean(report: SchemaContractReport): boolean {
563
+ return (
564
+ report.missingTables.length === 0 && report.missingColumns.length === 0
565
+ );
566
+ }
567
+
568
+ function renderMissList(labels: string[], limit: number): string {
569
+ let out = "";
570
+ const shown = labels.slice(0, limit);
571
+ for (const label of shown) {
572
+ out += ` ${label}\n`;
573
+ }
574
+ if (labels.length > limit) {
575
+ out += ` … and ${formatCount(labels.length - limit)} more\n`;
576
+ }
577
+ return out;
578
+ }
579
+
580
+ function renderSchemaContract(
581
+ contract: SchemaContractReport | undefined,
582
+ ): string {
583
+ if (!contract) {
584
+ return "";
585
+ }
586
+
587
+ let out = "\nSchema contract (drizzle vs SQLite)\n";
588
+ if (schemaContractIsClean(contract)) {
589
+ out += row("Status", "ok");
590
+ return out;
591
+ }
592
+
593
+ const parts: string[] = [];
594
+ if (contract.missingColumns.length > 0) {
595
+ parts.push(
596
+ `${formatCount(contract.missingColumns.length)} missing column${
597
+ contract.missingColumns.length === 1 ? "" : "s"
598
+ }`,
599
+ );
600
+ }
601
+ if (contract.missingTables.length > 0) {
602
+ parts.push(
603
+ `${formatCount(contract.missingTables.length)} missing table${
604
+ contract.missingTables.length === 1 ? "" : "s"
605
+ }`,
606
+ );
607
+ }
608
+ out += row("Status", red(parts.join(", ")));
609
+
610
+ if (contract.missingColumns.length > 0) {
611
+ out += "Missing columns\n";
612
+ out += renderMissList(
613
+ contract.missingColumns.map((miss) => `${miss.table}.${miss.column}`),
614
+ HUMAN_COLUMN_MISS_LIMIT,
615
+ );
616
+ }
617
+ if (contract.missingTables.length > 0) {
618
+ out += "Missing tables\n";
619
+ out += renderMissList(contract.missingTables, HUMAN_TABLE_MISS_LIMIT);
620
+ }
621
+
622
+ return out;
623
+ }
624
+
472
625
  function renderMissing(path: string): string {
473
626
  return (
474
627
  `${red("ERROR")} Database not found at ${path}\n\n` +
@@ -487,7 +640,7 @@ function renderMissing(path: string): string {
487
640
  // ---------------------------------------------------------------------------
488
641
 
489
642
  export function registerDbStatus(parent: Command): void {
490
- subcommand(parent, "status").action(function (this: Command) {
643
+ subcommand(parent, "status").action(async function (this: Command) {
491
644
  const file = readFileFacts(getDbPath());
492
645
 
493
646
  if (!file.exists) {
@@ -561,10 +714,28 @@ export function registerDbStatus(parent: Command): void {
561
714
  migration = null;
562
715
  }
563
716
 
717
+ const schemaPaths = [file.path];
718
+ if (logsFile.exists) {
719
+ schemaPaths.push(logsFile.path);
720
+ }
721
+ if (memoryFile.exists) {
722
+ schemaPaths.push(memoryFile.path);
723
+ }
724
+ if (telemetryFile.exists) {
725
+ schemaPaths.push(telemetryFile.path);
726
+ }
727
+ let schemaContract: SchemaContractReport | undefined;
728
+ try {
729
+ schemaContract = await readSchemaContract(schemaPaths);
730
+ } catch {
731
+ schemaContract = undefined;
732
+ }
733
+
564
734
  const report: StatusReport = {
565
735
  file,
566
736
  db,
567
737
  migration: migration ?? undefined,
738
+ schemaContract,
568
739
  logsFile,
569
740
  logsDb,
570
741
  memoryFile,
@@ -69,6 +69,35 @@ describe("assistant platform connect", () => {
69
69
  expect(parsed.showPlatformLogin).toBe(true);
70
70
  });
71
71
 
72
+ // A stored key the platform rejects is a recovery, not a first setup: the
73
+ // outcome has to reach the caller in the response, since the daemon's login
74
+ // signal has no shipped consumer.
75
+ test("a rejected stored credential is reported as such", async () => {
76
+ mockResponse = {
77
+ ok: true,
78
+ result: { showPlatformLogin: true, credentialRejected: true },
79
+ };
80
+
81
+ const program = buildProgram();
82
+ const stdoutChunks: string[] = [];
83
+ const origWrite = process.stdout.write.bind(process.stdout);
84
+ process.stdout.write = ((chunk: unknown) => {
85
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
86
+ return true;
87
+ }) as typeof process.stdout.write;
88
+
89
+ try {
90
+ await program.parseAsync(["node", "assistant", "connect", "--json"]);
91
+ } finally {
92
+ process.stdout.write = origWrite;
93
+ }
94
+
95
+ const parsed = JSON.parse(stdoutChunks.join(""));
96
+ expect(parsed.ok).toBe(true);
97
+ expect(parsed.credentialRejected).toBe(true);
98
+ expect(parsed.alreadyConnected).toBeUndefined();
99
+ });
100
+
72
101
  test("already connected returns success with existing base URL", async () => {
73
102
  mockResponse = {
74
103
  ok: true,