@vellumai/assistant 0.11.6 → 0.11.7-staging.1

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 (447) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/environments/package.json +2 -1
  21. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  22. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  30. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  32. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  33. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  34. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  35. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  36. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  37. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  38. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  39. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  40. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  41. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  42. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  43. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  44. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  45. package/openapi.yaml +419 -55
  46. package/package.json +4 -1
  47. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  48. package/src/__tests__/approval-card-edit.test.ts +153 -0
  49. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  50. package/src/__tests__/available-skills.test.ts +54 -0
  51. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  52. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  53. package/src/__tests__/channel-policy.test.ts +1 -3
  54. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  55. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  56. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  57. package/src/__tests__/config-schema.test.ts +2 -3
  58. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  59. package/src/__tests__/conversation-attachments.test.ts +0 -1
  60. package/src/__tests__/conversation-error.test.ts +44 -0
  61. package/src/__tests__/conversation-evictor.test.ts +19 -0
  62. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  63. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  65. package/src/__tests__/conversation-slash.test.ts +9 -0
  66. package/src/__tests__/conversation-title-service.test.ts +97 -0
  67. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  68. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  69. package/src/__tests__/credential-broker.test.ts +43 -420
  70. package/src/__tests__/credential-routes.test.ts +17 -1
  71. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  72. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  73. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  74. package/src/__tests__/discord-channel-config.test.ts +237 -0
  75. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  76. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  77. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  78. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  79. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  80. package/src/__tests__/image-conversion.test.ts +38 -4
  81. package/src/__tests__/install-skill-routing.test.ts +25 -0
  82. package/src/__tests__/llm-resolver.test.ts +74 -0
  83. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  84. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  85. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  86. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  87. package/src/__tests__/mcp-cli.test.ts +23 -1
  88. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  89. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  90. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  91. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  92. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  93. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  94. package/src/__tests__/platform.test.ts +19 -1
  95. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  96. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  97. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  98. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  99. package/src/__tests__/schedule-retry.test.ts +183 -4
  100. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  101. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  102. package/src/__tests__/skill-load-tool.test.ts +34 -0
  103. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  104. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  105. package/src/__tests__/skills.test.ts +25 -0
  106. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  107. package/src/__tests__/terminal-tools.test.ts +2 -0
  108. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  109. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  110. package/src/__tests__/tool-audit.test.ts +111 -40
  111. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  112. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  113. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  114. package/src/__tests__/voice-config-update.test.ts +43 -0
  115. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  116. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  117. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  118. package/src/agent/loop.ts +31 -2
  119. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  120. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  121. package/src/avatar/avatar-manifest.ts +48 -100
  122. package/src/avatar/avatar-store.ts +7 -7
  123. package/src/avatar/ensure-raster.ts +146 -0
  124. package/src/avatar/resvg-lazy.test.ts +0 -1
  125. package/src/avatar/traits-png-sync.ts +9 -10
  126. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  127. package/src/backup/__tests__/paths.test.ts +6 -2
  128. package/src/backup/paths.ts +6 -4
  129. package/src/bundler/app-compiler.ts +2 -2
  130. package/src/bundler/compiler-tools.test.ts +36 -0
  131. package/src/bundler/compiler-tools.ts +54 -18
  132. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
  134. package/src/calls/media-stream-stt-session.ts +2 -1
  135. package/src/calls/telephony-synthesis-language.ts +13 -5
  136. package/src/calls/voice-session-bridge.ts +6 -4
  137. package/src/channels/config.ts +23 -12
  138. package/src/cli/AGENTS.md +3 -1
  139. package/src/cli/commands/backup.help.ts +10 -13
  140. package/src/cli/commands/contacts.help.ts +16 -4
  141. package/src/cli/commands/contacts.ts +5 -1
  142. package/src/cli/commands/credentials.help.ts +1 -1
  143. package/src/cli/commands/inference-providers.ts +18 -53
  144. package/src/cli/commands/mcp.help.ts +6 -5
  145. package/src/cli/commands/mcp.ts +1 -1
  146. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  147. package/src/cli/commands/stt.help.ts +2 -2
  148. package/src/config/__tests__/backup-schema.test.ts +1 -1
  149. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  150. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  151. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  152. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  153. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  154. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  155. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  156. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  157. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  158. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  159. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  160. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  161. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  162. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  163. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  164. package/src/config/default-profile-catalog.ts +195 -10
  165. package/src/config/default-profile-names.ts +41 -0
  166. package/src/config/feature-flag-registry.json +18 -2
  167. package/src/config/inference-profile-validation.ts +11 -2
  168. package/src/config/llm-resolver.ts +25 -0
  169. package/src/config/loader.ts +3 -0
  170. package/src/config/managed-speech-defaults.ts +160 -10
  171. package/src/config/schema.ts +2 -0
  172. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  173. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  174. package/src/config/schemas/backup.ts +1 -1
  175. package/src/config/schemas/channels.ts +32 -0
  176. package/src/config/schemas/live-voice.ts +13 -4
  177. package/src/config/schemas/llm.ts +259 -11
  178. package/src/config/schemas/mcp.ts +10 -2
  179. package/src/config/schemas/memory-retrospective.ts +1 -19
  180. package/src/config/schemas/services.ts +24 -0
  181. package/src/config/schemas/stt.ts +128 -5
  182. package/src/config/seed-inference-profiles.ts +32 -3
  183. package/src/config/skill-state.ts +5 -0
  184. package/src/config/skills.ts +21 -0
  185. package/src/contacts/notify-contacts-changed.ts +5 -2
  186. package/src/context/compactor.ts +74 -1
  187. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  188. package/src/daemon/config-watcher.ts +2 -1
  189. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  190. package/src/daemon/conversation-agent-loop.ts +86 -10
  191. package/src/daemon/conversation-error.ts +25 -0
  192. package/src/daemon/conversation-evictor.ts +2 -4
  193. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  194. package/src/daemon/conversation-skill-tools.ts +5 -1
  195. package/src/daemon/conversation-slash.ts +2 -2
  196. package/src/daemon/conversation-store.ts +8 -1
  197. package/src/daemon/conversation-tool-setup.ts +37 -0
  198. package/src/daemon/conversation.ts +0 -4
  199. package/src/daemon/handlers/channel-config-result.ts +30 -0
  200. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  201. package/src/daemon/handlers/config-ingress.ts +9 -0
  202. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  203. package/src/daemon/handlers/config-telegram.ts +2 -5
  204. package/src/daemon/handlers/conversations.ts +0 -2
  205. package/src/daemon/handlers/skills.ts +25 -7
  206. package/src/daemon/lifecycle.ts +3 -1
  207. package/src/daemon/message-types/messages.ts +0 -2
  208. package/src/daemon/tool-setup-types.ts +5 -3
  209. package/src/daemon/trust-context-types.test.ts +33 -0
  210. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  211. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  212. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  213. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  214. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  215. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  216. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  217. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  218. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  219. package/src/live-voice/live-voice-session-manager.ts +39 -2
  220. package/src/live-voice/live-voice-session.ts +213 -8
  221. package/src/live-voice/protocol.ts +187 -1
  222. package/src/messaging/providers/discord/api.ts +2 -1
  223. package/src/messaging/providers/discord/withdraw.ts +62 -0
  224. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  225. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  226. package/src/monitoring/db-integrity-sample.ts +1 -0
  227. package/src/monitoring/file-descriptors.ts +39 -6
  228. package/src/monitoring/process-memory.ts +37 -5
  229. package/src/monitoring/resource-sampler.ts +25 -8
  230. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  231. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  232. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  233. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  234. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  235. package/src/notifications/activity-failed-dedupe.ts +95 -0
  236. package/src/notifications/adapters/discord.ts +132 -0
  237. package/src/notifications/adapters/shared.ts +16 -0
  238. package/src/notifications/adapters/slack.ts +6 -1
  239. package/src/notifications/adapters/telegram.ts +5 -8
  240. package/src/notifications/approval-card-data.ts +94 -1
  241. package/src/notifications/background-failure-signal.ts +116 -0
  242. package/src/notifications/broadcaster.ts +3 -9
  243. package/src/notifications/copy-composer.ts +41 -8
  244. package/src/notifications/decision-engine.ts +55 -0
  245. package/src/notifications/destination-resolver.ts +45 -5
  246. package/src/notifications/emit-signal.ts +19 -5
  247. package/src/notifications/guardian-question-mode.ts +57 -0
  248. package/src/notifications/types.ts +2 -8
  249. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  250. package/src/oauth/seed-providers.ts +261 -0
  251. package/src/permissions/checker.test.ts +21 -0
  252. package/src/permissions/checker.ts +6 -0
  253. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  254. package/src/permissions/gateway-threshold-reader.ts +144 -10
  255. package/src/permissions/prompter.ts +0 -6
  256. package/src/permissions/types.ts +5 -4
  257. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  258. package/src/persistence/bulk-write-gate.ts +9 -10
  259. package/src/persistence/conversation-crud.ts +85 -133
  260. package/src/persistence/conversation-title-service.ts +43 -29
  261. package/src/persistence/embeddings/embedding-local.ts +14 -85
  262. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  263. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  264. package/src/persistence/lifecycle-events-store.ts +75 -5
  265. package/src/platform/platform-patch-queue.test.ts +368 -0
  266. package/src/platform/platform-patch-queue.ts +190 -0
  267. package/src/platform/sync-avatar.test.ts +527 -0
  268. package/src/platform/sync-avatar.ts +209 -0
  269. package/src/platform/sync-identity.test.ts +75 -0
  270. package/src/platform/sync-identity.ts +17 -110
  271. package/src/plugin-api/model-profiles.test.ts +2 -2
  272. package/src/plugin-api/model-profiles.ts +7 -7
  273. package/src/plugin-api/transcription-session.ts +1 -1
  274. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  275. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  276. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  280. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  281. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  282. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  283. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  284. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  286. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  287. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  288. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  289. package/src/plugins/defaults/memory/worker.ts +7 -0
  290. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  291. package/src/providers/connection-resolution.ts +2 -17
  292. package/src/providers/fallback-breaker.ts +389 -0
  293. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  294. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  295. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  296. package/src/providers/inference/adapter-factory.ts +231 -3
  297. package/src/providers/inference/auth.ts +1 -0
  298. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  299. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  300. package/src/providers/inference/resolve-auth.ts +1 -1
  301. package/src/providers/media-resolve.ts +2 -3
  302. package/src/providers/model-catalog.ts +75 -0
  303. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  304. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  305. package/src/providers/openai/api-error-normalization.ts +13 -0
  306. package/src/providers/openai/chat-completions-provider.ts +84 -0
  307. package/src/providers/opencode/client.test.ts +150 -0
  308. package/src/providers/opencode/client.ts +61 -0
  309. package/src/providers/provider-resolvability.ts +20 -0
  310. package/src/providers/retry.ts +956 -68
  311. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  312. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  313. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  314. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  315. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  316. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  317. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  318. package/src/providers/speech-to-text/resolve.ts +132 -24
  319. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  320. package/src/providers/types.ts +39 -0
  321. package/src/providers/usage-tracking.ts +12 -1
  322. package/src/runtime/AGENTS.md +1 -2
  323. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  324. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  325. package/src/runtime/agent-wake.ts +44 -5
  326. package/src/runtime/background-job-runner.ts +68 -50
  327. package/src/runtime/channel-readiness-service.ts +0 -39
  328. package/src/runtime/http-server.ts +12 -4
  329. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  330. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  331. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  332. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  333. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  334. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  335. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  336. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  337. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  338. package/src/runtime/routes/approval-routes.ts +0 -12
  339. package/src/runtime/routes/avatar-routes.ts +17 -30
  340. package/src/runtime/routes/backup-routes.ts +2 -2
  341. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  342. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  343. package/src/runtime/routes/contact-routes.ts +20 -15
  344. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  345. package/src/runtime/routes/conversation-routes.ts +2 -2
  346. package/src/runtime/routes/credential-routes.ts +2 -1
  347. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  348. package/src/runtime/routes/inbound-message-handler.ts +21 -9
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  350. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  351. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  352. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  353. package/src/runtime/routes/index.ts +2 -0
  354. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  355. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  356. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  357. package/src/runtime/routes/inference-send-routes.ts +2 -2
  358. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  359. package/src/runtime/routes/integrations/discord.ts +119 -0
  360. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  361. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  362. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  363. package/src/runtime/routes/secret-routes.ts +7 -0
  364. package/src/runtime/routes/stt-routes.ts +38 -6
  365. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  366. package/src/runtime/routes/watch-routes.ts +9 -5
  367. package/src/runtime/sync/resource-sync-events.ts +2 -0
  368. package/src/runtime/trust-verdict-consumer.ts +3 -3
  369. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  370. package/src/schedule/retry-policy.ts +8 -8
  371. package/src/schedule/run-script.ts +14 -23
  372. package/src/schedule/schedule-recovery.ts +0 -1
  373. package/src/schedule/scheduler.ts +89 -51
  374. package/src/schedule/worker.ts +57 -0
  375. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  376. package/src/security/oauth2.ts +7 -0
  377. package/src/security/openai-device-auth.ts +364 -0
  378. package/src/skills/available-skills.ts +10 -1
  379. package/src/skills/catalog-install.ts +19 -1
  380. package/src/skills/inline-command-runner.ts +6 -2
  381. package/src/skills/platform-compatibility.test.ts +49 -0
  382. package/src/skills/platform-compatibility.ts +65 -0
  383. package/src/skills/tool-manifest.ts +20 -0
  384. package/src/stt/__tests__/roles.test.ts +152 -0
  385. package/src/stt/daemon-batch-transcriber.ts +4 -1
  386. package/src/stt/roles.ts +180 -0
  387. package/src/stt/stt-stream-session.ts +3 -2
  388. package/src/stt/types.ts +33 -3
  389. package/src/subagent/manager.ts +12 -0
  390. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  391. package/src/telemetry/live-voice-funnel.ts +12 -2
  392. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  393. package/src/telemetry/tool-audit.ts +83 -30
  394. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  395. package/src/telemetry/turn-outcome.ts +75 -4
  396. package/src/tools/client-os.ts +115 -0
  397. package/src/tools/computer-use/definitions.ts +11 -7
  398. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  399. package/src/tools/credentials/broker-types.ts +0 -71
  400. package/src/tools/credentials/broker.ts +6 -201
  401. package/src/tools/executor.ts +2 -8
  402. package/src/tools/host-shell.test.ts +75 -0
  403. package/src/tools/host-shell.ts +24 -0
  404. package/src/tools/host-terminal/host-shell.ts +15 -44
  405. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  406. package/src/tools/permission-checker.ts +73 -21
  407. package/src/tools/policy-context.ts +1 -0
  408. package/src/tools/shared/filesystem/image-read.ts +1 -1
  409. package/src/tools/skills/find-similar-skills.ts +10 -2
  410. package/src/tools/skills/load.ts +41 -6
  411. package/src/tools/skills/sandbox-runner.ts +8 -16
  412. package/src/tools/skills/skill-tool-factory.ts +1 -0
  413. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  414. package/src/tools/terminal/safe-env.ts +39 -9
  415. package/src/tools/terminal/shell.ts +12 -31
  416. package/src/tools/tool-approval-handler.ts +64 -10
  417. package/src/tools/tool-types.ts +10 -0
  418. package/src/tools/types.ts +19 -3
  419. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  420. package/src/tools/workflows/manage-workflows.ts +2 -2
  421. package/src/tools/workflows/run-workflow.test.ts +2 -1
  422. package/src/util/__tests__/file-use.test.ts +61 -0
  423. package/src/util/__tests__/process-table.test.ts +66 -0
  424. package/src/util/__tests__/process-tree.test.ts +46 -0
  425. package/src/util/errors.ts +1 -0
  426. package/src/util/file-use.ts +91 -0
  427. package/src/util/host-process.test.ts +17 -0
  428. package/src/util/host-process.ts +75 -0
  429. package/src/util/image-conversion.ts +39 -7
  430. package/src/util/platform.ts +32 -13
  431. package/src/util/process-table.ts +249 -0
  432. package/src/util/process-tree.ts +85 -129
  433. package/src/util/provider-error-patterns.ts +20 -0
  434. package/src/util/spawn.ts +9 -2
  435. package/src/workspace/git-service.ts +4 -14
  436. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  437. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  438. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  439. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  440. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  441. package/src/workspace/migrations/registry.ts +8 -0
  442. package/src/__tests__/permission-types.test.ts +0 -17
  443. package/src/channels/gateway-discord-admission.ts +0 -32
  444. package/src/persistence/fork-message-copy.ts +0 -239
  445. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  446. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  447. package/src/security/oauth2-device-code.ts +0 -312
@@ -1,27 +1,25 @@
1
1
  /**
2
- * End-to-end guards over referential forking: `forkStrategy: "reference"`
3
- * builds a fork that copies no rows and reads its inherited window back
4
- * through `fork_parent_message_id`.
2
+ * End-to-end guards over referential forking. A retrospective fork copies no
3
+ * rows and reads its inherited window back through `fork_parent_message_id`.
5
4
  *
6
5
  * The load-bearing claim is equivalence. A referential fork must present the
7
- * same message list a copied fork presents, because the retrospective agent
8
- * reads the fork natively and every downstream consumer (accounting, dedup,
9
- * the fork-boundary scan) reads it through the same helpers. The parity test
10
- * against the cloning path is what pins that.
6
+ * same message list the source presents (through the fork point), because the
7
+ * retrospective agent reads the fork natively and every downstream consumer
8
+ * (accounting, dedup, the fork-boundary scan) reads it through the same
9
+ * helpers.
11
10
  */
12
11
 
13
- import { rmSync, writeFileSync } from "node:fs";
14
- import { afterAll, beforeEach, describe, expect, test } from "bun:test";
12
+ import { beforeEach, describe, expect, test } from "bun:test";
15
13
 
16
14
  import { eq } from "drizzle-orm";
17
15
 
18
- import { invalidateConfigCache } from "../config/loader.js";
19
16
  import {
20
17
  addMessage,
21
18
  countMessagesAfter,
22
19
  createConversation,
23
20
  deleteConversation,
24
21
  deleteConversationGently,
22
+ forkConversation,
25
23
  forkConversationForRetrospective,
26
24
  getMessages,
27
25
  getMessagesAfter,
@@ -38,36 +36,14 @@ import {
38
36
  memoryRetrospectiveState,
39
37
  messages,
40
38
  } from "../persistence/schema/index.js";
41
- import { getWorkspaceConfigPath } from "../util/platform.js";
39
+ import {
40
+ MEMORY_RETROSPECTIVE_FORK_SOURCE,
41
+ MEMORY_RETROSPECTIVE_INSTRUCTION_KIND,
42
+ } from "../plugins/defaults/memory/memory-retrospective-constants.js";
43
+ import { loadRetrospectiveRunMessages } from "../plugins/defaults/memory/memory-retrospective-fork-boundary.js";
42
44
 
43
45
  await initializeDb();
44
46
 
45
- const configPath = getWorkspaceConfigPath();
46
-
47
- /**
48
- * Point `memory.retrospective.forkStrategy` at a strategy. The fork path reads
49
- * it from config rather than taking a parameter, so exercising both branches
50
- * means writing the workspace config the loader reads.
51
- */
52
- function setForkStrategy(strategy: "cloning" | "reference"): void {
53
- writeFileSync(
54
- configPath,
55
- JSON.stringify(
56
- { memory: { retrospective: { forkStrategy: strategy } } },
57
- null,
58
- 2,
59
- ) + "\n",
60
- );
61
- invalidateConfigCache();
62
- }
63
-
64
- function clearConfig(): void {
65
- rmSync(configPath, { force: true });
66
- invalidateConfigCache();
67
- }
68
-
69
- afterAll(clearConfig);
70
-
71
47
  function resetTables(): void {
72
48
  const db = getDb();
73
49
  getMemoryDb()!.delete(memoryRetrospectiveState).run();
@@ -133,13 +109,11 @@ function textOf(rows: Array<{ content: unknown }>): string[] {
133
109
  describe("referential forking", () => {
134
110
  beforeEach(() => {
135
111
  resetTables();
136
- clearConfig();
137
112
  });
138
113
 
139
114
  test("copies no message rows and stamps the strategy", async () => {
140
115
  const source = await seedSource("Launch");
141
116
 
142
- setForkStrategy("reference");
143
117
  const fork = await forkConversationForRetrospective({
144
118
  conversationId: source.id,
145
119
  });
@@ -153,7 +127,6 @@ describe("referential forking", () => {
153
127
  test("reads the source's history through the fork pointer", async () => {
154
128
  const source = await seedSource("Launch");
155
129
 
156
- setForkStrategy("reference");
157
130
  const fork = await forkConversationForRetrospective({
158
131
  conversationId: source.id,
159
132
  });
@@ -167,29 +140,23 @@ describe("referential forking", () => {
167
140
  expect(hasMessages(fork.id)).toBe(true);
168
141
  });
169
142
 
170
- test("presents the same messages a cloning fork does", async () => {
143
+ test("presents the same messages as the source through the fork point", async () => {
171
144
  const source = await seedSource("Launch");
172
145
 
173
- setForkStrategy("cloning");
174
- const cloned = await forkConversationForRetrospective({
175
- conversationId: source.id,
176
- });
177
- setForkStrategy("reference");
178
146
  const referenced = await forkConversationForRetrospective({
179
147
  conversationId: source.id,
180
148
  });
181
149
 
182
150
  expect(textOf(getMessages(referenced.id))).toEqual(
183
- textOf(getMessages(cloned.id)),
151
+ textOf(getMessages(source.id)),
184
152
  );
185
153
  expect(countMessagesAfter(referenced.id, null)).toBe(
186
- countMessagesAfter(cloned.id, null),
154
+ countMessagesAfter(source.id, null),
187
155
  );
188
156
  });
189
157
 
190
158
  test("rows written on the fork interleave after the inherited ones", async () => {
191
159
  const source = await seedSource("Launch");
192
- setForkStrategy("reference");
193
160
  const fork = await forkConversationForRetrospective({
194
161
  conversationId: source.id,
195
162
  });
@@ -206,7 +173,6 @@ describe("referential forking", () => {
206
173
 
207
174
  test("messages written on the source after the fork point are excluded", async () => {
208
175
  const source = await seedSource("Launch");
209
- setForkStrategy("reference");
210
176
  const fork = await forkConversationForRetrospective({
211
177
  conversationId: source.id,
212
178
  });
@@ -224,7 +190,6 @@ describe("referential forking", () => {
224
190
  const source = await seedSource("Launch");
225
191
  const sourceRows = getMessages(source.id);
226
192
 
227
- setForkStrategy("reference");
228
193
  const fork = await forkConversationForRetrospective({
229
194
  conversationId: source.id,
230
195
  throughMessageId: sourceRows[1]!.id,
@@ -239,7 +204,6 @@ describe("referential forking", () => {
239
204
  test("getMessagesAfter and countMessagesAfter cross the lineage boundary", async () => {
240
205
  const source = await seedSource("Launch");
241
206
  const sourceRows = getMessages(source.id);
242
- setForkStrategy("reference");
243
207
  const fork = await forkConversationForRetrospective({
244
208
  conversationId: source.id,
245
209
  });
@@ -255,7 +219,6 @@ describe("referential forking", () => {
255
219
 
256
220
  test("pagination returns the inherited rows", async () => {
257
221
  const source = await seedSource("Launch");
258
- setForkStrategy("reference");
259
222
  const fork = await forkConversationForRetrospective({
260
223
  conversationId: source.id,
261
224
  });
@@ -272,7 +235,6 @@ describe("referential forking", () => {
272
235
 
273
236
  test("deleting the source orphans the fork instead of blocking or cascading", async () => {
274
237
  const source = await seedSource("Launch");
275
- setForkStrategy("reference");
276
238
  const fork = await forkConversationForRetrospective({
277
239
  conversationId: source.id,
278
240
  });
@@ -291,7 +253,6 @@ describe("referential forking", () => {
291
253
 
292
254
  test("the gentle delete path orphans the same way", async () => {
293
255
  const source = await seedSource("Launch");
294
- setForkStrategy("reference");
295
256
  const fork = await forkConversationForRetrospective({
296
257
  conversationId: source.id,
297
258
  });
@@ -306,7 +267,6 @@ describe("referential forking", () => {
306
267
 
307
268
  test("an orphaned fork reports the loss so clients can explain it", async () => {
308
269
  const source = await seedSource("Launch");
309
- setForkStrategy("reference");
310
270
  const fork = await forkConversationForRetrospective({
311
271
  conversationId: source.id,
312
272
  });
@@ -322,10 +282,9 @@ describe("referential forking", () => {
322
282
  expect(isReferentialHistoryOrphaned(orphan)).toBe(true);
323
283
  });
324
284
 
325
- test("a cloning fork whose source is deleted is not reported as orphaned", async () => {
285
+ test("a copied user fork whose source is deleted is not reported as orphaned", async () => {
326
286
  const source = await seedSource("Launch");
327
- setForkStrategy("cloning");
328
- const cloned = await forkConversationForRetrospective({
287
+ const cloned = forkConversation({
329
288
  conversationId: source.id,
330
289
  });
331
290
 
@@ -339,7 +298,6 @@ describe("referential forking", () => {
339
298
 
340
299
  test("deleting a fork leaves its source alone", async () => {
341
300
  const source = await seedSource("Launch");
342
- setForkStrategy("reference");
343
301
  const fork = await forkConversationForRetrospective({
344
302
  conversationId: source.id,
345
303
  });
@@ -349,12 +307,66 @@ describe("referential forking", () => {
349
307
  expect(conversationIds()).toContain(source.id);
350
308
  expect(textOf(getMessages(source.id))).toHaveLength(4);
351
309
  });
310
+
311
+ test("loadRetrospectiveRunMessages attributes only owned rows on a reference fork", async () => {
312
+ const source = await seedSource("Launch");
313
+ const tip = getMessages(source.id).at(-1)!;
314
+ const fork = await forkConversationForRetrospective({
315
+ conversationId: source.id,
316
+ throughMessageId: tip.id,
317
+ conversationType: "background",
318
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
319
+ });
320
+
321
+ const instruction = await addMessage(
322
+ fork.id,
323
+ "user",
324
+ "review this conversation",
325
+ {
326
+ metadata: {
327
+ kind: MEMORY_RETROSPECTIVE_INSTRUCTION_KIND,
328
+ hidden: true,
329
+ },
330
+ skipIndexing: true,
331
+ },
332
+ );
333
+ const remember = await addMessage(fork.id, "assistant", "remembered a fact", {
334
+ skipIndexing: true,
335
+ });
336
+
337
+ const runRows = await loadRetrospectiveRunMessages(
338
+ fork.id,
339
+ MEMORY_RETROSPECTIVE_FORK_SOURCE,
340
+ );
341
+ expect(runRows?.map((m) => m.id)).toEqual([instruction.id, remember.id]);
342
+ expect(runRows?.every((m) => m.conversationId === fork.id)).toBe(true);
343
+ });
344
+
345
+ test("loadRetrospectiveRunMessages returns an empty owned tail on a reference fork with no run rows", async () => {
346
+ const source = await seedSource("Launch");
347
+ const tip = getMessages(source.id).at(-1)!;
348
+ const fork = await forkConversationForRetrospective({
349
+ conversationId: source.id,
350
+ throughMessageId: tip.id,
351
+ conversationType: "background",
352
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
353
+ });
354
+
355
+ // Lineage still presents the source prefix (those rows keep the source
356
+ // conversationId). Empty owned set is determined output, not
357
+ // indeterminate, and does not need a conversation-row strategy lookup.
358
+ expect(getMessages(fork.id).length).toBeGreaterThan(0);
359
+ const runRows = await loadRetrospectiveRunMessages(
360
+ fork.id,
361
+ MEMORY_RETROSPECTIVE_FORK_SOURCE,
362
+ );
363
+ expect(runRows).toEqual([]);
364
+ });
352
365
  });
353
366
 
354
367
  describe("referential forking with unfinalized rows", () => {
355
368
  beforeEach(() => {
356
369
  resetTables();
357
- clearConfig();
358
370
  });
359
371
 
360
372
  test("ancestor rows are hidden while unfinalized and appear once finalized", async () => {
@@ -366,7 +378,6 @@ describe("referential forking with unfinalized rows", () => {
366
378
  .where(eq(messages.id, midRow.id))
367
379
  .run();
368
380
 
369
- setForkStrategy("reference");
370
381
  const fork = await forkConversationForRetrospective({
371
382
  conversationId: source.id,
372
383
  });
@@ -404,7 +415,6 @@ describe("referential forking with unfinalized rows", () => {
404
415
  .where(eq(messages.id, tail.id))
405
416
  .run();
406
417
 
407
- setForkStrategy("reference");
408
418
  const fork = await forkConversationForRetrospective({
409
419
  conversationId: source.id,
410
420
  });
@@ -76,19 +76,25 @@ function resetTables(): void {
76
76
  db.run("DELETE FROM conversations");
77
77
  }
78
78
 
79
- /** Strip per-row identity so two forks of the same source compare equal. */
80
- function normalize(message: {
81
- role: string;
82
- content: unknown;
83
- createdAt: number;
84
- metadata: string | null;
85
- }) {
86
- return {
87
- role: message.role,
88
- content: message.content,
89
- createdAt: message.createdAt,
90
- metadata: message.metadata,
91
- };
79
+ /** Rows physically stored on a conversation, ignoring anything inherited. */
80
+ function ownedRowCount(conversationId: string): number {
81
+ return getDb()
82
+ .select()
83
+ .from(messages)
84
+ .where(eq(messages.conversationId, conversationId))
85
+ .all().length;
86
+ }
87
+
88
+ function contentOf(m: { role: string; content: unknown; createdAt: number }) {
89
+ return { role: m.role, content: m.content, createdAt: m.createdAt };
90
+ }
91
+
92
+ function renderWindow(
93
+ summary: string | null,
94
+ rows: Array<{ role: string; content: unknown; createdAt: number }>,
95
+ count: number,
96
+ ) {
97
+ return [summary, ...rows.slice(Math.min(count, rows.length)).map(contentOf)];
92
98
  }
93
99
 
94
100
  async function seedSource(title: string): Promise<{ id: string }> {
@@ -113,7 +119,7 @@ describe("forkConversationForRetrospective", () => {
113
119
  resetTables();
114
120
  });
115
121
 
116
- test("full fork is row-identical to the synchronous fork", async () => {
122
+ test("full fork presents the source window without copying rows", async () => {
117
123
  const source = await seedSource("Planning thread");
118
124
 
119
125
  const syncFork = forkConversation({
@@ -127,21 +133,20 @@ describe("forkConversationForRetrospective", () => {
127
133
  source: "memory-retrospective-fork",
128
134
  });
129
135
 
130
- // The forkSourceMessageId stamp points at the SOURCE message id (shared by
131
- // both forks), so the normalized rows must be byte-identical.
132
- expect(getMessages(asyncFork.id).map(normalize)).toEqual(
133
- getMessages(syncFork.id).map(normalize),
136
+ expect(asyncFork.forkStrategy).toBe("reference");
137
+ expect(ownedRowCount(asyncFork.id)).toBe(0);
138
+ expect(getMessages(asyncFork.id).map(contentOf)).toEqual(
139
+ getMessages(source.id).map(contentOf),
134
140
  );
135
141
  expect(asyncFork.forkParentConversationId).toBe(source.id);
136
142
  expect(asyncFork.forkParentMessageId).toBe(syncFork.forkParentMessageId);
137
- // Fresh ids — not the source's.
138
143
  const sourceIds = new Set(getMessages(source.id).map((m) => m.id));
139
- expect(getMessages(asyncFork.id).every((m) => !sourceIds.has(m.id))).toBe(
144
+ expect(getMessages(asyncFork.id).every((m) => sourceIds.has(m.id))).toBe(
140
145
  true,
141
146
  );
142
147
  });
143
148
 
144
- test("through-cutoff (truncated) fork matches the synchronous fork", async () => {
149
+ test("through-cutoff (truncated) fork bounds the inherited window", async () => {
145
150
  const source = await seedSource("Truncated thread");
146
151
  const sourceMessages = getMessages(source.id);
147
152
  const cutoff = sourceMessages[1]!.id;
@@ -159,8 +164,10 @@ describe("forkConversationForRetrospective", () => {
159
164
  source: "memory-retrospective-fork",
160
165
  });
161
166
 
162
- expect(getMessages(asyncFork.id).map(normalize)).toEqual(
163
- getMessages(syncFork.id).map(normalize),
167
+ expect(asyncFork.forkStrategy).toBe("reference");
168
+ expect(ownedRowCount(asyncFork.id)).toBe(0);
169
+ expect(getMessages(asyncFork.id).map(contentOf)).toEqual(
170
+ sourceMessages.slice(0, 2).map(contentOf),
164
171
  );
165
172
  expect(asyncFork.forkParentMessageId).toBe(syncFork.forkParentMessageId);
166
173
  });
@@ -193,7 +200,7 @@ describe("forkConversationForRetrospective", () => {
193
200
  ).toBe(false);
194
201
  });
195
202
 
196
- test("relinks attachments per-conversation, like the synchronous fork", async () => {
203
+ test("resolves attachments through source message ids", async () => {
197
204
  const source = createConversation("Attachment thread");
198
205
  const assistant = await addMessage(source.id, "assistant", "see mockup", {
199
206
  skipIndexing: true,
@@ -212,10 +219,11 @@ describe("forkConversationForRetrospective", () => {
212
219
  (m) => m.role === "assistant",
213
220
  );
214
221
  expect(forkAssistant).toBeDefined();
222
+ expect(forkAssistant!.id).toBe(assistant.id);
223
+ expect(ownedRowCount(asyncFork.id)).toBe(0);
215
224
  const forkAttachments = getAttachmentsForMessage(forkAssistant!.id);
216
225
  expect(forkAttachments).toHaveLength(1);
217
- // Scoped to the fork — a distinct attachment row from the source's.
218
- expect(forkAttachments[0]?.id).not.toBe(
226
+ expect(forkAttachments[0]?.id).toBe(
219
227
  getAttachmentsForMessage(assistant.id)[0]?.id,
220
228
  );
221
229
  });
@@ -321,11 +329,7 @@ describe("forkConversationForRetrospective — compacted source", () => {
321
329
  return { id: source.id, summary, compactedAt, compactedCount: 4, base };
322
330
  }
323
331
 
324
- function contentOf(m: { role: string; content: unknown; createdAt: number }) {
325
- return { role: m.role, content: m.content, createdAt: m.createdAt };
326
- }
327
-
328
- test("copies only the visible tail and renders identically to the source", async () => {
332
+ test("copies no rows and renders identically to the source", async () => {
329
333
  const source = await seedCompactedSource();
330
334
  const sourceRows = getMessages(source.id);
331
335
  const tip = sourceRows.at(-1)!;
@@ -337,33 +341,25 @@ describe("forkConversationForRetrospective — compacted source", () => {
337
341
  source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
338
342
  });
339
343
 
340
- // Physical rows: the visible tail only, each stamped with its source id.
344
+ expect(fork.forkStrategy).toBe("reference");
345
+ expect(ownedRowCount(fork.id)).toBe(0);
341
346
  const forkRows = getMessages(fork.id);
342
- expect(forkRows.map(contentOf)).toEqual(sourceRows.slice(4).map(contentOf));
343
- expect(
344
- forkRows.map(
345
- (m) =>
346
- (JSON.parse(m.metadata!) as { forkSourceMessageId: string })
347
- .forkSourceMessageId,
348
- ),
349
- ).toEqual(sourceRows.slice(4).map((m) => m.id));
347
+ expect(forkRows.map((m) => m.id)).toEqual(sourceRows.map((m) => m.id));
350
348
 
351
- // Fork row: inherited summary + timestamp, fork-local count of 0.
352
349
  expect(fork.contextSummary).toBe(source.summary);
353
- expect(fork.contextCompactedMessageCount).toBe(0);
350
+ expect(fork.contextCompactedMessageCount).toBe(4);
354
351
  expect(fork.contextCompactedAt).toBe(source.compactedAt);
355
352
  expect(fork.forkParentMessageId).toBe(tip.id);
356
353
 
357
- // Rendered history — summary + post-slice rows, assembled the way
358
- // `loadFromDb` does — is identical to the source's.
359
- const render = (
360
- summary: string | null,
361
- rows: Array<{ role: string; content: unknown; createdAt: number }>,
362
- count: number,
363
- ) => [summary, ...rows.slice(Math.min(count, rows.length)).map(contentOf)];
364
354
  expect(
365
- render(fork.contextSummary, forkRows, fork.contextCompactedMessageCount),
366
- ).toEqual(render(source.summary, sourceRows, source.compactedCount));
355
+ renderWindow(
356
+ fork.contextSummary,
357
+ forkRows,
358
+ fork.contextCompactedMessageCount,
359
+ ),
360
+ ).toEqual(
361
+ renderWindow(source.summary, sourceRows, source.compactedCount),
362
+ );
367
363
 
368
364
  // The synchronous user fork keeps the full physical history.
369
365
  const syncFork = forkConversation({
@@ -455,9 +451,21 @@ describe("forkConversationForRetrospective — compacted source", () => {
455
451
  source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
456
452
  });
457
453
 
458
- // The compaction slice composes with the cutoff: source row 5 only.
454
+ // Lineage includes the cutoff window; the inherited count hides the
455
+ // compacted prefix so the rendered tail is source row 5 only.
459
456
  const forkRows = getMessages(fork.id);
460
- expect(forkRows.map((m) => m.content)).toEqual([sourceRows[4]!.content]);
457
+ expect(ownedRowCount(fork.id)).toBe(0);
458
+ expect(fork.contextCompactedMessageCount).toBe(4);
459
+ expect(forkRows.map((m) => m.id)).toEqual(
460
+ sourceRows.slice(0, 5).map((m) => m.id),
461
+ );
462
+ expect(
463
+ renderWindow(
464
+ fork.contextSummary,
465
+ forkRows,
466
+ fork.contextCompactedMessageCount,
467
+ ).slice(1),
468
+ ).toEqual([contentOf(sourceRows[4]!)]);
461
469
  expect(fork.forkParentMessageId).toBe(sourceRows[4]!.id);
462
470
 
463
471
  // Derived seeding sees only the copied tail's slug; the wholesale graph
@@ -499,24 +507,24 @@ describe("forkConversationForRetrospective — compacted source", () => {
499
507
  expect(forkEvents[0]).toMatchObject({
500
508
  compactedAt: source.compactedAt,
501
509
  summary: source.summary,
502
- compactedMessageCount: 0,
510
+ compactedMessageCount: 4,
503
511
  });
504
512
 
505
- // A fork of the fork at its tip inherits the summary with the fork-local
506
- // count — no rows are hidden behind it.
513
+ // A user fork of the retrospective fork at its tip inherits the same
514
+ // summary and hidden-prefix count.
507
515
  const forkTip = getMessages(fork.id).at(-1)!;
508
516
  const grandchild = forkConversation({
509
517
  conversationId: fork.id,
510
518
  throughMessageId: forkTip.id,
511
519
  });
512
520
  expect(grandchild.contextSummary).toBe(source.summary);
513
- expect(grandchild.contextCompactedMessageCount).toBe(0);
521
+ expect(grandchild.contextCompactedMessageCount).toBe(4);
514
522
  expect(getMessages(grandchild.id)).toHaveLength(
515
523
  getMessages(fork.id).length,
516
524
  );
517
525
  });
518
526
 
519
- test("post-fork tail extraction still detects the copied boundary", async () => {
527
+ test("post-fork tail extraction attributes only owned rows", async () => {
520
528
  const source = await seedCompactedSource();
521
529
  const tip = getMessages(source.id).at(-1)!;
522
530
  const fork = await forkConversationForRetrospective({
@@ -526,11 +534,10 @@ describe("forkConversationForRetrospective — compacted source", () => {
526
534
  source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
527
535
  });
528
536
 
529
- const boundary = findForkBoundaryCreatedAt(getMessages(fork.id));
530
- expect(boundary).toBe(tip.createdAt);
537
+ // Referential forks stamp no copied-row metadata. The boundary helper
538
+ // returns null; ownership is what scopes the run.
539
+ expect(findForkBoundaryCreatedAt(getMessages(fork.id))).toBeNull();
531
540
 
532
- // A run-authored message lands after the boundary and is the only row
533
- // attributed to the run.
534
541
  const runMessage = await addMessage(fork.id, "user", "retro instruction", {
535
542
  skipIndexing: true,
536
543
  });
@@ -544,6 +551,7 @@ describe("forkConversationForRetrospective — compacted source", () => {
544
551
  MEMORY_RETROSPECTIVE_FORK_SOURCE,
545
552
  );
546
553
  expect(runRows?.map((m) => m.id)).toEqual([runMessage.id]);
554
+ expect(runRows?.every((m) => m.conversationId === fork.id)).toBe(true);
547
555
  });
548
556
 
549
557
  test("succeeds with an empty tail when the compaction covers the whole cutoff range", async () => {
@@ -573,18 +581,25 @@ describe("forkConversationForRetrospective — compacted source", () => {
573
581
  source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
574
582
  });
575
583
 
576
- expect(getMessages(fork.id)).toHaveLength(0);
584
+ expect(ownedRowCount(fork.id)).toBe(0);
585
+ expect(getMessages(fork.id)).toHaveLength(sourceRows.length);
577
586
  expect(fork.contextSummary).toBe("Everything summarized");
578
- expect(fork.contextCompactedMessageCount).toBe(0);
587
+ expect(fork.contextCompactedMessageCount).toBe(sourceRows.length);
579
588
  expect(fork.contextCompactedAt).toBe(tip.createdAt);
580
589
  expect(fork.forkParentMessageId).toBe(tip.id);
590
+ expect(
591
+ renderWindow(
592
+ fork.contextSummary,
593
+ getMessages(fork.id),
594
+ fork.contextCompactedMessageCount,
595
+ ),
596
+ ).toEqual(["Everything summarized"]);
581
597
  // Ageable despite copying no rows, so the startup orphan sweep (which
582
598
  // skips null `lastMessageAt` rows) can reclaim it after a crash.
583
599
  expect(fork.lastMessageAt).toBe(tip.createdAt);
584
600
 
585
- // With no stamped copied rows and the run's instruction opening the
586
- // conversation, every message is the run's own output — it still feeds
587
- // the success bookkeeping (dedup baseline, skill cards).
601
+ // The run's instruction is the first owned row. Lineage still presents
602
+ // the compacted source prefix, so attribution is by ownership.
588
603
  const runMessage = await addMessage(fork.id, "user", "retro instruction", {
589
604
  metadata: {
590
605
  kind: MEMORY_RETROSPECTIVE_INSTRUCTION_KIND,
@@ -653,8 +668,14 @@ describe("forkConversationForRetrospective — compacted source", () => {
653
668
  });
654
669
 
655
670
  // The source renders ["still visible", "fresh tail"] after its slice;
656
- // the fork must carry exactly those rows.
657
- expect(getMessages(fork.id).map((m) => m.content)).toEqual([
671
+ // the fork must render the same window.
672
+ expect(ownedRowCount(fork.id)).toBe(0);
673
+ expect(fork.contextCompactedMessageCount).toBe(1);
674
+ expect(
675
+ getMessages(fork.id)
676
+ .slice(fork.contextCompactedMessageCount)
677
+ .map((m) => m.content),
678
+ ).toEqual([
658
679
  [{ type: "text", text: "still visible" }],
659
680
  [{ type: "text", text: "fresh tail" }],
660
681
  ]);
@@ -705,9 +726,12 @@ describe("forkConversationForRetrospective — compacted source", () => {
705
726
  source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
706
727
  });
707
728
 
708
- expect(getMessages(fork.id).map((m) => m.content)).toEqual([
709
- [{ type: "text", text: "visible" }],
710
- ]);
729
+ expect(ownedRowCount(fork.id)).toBe(0);
730
+ expect(
731
+ getMessages(fork.id)
732
+ .slice(fork.contextCompactedMessageCount)
733
+ .map((m) => m.content),
734
+ ).toEqual([[{ type: "text", text: "visible" }]]);
711
735
  expect(fork.contextSummary).toBe("Tie summary");
712
736
  expect(fork.contextCompactedMessageCount).toBe(0);
713
737
  const forkEvents = getDb()
@@ -740,8 +764,9 @@ describe("forkConversationForRetrospective with unfinalized rows", () => {
740
764
  });
741
765
  const forkMessages = getMessages(fork.id);
742
766
 
743
- // Invariant: only finalized rows are copied, and the lineage anchor is a
744
- // row the fork actually holds.
767
+ // Lineage excludes the ancestor's in-flight row; the anchor is a
768
+ // finalized row the inherited window actually holds.
769
+ expect(ownedRowCount(fork.id)).toBe(0);
745
770
  expect(forkMessages).toHaveLength(3);
746
771
  expect(forkMessages.every((row) => row.finalized === 1)).toBe(true);
747
772
  expect(fork.forkParentMessageId).not.toBe(tail.id);
@@ -862,6 +862,29 @@ describe("trust-gating via channel capabilities", () => {
862
862
  expect(injected).not.toContain("CHANNEL CONSTRAINTS");
863
863
  });
864
864
 
865
+ test("vellum channel with Windows client OS injects PowerShell guidance", () => {
866
+ const caps: ChannelCapabilities = {
867
+ channel: "vellum",
868
+ dashboardCapable: true,
869
+ supportsDynamicUi: true,
870
+ supportsVoiceInput: true,
871
+ clientOS: "web",
872
+ };
873
+ const message: Message = {
874
+ role: "user",
875
+ content: [{ type: "text", text: "List my downloads" }],
876
+ };
877
+
878
+ const result = injectChannelCapabilityContext(message, caps, "windows");
879
+
880
+ expect(result).not.toBe(message);
881
+ const injected = (result.content[0] as { type: "text"; text: string }).text;
882
+ expect(injected).toContain("client_os: windows");
883
+ expect(injected).toContain("`host_bash` runs PowerShell");
884
+ expect(injected).toContain("Windows paths");
885
+ expect(injected).not.toContain("CHANNEL CONSTRAINTS");
886
+ });
887
+
865
888
  test("non-dashboard channel adds constraint rules preventing UI references", () => {
866
889
  const caps = resolveChannelCapabilities("telegram");
867
890
  const message: Message = {
@@ -36,6 +36,7 @@ describe("/model list", () => {
36
36
  expect(message).toContain("`balanced`");
37
37
  expect(message).toContain("`quality-optimized`");
38
38
  expect(message).toContain("`cost-optimized`");
39
+ expect(message).not.toContain("`balanced-backup`");
39
40
  });
40
41
 
41
42
  test("default profile descriptions follow the default provider's column", async () => {
@@ -71,6 +72,14 @@ describe("/model list", () => {
71
72
  });
72
73
 
73
74
  describe("/model switch", () => {
75
+ test("rejects a managed backup profile as a direct selection", async () => {
76
+ const result = await resolveSlash("/model balanced-backup");
77
+ expect(result.kind).toBe("unknown");
78
+ expect((result as { message: string }).message).toContain(
79
+ "Profile `balanced-backup` not found",
80
+ );
81
+ });
82
+
74
83
  test("rejects an unknown profile with the available set", async () => {
75
84
  const result = await resolveSlash("/model nope");
76
85
  expect(result.kind).toBe("unknown");