@vellumai/assistant 0.11.6 → 0.11.7-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 (478) 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/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  21. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
  22. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  23. package/node_modules/@vellumai/environments/package.json +2 -1
  24. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  25. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  27. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  28. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  29. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  30. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  32. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  33. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  34. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  35. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  36. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  37. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  38. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  39. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  40. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  41. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  42. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  43. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  44. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  45. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  46. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  47. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  48. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  49. package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  50. package/openapi.yaml +419 -55
  51. package/package.json +4 -1
  52. package/scripts/sync-llm-catalog.ts +6 -0
  53. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  54. package/src/__tests__/approval-card-edit.test.ts +153 -0
  55. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  56. package/src/__tests__/available-skills.test.ts +54 -0
  57. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  58. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  59. package/src/__tests__/channel-policy.test.ts +1 -3
  60. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  61. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  63. package/src/__tests__/config-schema.test.ts +2 -3
  64. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  65. package/src/__tests__/conversation-attachments.test.ts +0 -1
  66. package/src/__tests__/conversation-error.test.ts +44 -0
  67. package/src/__tests__/conversation-evictor.test.ts +19 -0
  68. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  69. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  70. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  71. package/src/__tests__/conversation-slash.test.ts +9 -0
  72. package/src/__tests__/conversation-title-service.test.ts +97 -0
  73. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  74. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  75. package/src/__tests__/credential-broker.test.ts +43 -420
  76. package/src/__tests__/credential-record-write-through.test.ts +78 -0
  77. package/src/__tests__/credential-routes.test.ts +17 -1
  78. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  79. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  80. package/src/__tests__/delete-propagation.test.ts +92 -2
  81. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  82. package/src/__tests__/discord-channel-config.test.ts +237 -0
  83. package/src/__tests__/edit-propagation.test.ts +42 -2
  84. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  85. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  86. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  87. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  88. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  89. package/src/__tests__/image-conversion.test.ts +38 -4
  90. package/src/__tests__/install-skill-routing.test.ts +25 -0
  91. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  92. package/src/__tests__/llm-resolver.test.ts +74 -0
  93. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  94. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  95. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  96. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  97. package/src/__tests__/mcp-cli.test.ts +23 -1
  98. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  99. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  100. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  101. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  102. package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
  103. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  104. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  105. package/src/__tests__/platform.test.ts +19 -1
  106. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  107. package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
  108. package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
  109. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  110. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  111. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  112. package/src/__tests__/schedule-retry.test.ts +183 -4
  113. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  115. package/src/__tests__/skill-load-tool.test.ts +34 -0
  116. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  117. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  118. package/src/__tests__/skills.test.ts +25 -0
  119. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  120. package/src/__tests__/terminal-tools.test.ts +2 -0
  121. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  122. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  123. package/src/__tests__/tool-audit.test.ts +111 -40
  124. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  125. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  126. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  127. package/src/__tests__/voice-config-update.test.ts +43 -0
  128. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  129. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  130. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  131. package/src/agent/loop.ts +31 -2
  132. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  133. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  134. package/src/avatar/avatar-manifest.ts +48 -100
  135. package/src/avatar/avatar-store.ts +7 -7
  136. package/src/avatar/ensure-raster.ts +146 -0
  137. package/src/avatar/resvg-lazy.test.ts +0 -1
  138. package/src/avatar/traits-png-sync.ts +9 -10
  139. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  140. package/src/backup/__tests__/paths.test.ts +6 -2
  141. package/src/backup/paths.ts +6 -4
  142. package/src/bundler/app-compiler.ts +2 -2
  143. package/src/bundler/compiler-tools.test.ts +36 -0
  144. package/src/bundler/compiler-tools.ts +54 -18
  145. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  146. package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
  147. package/src/calls/media-stream-stt-session.ts +2 -1
  148. package/src/calls/telephony-synthesis-language.ts +13 -5
  149. package/src/calls/voice-session-bridge.ts +19 -4
  150. package/src/channels/config.ts +23 -12
  151. package/src/cli/AGENTS.md +3 -1
  152. package/src/cli/commands/backup.help.ts +10 -13
  153. package/src/cli/commands/contacts.help.ts +16 -4
  154. package/src/cli/commands/contacts.ts +5 -1
  155. package/src/cli/commands/credentials.help.ts +1 -1
  156. package/src/cli/commands/inference-providers.ts +18 -53
  157. package/src/cli/commands/mcp.help.ts +6 -5
  158. package/src/cli/commands/mcp.ts +1 -1
  159. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  160. package/src/cli/commands/stt.help.ts +2 -2
  161. package/src/config/__tests__/backup-schema.test.ts +1 -1
  162. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  163. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  164. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  165. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  166. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  167. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  168. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  169. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  170. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  171. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  172. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  173. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  174. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  175. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  176. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  177. package/src/config/default-profile-catalog.ts +195 -10
  178. package/src/config/default-profile-names.ts +41 -0
  179. package/src/config/feature-flag-registry.json +34 -2
  180. package/src/config/inference-profile-validation.ts +11 -2
  181. package/src/config/llm-resolver.ts +25 -0
  182. package/src/config/loader.ts +3 -0
  183. package/src/config/managed-speech-defaults.ts +160 -10
  184. package/src/config/schema.ts +2 -0
  185. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  186. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  187. package/src/config/schemas/backup.ts +1 -1
  188. package/src/config/schemas/channels.ts +32 -0
  189. package/src/config/schemas/live-voice.ts +13 -4
  190. package/src/config/schemas/llm.ts +263 -15
  191. package/src/config/schemas/mcp.ts +10 -2
  192. package/src/config/schemas/memory-retrospective.ts +1 -19
  193. package/src/config/schemas/services.ts +24 -0
  194. package/src/config/schemas/stt.ts +128 -5
  195. package/src/config/seed-inference-profiles.ts +32 -3
  196. package/src/config/skill-state.ts +5 -0
  197. package/src/config/skills.ts +21 -0
  198. package/src/contacts/notify-contacts-changed.ts +5 -2
  199. package/src/context/compactor.ts +74 -1
  200. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  201. package/src/daemon/config-watcher.ts +2 -1
  202. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  203. package/src/daemon/conversation-agent-loop.ts +86 -10
  204. package/src/daemon/conversation-error.ts +25 -0
  205. package/src/daemon/conversation-evictor.ts +2 -4
  206. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  207. package/src/daemon/conversation-skill-tools.ts +5 -1
  208. package/src/daemon/conversation-slash.ts +2 -2
  209. package/src/daemon/conversation-store.ts +8 -1
  210. package/src/daemon/conversation-tool-setup.ts +37 -0
  211. package/src/daemon/conversation.ts +0 -4
  212. package/src/daemon/handlers/channel-config-result.ts +30 -0
  213. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  214. package/src/daemon/handlers/config-ingress.ts +9 -0
  215. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  216. package/src/daemon/handlers/config-telegram.ts +2 -5
  217. package/src/daemon/handlers/conversations.ts +0 -2
  218. package/src/daemon/handlers/skills.ts +25 -7
  219. package/src/daemon/lifecycle.ts +3 -1
  220. package/src/daemon/message-types/messages.ts +0 -2
  221. package/src/daemon/tool-setup-types.ts +5 -3
  222. package/src/daemon/trust-context-types.test.ts +33 -0
  223. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  224. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  225. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  226. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  227. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  228. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
  229. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  230. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  231. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  232. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  233. package/src/live-voice/live-voice-session-manager.ts +39 -2
  234. package/src/live-voice/live-voice-session.ts +224 -9
  235. package/src/live-voice/protocol.ts +187 -1
  236. package/src/messaging/providers/discord/api.ts +2 -1
  237. package/src/messaging/providers/discord/withdraw.ts +62 -0
  238. package/src/messaging/read-provider-metadata.ts +32 -0
  239. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  240. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  241. package/src/monitoring/db-integrity-sample.ts +1 -0
  242. package/src/monitoring/file-descriptors.ts +39 -6
  243. package/src/monitoring/process-memory.ts +37 -5
  244. package/src/monitoring/resource-sampler.ts +25 -8
  245. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  246. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  247. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  248. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  249. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  250. package/src/notifications/activity-failed-dedupe.ts +95 -0
  251. package/src/notifications/adapters/discord.ts +132 -0
  252. package/src/notifications/adapters/shared.ts +16 -0
  253. package/src/notifications/adapters/slack.ts +6 -1
  254. package/src/notifications/adapters/telegram.ts +5 -8
  255. package/src/notifications/approval-card-data.ts +94 -1
  256. package/src/notifications/background-failure-signal.ts +116 -0
  257. package/src/notifications/broadcaster.ts +3 -9
  258. package/src/notifications/copy-composer.ts +41 -8
  259. package/src/notifications/decision-engine.ts +55 -0
  260. package/src/notifications/destination-resolver.ts +45 -5
  261. package/src/notifications/emit-signal.ts +19 -5
  262. package/src/notifications/guardian-question-mode.ts +57 -0
  263. package/src/notifications/types.ts +2 -8
  264. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  265. package/src/oauth/connect-orchestrator.ts +16 -0
  266. package/src/oauth/seed-providers.ts +262 -0
  267. package/src/permissions/checker.test.ts +21 -0
  268. package/src/permissions/checker.ts +6 -0
  269. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  270. package/src/permissions/gateway-threshold-reader.ts +144 -10
  271. package/src/permissions/prompter.ts +0 -6
  272. package/src/permissions/types.ts +5 -4
  273. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  274. package/src/persistence/bulk-write-gate.ts +9 -10
  275. package/src/persistence/conversation-crud.ts +88 -134
  276. package/src/persistence/conversation-title-service.ts +43 -29
  277. package/src/persistence/embeddings/embedding-local.ts +14 -85
  278. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  279. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  280. package/src/persistence/lifecycle-events-store.ts +75 -5
  281. package/src/platform/platform-patch-queue.test.ts +368 -0
  282. package/src/platform/platform-patch-queue.ts +190 -0
  283. package/src/platform/sync-avatar.test.ts +527 -0
  284. package/src/platform/sync-avatar.ts +209 -0
  285. package/src/platform/sync-identity.test.ts +75 -0
  286. package/src/platform/sync-identity.ts +17 -110
  287. package/src/plugin-api/model-profiles.test.ts +2 -2
  288. package/src/plugin-api/model-profiles.ts +7 -7
  289. package/src/plugin-api/transcription-session.ts +1 -1
  290. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  291. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  292. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  293. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  294. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  295. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  296. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  297. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  298. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  299. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  300. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  301. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  302. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  303. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  304. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  305. package/src/plugins/defaults/memory/worker.ts +7 -0
  306. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  307. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  308. package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
  309. package/src/providers/connection-resolution.ts +2 -17
  310. package/src/providers/fallback-breaker.ts +389 -0
  311. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  312. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  313. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  314. package/src/providers/inference/adapter-factory.ts +246 -3
  315. package/src/providers/inference/auth.ts +15 -14
  316. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  317. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  318. package/src/providers/inference/resolve-auth.ts +1 -1
  319. package/src/providers/media-resolve.ts +2 -3
  320. package/src/providers/model-catalog.ts +100 -0
  321. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  322. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  323. package/src/providers/openai/api-error-normalization.ts +13 -0
  324. package/src/providers/openai/chat-completions-provider.ts +84 -0
  325. package/src/providers/opencode/client.test.ts +150 -0
  326. package/src/providers/opencode/client.ts +61 -0
  327. package/src/providers/platform-proxy/constants.ts +5 -0
  328. package/src/providers/provider-resolvability.ts +20 -0
  329. package/src/providers/provider-secret-catalog.ts +3 -2
  330. package/src/providers/retry.ts +956 -68
  331. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  332. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  333. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  334. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  335. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  336. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  337. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  338. package/src/providers/speech-to-text/resolve.ts +132 -24
  339. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  340. package/src/providers/types.ts +39 -0
  341. package/src/providers/usage-tracking.ts +12 -1
  342. package/src/providers/vellum/client.ts +29 -0
  343. package/src/providers/vellum-model-routing.test.ts +2 -0
  344. package/src/providers/vellum-model-routing.ts +3 -3
  345. package/src/runtime/AGENTS.md +1 -2
  346. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  347. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  348. package/src/runtime/agent-wake.ts +44 -5
  349. package/src/runtime/background-job-runner.ts +68 -50
  350. package/src/runtime/channel-readiness-service.ts +0 -39
  351. package/src/runtime/http-server.ts +12 -4
  352. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  353. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  354. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  355. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  356. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  357. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  358. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  359. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  360. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  361. package/src/runtime/routes/approval-routes.ts +0 -12
  362. package/src/runtime/routes/avatar-routes.ts +17 -30
  363. package/src/runtime/routes/backup-routes.ts +2 -2
  364. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  365. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  366. package/src/runtime/routes/contact-routes.ts +20 -15
  367. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  368. package/src/runtime/routes/conversation-routes.ts +2 -2
  369. package/src/runtime/routes/credential-routes.ts +2 -1
  370. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  371. package/src/runtime/routes/inbound-message-handler.ts +47 -48
  372. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  373. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  374. package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
  375. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  376. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  377. package/src/runtime/routes/index.ts +2 -0
  378. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  379. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  380. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  381. package/src/runtime/routes/inference-send-routes.ts +2 -2
  382. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  383. package/src/runtime/routes/integrations/discord.ts +119 -0
  384. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  385. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  386. package/src/runtime/routes/migration-routes.ts +3 -1
  387. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  388. package/src/runtime/routes/secret-routes.ts +7 -0
  389. package/src/runtime/routes/stt-routes.ts +38 -6
  390. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  391. package/src/runtime/routes/watch-routes.ts +9 -5
  392. package/src/runtime/sync/resource-sync-events.ts +2 -0
  393. package/src/runtime/trust-verdict-consumer.ts +3 -3
  394. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  395. package/src/schedule/retry-policy.ts +8 -8
  396. package/src/schedule/run-script.ts +14 -23
  397. package/src/schedule/schedule-recovery.ts +0 -1
  398. package/src/schedule/scheduler.ts +89 -51
  399. package/src/schedule/worker.ts +57 -0
  400. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  401. package/src/security/ces-rpc-record-backend.ts +123 -0
  402. package/src/security/oauth2.ts +7 -0
  403. package/src/security/openai-device-auth.ts +364 -0
  404. package/src/security/secure-keys.ts +15 -0
  405. package/src/skills/available-skills.ts +10 -1
  406. package/src/skills/catalog-install.ts +19 -1
  407. package/src/skills/inline-command-runner.ts +6 -2
  408. package/src/skills/platform-compatibility.test.ts +49 -0
  409. package/src/skills/platform-compatibility.ts +65 -0
  410. package/src/skills/tool-manifest.ts +20 -0
  411. package/src/stt/__tests__/roles.test.ts +152 -0
  412. package/src/stt/daemon-batch-transcriber.ts +4 -1
  413. package/src/stt/roles.ts +180 -0
  414. package/src/stt/stt-stream-session.ts +3 -2
  415. package/src/stt/types.ts +33 -3
  416. package/src/subagent/manager.ts +12 -0
  417. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  418. package/src/telemetry/live-voice-funnel.ts +12 -2
  419. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  420. package/src/telemetry/tool-audit.ts +83 -30
  421. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  422. package/src/telemetry/turn-outcome.ts +75 -4
  423. package/src/tools/client-os.ts +115 -0
  424. package/src/tools/computer-use/definitions.ts +11 -7
  425. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  426. package/src/tools/credentials/broker-types.ts +0 -71
  427. package/src/tools/credentials/broker.ts +6 -201
  428. package/src/tools/credentials/metadata-store.ts +65 -16
  429. package/src/tools/credentials/store.ts +2 -0
  430. package/src/tools/executor.ts +2 -8
  431. package/src/tools/host-shell.test.ts +75 -0
  432. package/src/tools/host-shell.ts +24 -0
  433. package/src/tools/host-terminal/host-shell.ts +15 -44
  434. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  435. package/src/tools/permission-checker.ts +73 -21
  436. package/src/tools/policy-context.ts +1 -0
  437. package/src/tools/shared/filesystem/image-read.ts +1 -1
  438. package/src/tools/skills/find-similar-skills.ts +10 -2
  439. package/src/tools/skills/load.ts +41 -6
  440. package/src/tools/skills/sandbox-runner.ts +8 -16
  441. package/src/tools/skills/skill-tool-factory.ts +1 -0
  442. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  443. package/src/tools/terminal/safe-env.ts +39 -9
  444. package/src/tools/terminal/shell.ts +12 -31
  445. package/src/tools/tool-approval-handler.ts +64 -10
  446. package/src/tools/tool-types.ts +10 -0
  447. package/src/tools/types.ts +19 -3
  448. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  449. package/src/tools/workflows/manage-workflows.ts +2 -2
  450. package/src/tools/workflows/run-workflow.test.ts +2 -1
  451. package/src/util/__tests__/file-use.test.ts +61 -0
  452. package/src/util/__tests__/process-table.test.ts +66 -0
  453. package/src/util/__tests__/process-tree.test.ts +46 -0
  454. package/src/util/errors.ts +1 -0
  455. package/src/util/file-use.ts +91 -0
  456. package/src/util/host-process.test.ts +17 -0
  457. package/src/util/host-process.ts +75 -0
  458. package/src/util/image-conversion.ts +39 -7
  459. package/src/util/platform.ts +32 -13
  460. package/src/util/process-table.ts +249 -0
  461. package/src/util/process-tree.ts +85 -129
  462. package/src/util/provider-error-patterns.ts +20 -0
  463. package/src/util/spawn.ts +9 -2
  464. package/src/watch/__tests__/watch-retro.test.ts +48 -1
  465. package/src/watch/watch-retro.ts +46 -4
  466. package/src/workspace/git-service.ts +4 -14
  467. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  468. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  469. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  470. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  471. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  472. package/src/workspace/migrations/registry.ts +8 -0
  473. package/src/__tests__/permission-types.test.ts +0 -17
  474. package/src/channels/gateway-discord-admission.ts +0 -32
  475. package/src/persistence/fork-message-copy.ts +0 -239
  476. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  477. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  478. package/src/security/oauth2-device-code.ts +0 -312
@@ -104,10 +104,6 @@ import {
104
104
  getSqliteFrom,
105
105
  getTelemetryDb,
106
106
  } from "./db-connection.js";
107
- import {
108
- copyForkMessagesViaSubprocess,
109
- type ForkIdPair,
110
- } from "./fork-message-copy.js";
111
107
  import {
112
108
  clearMessagesLexicalIndex,
113
109
  enqueueDeleteMessageLexical,
@@ -653,7 +649,7 @@ export interface ConversationRow {
653
649
  originInterface: string | null;
654
650
  forkParentConversationId: string | null;
655
651
  forkParentMessageId: string | null;
656
- /** `"reference"` on referential forks; `"cloning"` or null on copied ones. */
652
+ /** `"reference"` on referential forks; null on copied ones. */
657
653
  forkStrategy: string | null;
658
654
  isAutoTitle: number;
659
655
  scheduleJobId: string | null;
@@ -1723,25 +1719,36 @@ function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
1723
1719
  }
1724
1720
 
1725
1721
  /**
1726
- * Resolve the fork id + timestamp of the LAST assistant message among the
1727
- * copied rows. The synchronous copy loop tracks this inline; the off-loop
1728
- * subprocess copy does not, so the async fork derives it from the id map.
1722
+ * How many leading rows of the inherited window sit behind the source's
1723
+ * compaction boundary, counted in `getMessages` load order (`createdAt`
1724
+ * only). The retrospective fork is referential: it reads the source's rows
1725
+ * through the fork point, prefix included, so it must carry this count for
1726
+ * the render to hide the same rows the source hides. Getting the count
1727
+ * wrong does not lose data; it re-shows compacted history the summary
1728
+ * already covers, or hides a visible row when a cutoff drops part of the
1729
+ * hidden prefix from the lineage.
1730
+ *
1731
+ * Rows after the cutoff are skipped. The first in-window row that is not
1732
+ * hidden ends the prefix, matching the source render's slice.
1729
1733
  */
1730
- function latestForkedAssistantFrom(
1731
- messagesToCopy: MessageRow[],
1732
- forkedMessageIds: Map<string, string>,
1733
- ): { messageId: string; messageAt: number } | null {
1734
- for (let i = messagesToCopy.length - 1; i >= 0; i--) {
1735
- const message = messagesToCopy[i]!;
1736
- if (message.role !== "assistant") {
1734
+ function compactedPrefixLengthInWindow(
1735
+ loadOrderIds: readonly string[],
1736
+ compactedCount: number,
1737
+ windowIds: ReadonlySet<string>,
1738
+ ): number {
1739
+ const hiddenIds = new Set(loadOrderIds.slice(0, compactedCount));
1740
+ let count = 0;
1741
+ for (const id of loadOrderIds) {
1742
+ if (!windowIds.has(id)) {
1737
1743
  continue;
1738
1744
  }
1739
- const forkedMessageId = forkedMessageIds.get(message.id);
1740
- if (forkedMessageId) {
1741
- return { messageId: forkedMessageId, messageAt: message.createdAt };
1745
+ if (hiddenIds.has(id)) {
1746
+ count += 1;
1747
+ continue;
1742
1748
  }
1749
+ break;
1743
1750
  }
1744
- return null;
1751
+ return count;
1745
1752
  }
1746
1753
 
1747
1754
  /**
@@ -1749,35 +1756,30 @@ function latestForkedAssistantFrom(
1749
1756
  * which forks the source conversation's visible window into a throwaway
1750
1757
  * background conversation on a hot path that must not stall the daemon.
1751
1758
  *
1752
- * Only rows at-or-after the inherited compaction boundary are copied. The
1753
- * retrospective wake always runs under guardian trust, whose history render
1754
- * slices `contextCompactedMessageCount` rows off the front and prepends the
1755
- * summary on `contextSummary` presence alone — so the fork carries the
1756
- * inherited summary with a compacted count of 0 and renders identically to
1757
- * the source (summary + tail) without materializing rows the agent cannot
1758
- * see. The user-facing {@link forkConversation} keeps the full physical
1759
- * history: user forks are long-lived and browsable, and untrusted-actor
1760
- * views render the persisted history unsliced.
1759
+ * The fork is referential: it holds only the rows written after it is
1760
+ * created and reads the inherited window through `fork_parent_message_id`.
1761
+ * The retrospective wake always runs under guardian trust, whose history
1762
+ * render slices `contextCompactedMessageCount` rows off the front and
1763
+ * prepends the summary on `contextSummary` presence alone, so the fork
1764
+ * carries the source's own count and renders identically to the source
1765
+ * (summary + tail). The user-facing {@link forkConversation} keeps the full
1766
+ * physical history: user forks are long-lived and browsable, and
1767
+ * untrusted-actor views render the persisted history unsliced.
1761
1768
  *
1762
- * The dominant cost — copying the visible tail's message rows runs OFF the
1763
- * event loop in a `sqlite3` subprocess (see
1764
- * {@link copyForkMessagesViaSubprocess}), so `/healthz` and gateway IPC stay
1765
- * responsive during the copy. The cheap tail (conversation row, attachment
1766
- * relink, memory-state seeding) runs in-process and reuses
1767
- * {@link populateForkContentsInProcess}, the same helper the synchronous fork
1768
- * uses, so the two paths cannot drift on that logic. The cutoff/boundary
1769
- * computation mirrors {@link forkConversation} and is pinned by a parity
1770
- * test.
1769
+ * Creating the fork is a single conversation row plus memory-state
1770
+ * seeding. There is no message-row copy and no attachment relink: lineage
1771
+ * reads keep the source message ids, so `getAttachmentsForMessage` resolves
1772
+ * against the source rows. The cutoff/boundary computation mirrors
1773
+ * {@link forkConversation} and is pinned by a parity test.
1771
1774
  *
1772
1775
  * The disk-view projection (`syncMessageToDisk`) is intentionally skipped: the
1773
1776
  * fork is GC'd after the retrospective pass and never browsed, and the agent
1774
1777
  * reads it from the database, not the on-disk JSONL.
1775
1778
  *
1776
- * Atomicity spans two connections (the in-process row/tail and the subprocess
1777
- * copy), so a mid-flight failure can leave a partial fork. The partial is
1778
- * deleted best-effort on error; a crash between phases is reclaimed by the
1779
- * worker's startup orphan sweep. Callers only ever observe a fully-built fork
1780
- * because the returned promise resolves after every phase commits.
1779
+ * A mid-flight failure deletes the partial best-effort; a crash after the
1780
+ * conversation row lands is reclaimed by the worker's startup orphan sweep.
1781
+ * Callers only ever observe a fully-built fork because the returned promise
1782
+ * resolves after every phase commits.
1781
1783
  */
1782
1784
  export async function forkConversationForRetrospective(params: {
1783
1785
  conversationId: string;
@@ -1850,10 +1852,6 @@ export async function forkConversationForRetrospective(params: {
1850
1852
  const inheritsLatestCompaction =
1851
1853
  inheritedCompaction != null &&
1852
1854
  inheritedCompaction.compactedAt === sourceConversation.contextCompactedAt;
1853
- // Copy only the visible tail: rows behind the inherited summary are never
1854
- // rendered on this fork (the retrospective wake runs under guardian trust,
1855
- // which slices `contextCompactedMessageCount` rows off the front), so
1856
- // copying them would hold the write lock for rows the agent cannot see.
1857
1855
  // The drop-set is the first `compactedMessageCount` rows in LOAD order —
1858
1856
  // the exact rows the source's render hides — rather than a positional
1859
1857
  // slice of the re-sorted array, whose tie order can differ at the
@@ -1862,43 +1860,29 @@ export async function forkConversationForRetrospective(params: {
1862
1860
  const hiddenRowIds = new Set(
1863
1861
  loadOrderIds.slice(0, inheritedCompaction?.compactedMessageCount ?? 0),
1864
1862
  );
1865
- // Read straight from config rather than taking a caller-supplied strategy:
1866
- // how a fork is materialized is a workspace-wide storage decision, and a
1867
- // per-call override would let two callers disagree about it on the same
1868
- // workspace.
1869
- const isReferential =
1870
- getConfig().memory.retrospective.forkStrategy === "reference";
1871
- // A referential fork copies nothing: its inherited window is read back
1872
- // through `forkParentMessageId` by the lineage resolver.
1873
- const rowsToCopy = isReferential
1874
- ? []
1875
- : messagesToCopy.filter((message) => !hiddenRowIds.has(message.id));
1876
- // The compacted prefix is dropped from a copied fork, so its own row count
1877
- // behind the boundary is 0. A referential fork reads the source's rows
1878
- // through the fork point, prefix included, so it must carry the source's
1879
- // own count for the render to hide the same rows the source hides. Getting
1880
- // this wrong does not lose data, it re-shows compacted history the summary
1881
- // already covers.
1882
- const forkCompactedMessageCount = isReferential
1883
- ? (inheritedCompaction?.compactedMessageCount ?? 0)
1884
- : 0;
1863
+ const windowIds = new Set(messagesToCopy.map((message) => message.id));
1864
+ // A referential fork reads the source's rows through the fork point,
1865
+ // prefix included, so it carries the hidden-prefix length of that
1866
+ // inherited window (not the source's raw count, which can hide a visible
1867
+ // row when a cutoff drops part of the prefix from the lineage).
1868
+ const forkCompactedMessageCount = compactedPrefixLengthInWindow(
1869
+ loadOrderIds,
1870
+ inheritedCompaction?.compactedMessageCount ?? 0,
1871
+ windowIds,
1872
+ );
1873
+ // Visible source rows in the inherited window. Passed to memory seeding
1874
+ // so a truncated fork re-derives `everInjected` from the child's rendered
1875
+ // tail; attachments stay on the source ids and are not relinked.
1876
+ const visibleSourceMessages = messagesToCopy.filter(
1877
+ (message) => !hiddenRowIds.has(message.id),
1878
+ );
1885
1879
  const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
1886
1880
  const forkTitle =
1887
1881
  params.title ?? `${sourceConversation.title ?? "Untitled"} (Fork)`;
1888
1882
  const parentGroupId = getConversationGroupId(conversationId);
1883
+ const forkedMessageIds = new Map<string, string>();
1889
1884
 
1890
- // Pre-generate the id map in JS so the same map drives the off-loop copy and
1891
- // the in-process attachment relink that follows.
1892
- const idPairs: ForkIdPair[] = rowsToCopy.map((message) => ({
1893
- oldId: message.id,
1894
- newId: uuidv7(),
1895
- }));
1896
- const forkedMessageIds = new Map<string, string>(
1897
- idPairs.map((pair) => [pair.oldId, pair.newId]),
1898
- );
1899
-
1900
- // Phase 1 (in-process, tiny): create the fork conversation row + lineage so
1901
- // the subprocess connection sees it before inserting messages. The whole
1885
+ // Phase 1: create the fork conversation row + lineage. The whole
1902
1886
  // transaction is the retry unit — it rolls back atomically on contention, so
1903
1887
  // re-running it (with a fresh conversation id) is safe.
1904
1888
  const fork = await withSqliteRetry(
@@ -1916,18 +1900,13 @@ export async function forkConversationForRetrospective(params: {
1916
1900
  forkParentMessageId,
1917
1901
  // Stamped at creation so the startup orphan sweep (which only
1918
1902
  // considers rows with a non-null `lastMessageAt`) can age this
1919
- // fork even if the daemon crashes before the copy or the
1920
- // retrospective instruction lands — including the empty-tail
1921
- // fork, which copies no rows at all. Phase 3 re-derives the same
1922
- // value from the copied rows when the tail is non-empty.
1903
+ // fork even if the daemon crashes before the retrospective
1904
+ // instruction lands — including the empty-tail fork, which
1905
+ // copies no rows at all.
1923
1906
  lastMessageAt: boundaryMessageCreatedAt,
1924
1907
  contextSummary: inheritedCompaction?.summary ?? null,
1925
- // On a copied fork zero of its own rows sit behind the boundary
1926
- // (the compacted prefix is not copied). The summary still
1927
- // renders: the history render keys on `contextSummary` presence,
1928
- // not on this count.
1929
1908
  contextCompactedMessageCount: forkCompactedMessageCount,
1930
- forkStrategy: isReferential ? REFERENTIAL_FORK_STRATEGY : null,
1909
+ forkStrategy: REFERENTIAL_FORK_STRATEGY,
1931
1910
  contextCompactedAt: inheritedCompaction?.compactedAt ?? null,
1932
1911
  slackContextCompactionWatermarkTs: inheritsLatestCompaction
1933
1912
  ? sourceConversation.slackContextCompactionWatermarkTs
@@ -1951,39 +1930,13 @@ export async function forkConversationForRetrospective(params: {
1951
1930
  );
1952
1931
 
1953
1932
  try {
1954
- // Phase 2 (off the event loop): copy the message rows in a sqlite3
1955
- // subprocess so the daemon stays responsive during the heavy copy. The
1956
- // referential fork has no rows to copy, which is the whole point of it:
1957
- // the write burst this phase exists to keep off the event loop does not
1958
- // happen at all.
1959
- if (!isReferential) {
1960
- const copy = await copyForkMessagesViaSubprocess({
1961
- forkConversationId: fork.id,
1962
- idPairs,
1963
- });
1964
- if (!copy.ok) {
1965
- throw new Error(
1966
- `fork message copy failed (${copy.backend}): ${copy.error ?? "unknown"}`,
1967
- );
1968
- }
1969
- }
1970
-
1971
- // Phase 3 (in-process): attachments + memory-state seeding, reusing the
1972
- // same helper as the synchronous fork. Disk-view projection is skipped.
1933
+ // Phase 2 (in-process): memory-state seeding. Disk-view projection and
1934
+ // attachment relink are skipped: the fork owns no copied rows, and
1935
+ // lineage reads keep the source message ids.
1973
1936
  //
1974
- // The populate transaction reads (attachment links) before its first
1975
- // write, so a deferred BEGIN would take its snapshot as a reader and any
1976
- // concurrent writer committing before the first write fails the
1977
- // read→write upgrade with SQLITE_BUSY_SNAPSHOT — a hard error
1978
- // `busy_timeout` cannot wait out, which would discard the entire batch
1979
- // copy from phase 2. `behavior: "immediate"` acquires the write lock at
1980
- // BEGIN so the snapshot postdates it, and the retry absorbs residual
1981
- // contention: the transaction rolls back atomically, so re-running it is
1982
- // safe.
1983
- const latestForkedAssistant = latestForkedAssistantFrom(
1984
- rowsToCopy,
1985
- forkedMessageIds,
1986
- );
1937
+ // `behavior: "immediate"` acquires the write lock at BEGIN so the
1938
+ // snapshot postdates it, and the retry absorbs residual contention: the
1939
+ // transaction rolls back atomically, so re-running it is safe.
1987
1940
  await withSqliteRetry(
1988
1941
  () =>
1989
1942
  db.transaction(
@@ -1991,30 +1944,29 @@ export async function forkConversationForRetrospective(params: {
1991
1944
  populateForkContentsInProcess({
1992
1945
  fork,
1993
1946
  sourceConversationId: sourceConversation.id,
1994
- messagesToCopy: rowsToCopy,
1947
+ messagesToCopy: visibleSourceMessages,
1995
1948
  forkedMessageIds,
1996
- latestForkedAssistant,
1949
+ latestForkedAssistant: null,
1997
1950
  // Both conditions, for the same reason as `forkConversation`:
1998
1951
  // a tip boundary no longer implies an equal window once the
1999
1952
  // unfinalized filter dropped rows from the slice.
2000
1953
  isFullHistoryFork:
2001
1954
  copyBoundaryIndex === sourceMessages.length - 1 &&
2002
1955
  messagesToCopy.length === copyBoundaryIndex + 1,
2003
- // The copied range already starts at the visible window.
2004
- inheritedCompactedMessageCount: forkCompactedMessageCount,
1956
+ // The seeding range already starts at the visible window.
1957
+ inheritedCompactedMessageCount: 0,
2005
1958
  skipCompactionLedgerCopy: inheritedCompaction != null,
2006
1959
  });
2007
1960
  // The fork owns none of the source's ledger events — their
2008
- // counts index source-space row positions, and this fork row
2009
- // stores a count of 0. Seed a single event mirroring the fork
2010
- // row's compaction fields whenever a compaction is inherited
2011
- // (even when the hidden prefix fell outside the cutoff and
2012
- // nothing was dropped), so the ledger and the row cache agree
2013
- // and a fork of this fork inherits the summary with the
2014
- // fork-local count. With no inherited compaction the default
2015
- // ledger copy is a natural no-op (no source event is
2016
- // at-or-before the boundary). Rolls back with the transaction,
2017
- // so the retry unit stays atomic.
1961
+ // counts index source-space row positions. Seed a single event
1962
+ // mirroring the fork row's compaction fields whenever a
1963
+ // compaction is inherited (even when the hidden prefix fell
1964
+ // outside the cutoff and nothing was dropped), so the ledger
1965
+ // and the row cache agree and a fork of this fork inherits the
1966
+ // summary with the fork-local count. With no inherited
1967
+ // compaction the default ledger copy is a natural no-op (no
1968
+ // source event is at-or-before the boundary). Rolls back with
1969
+ // the transaction, so the retry unit stays atomic.
2018
1970
  if (inheritedCompaction) {
2019
1971
  appendCompactionEvent(fork.id, {
2020
1972
  compactedAt: inheritedCompaction.compactedAt,
@@ -4047,7 +3999,9 @@ export function updateMessageMetadata(
4047
3999
  .from(messages)
4048
4000
  .where(eq(messages.id, messageId))
4049
4001
  .get();
4050
- const existing = row?.metadata ? JSON.parse(row.metadata) : {};
4002
+ // Sanitized like the transactional sibling above: a malformed stored
4003
+ // envelope must not fail the update that is trying to stamp the row.
4004
+ const existing = row?.metadata ? safeParseRecord(row.metadata) : {};
4051
4005
  db.update(messages)
4052
4006
  .set({ metadata: JSON.stringify({ ...existing, ...updates }) })
4053
4007
  .where(eq(messages.id, messageId))
@@ -146,21 +146,22 @@ export function deriveDeterministicTitle(context: TitleContext): string {
146
146
  * inbound whose only content is a guardian access-request card: without this
147
147
  * the sidebar shows a permanent "Generating title…". Persisted as
148
148
  * `AUTO_TITLE_DETERMINISTIC` so a later genuine turn can still upgrade it, and
149
- * broadcast so connected clients converge. Returns whether the title changed.
149
+ * broadcast so connected clients converge. Returns the title the conversation
150
+ * carries once it settles, and whether this call is what set it.
150
151
  */
151
152
  export function applyDeterministicTitleIfReplaceable(
152
153
  conversationId: string,
153
154
  title: string,
154
- ): boolean {
155
+ ): { title: string; updated: boolean } {
155
156
  const conversation = getConversation(conversationId);
156
157
  if (conversation && !isReplaceableTitle(conversation.title)) {
157
- return false;
158
+ return { title: conversation.title!, updated: false };
158
159
  }
159
160
  const cleaned =
160
161
  truncateTitle(title.replace(/\s+/g, " ").trim()) || UNTITLED_FALLBACK;
161
162
  updateConversationTitle(conversationId, cleaned, AUTO_TITLE_DETERMINISTIC);
162
163
  publishConversationTitleChanged(conversationId, cleaned);
163
- return true;
164
+ return { title: cleaned, updated: true };
164
165
  }
165
166
 
166
167
  // ── Title generation ─────────────────────────────────────────────────
@@ -195,19 +196,21 @@ export async function generateAndPersistConversationTitle(
195
196
  return { title: conversation.title!, updated: false };
196
197
  }
197
198
 
199
+ const prompt = buildTitlePrompt(context, userMessage, assistantResponse);
200
+ // A prompt carrying no text gives the model nothing to title from: an
201
+ // image-only turn submits an empty prompt, and the forced title tool makes
202
+ // the model fabricate a topic rather than decline.
203
+ if (prompt.trim() === "") {
204
+ return settleForDeterministicTitle(conversationId, context);
205
+ }
206
+
198
207
  const provider =
199
208
  params.provider ?? (await getConfiguredProvider("conversationTitle"));
200
209
  if (!provider) {
201
- // No provider available — fall back to context-derived title or untitled.
202
- // Deterministic, so keep it upgradeable by a later generation pass.
203
- const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
204
- updateConversationTitle(conversationId, fallback, AUTO_TITLE_DETERMINISTIC);
205
- publishConversationTitleChanged(conversationId, fallback);
206
210
  logRetryableFallback(params, "no_provider");
207
- return { title: fallback, updated: true };
211
+ return settleForDeterministicTitle(conversationId, context);
208
212
  }
209
213
 
210
- const prompt = buildTitlePrompt(context, userMessage, assistantResponse);
211
214
  const title = await generateTitleViaLLM(
212
215
  provider,
213
216
  prompt,
@@ -227,21 +230,26 @@ export async function generateAndPersistConversationTitle(
227
230
  return { title, updated: true };
228
231
  }
229
232
 
230
- // No text in response — use fallback
231
- // Re-check replaceability before persisting (race guard — same as the
232
- // text-response path above). A concurrent custom rename may have landed
233
- // while the LLM request was in-flight; writing unconditionally would
234
- // clobber the user's intent.
235
- const currentForFallback = getConversation(conversationId);
236
- if (currentForFallback && !canReplaceTitle(currentForFallback)) {
237
- return { title: currentForFallback.title!, updated: false };
238
- }
239
-
240
- const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
241
- updateConversationTitle(conversationId, fallback, AUTO_TITLE_DETERMINISTIC);
242
- publishConversationTitleChanged(conversationId, fallback);
243
233
  logRetryableFallback(params, "empty_output");
244
- return { title: fallback, updated: true };
234
+ return settleForDeterministicTitle(conversationId, context);
235
+ }
236
+
237
+ /**
238
+ * Settle a conversation on a title derived from its creation context, for the
239
+ * outcomes that produce no LLM title: the provider is unreachable, the model
240
+ * declined, or the prompt had no text to title from. Clearing a loading
241
+ * placeholder is worth doing; overwriting a title that already names the work
242
+ * (a background job's bootstrap title) is not, which is why this defers to
243
+ * `applyDeterministicTitleIfReplaceable` rather than writing directly.
244
+ */
245
+ function settleForDeterministicTitle(
246
+ conversationId: string,
247
+ context: TitleContext | undefined,
248
+ ): { title: string; updated: boolean } {
249
+ return applyDeterministicTitleIfReplaceable(
250
+ conversationId,
251
+ deriveFallbackTitle(context) ?? UNTITLED_FALLBACK,
252
+ );
245
253
  }
246
254
 
247
255
  // ── Serial title-generation queue ────────────────────────────────────
@@ -531,11 +539,17 @@ function buildTitlePrompt(
531
539
  }
532
540
  }
533
541
 
534
- if (userMessage) {
535
- parts.push(`User: ${stripThinkingTags(userMessage)}`);
542
+ // Trim after stripping so a turn carrying nothing but thinking tags reads as
543
+ // no text at all, the same as an image-only or whitespace-only prompt.
544
+ const userText = userMessage ? stripThinkingTags(userMessage).trim() : "";
545
+ if (userText) {
546
+ parts.push(`User: ${userText}`);
536
547
  }
537
- if (assistantResponse) {
538
- parts.push(`Assistant: ${stripThinkingTags(assistantResponse)}`);
548
+ const assistantText = assistantResponse
549
+ ? stripThinkingTags(assistantResponse).trim()
550
+ : "";
551
+ if (assistantText) {
552
+ parts.push(`Assistant: ${assistantText}`);
539
553
  }
540
554
 
541
555
  return parts.join("\n");
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  existsSync,
3
- readdirSync,
4
3
  readFileSync,
5
4
  rmSync,
6
5
  unlinkSync,
@@ -14,6 +13,10 @@ import {
14
13
  getEmbeddingModelsDir,
15
14
  getEmbedWorkerPidPath,
16
15
  } from "../../util/platform.js";
16
+ import {
17
+ listProcessTable,
18
+ readRawProcessCommand,
19
+ } from "../../util/process-table.js";
17
20
  import { PromiseGuard } from "../../util/promise-guard.js";
18
21
  import { workerComputeEnv } from "../../util/worker-compute.js";
19
22
  import { workerMemoryEnv } from "../../util/worker-memory.js";
@@ -155,77 +158,7 @@ function pid1OwnsEmbedWorkers(): boolean {
155
158
  if (process.pid === 1) {
156
159
  return true;
157
160
  }
158
- let cmd: string;
159
- try {
160
- cmd = readFileSync("/proc/1/cmdline", "utf8").split("\0").join(" ");
161
- } catch {
162
- const result = Bun.spawnSync({
163
- cmd: ["ps", "-ww", "-p", "1", "-o", "command="],
164
- stdout: "pipe",
165
- stderr: "ignore",
166
- });
167
- if (result.exitCode !== 0) {
168
- return false;
169
- }
170
- cmd = new TextDecoder().decode(result.stdout);
171
- }
172
- return cmd.includes(DAEMON_ENTRYPOINT_MARKER);
173
- }
174
-
175
- /** Enumerate `(pid, ppid, rawCommand)` rows from Linux `/proc`. */
176
- function listProcessRowsFromProc(): {
177
- pid: number;
178
- ppid: number;
179
- cmd: string;
180
- }[] {
181
- const rows: { pid: number; ppid: number; cmd: string }[] = [];
182
- for (const entry of readdirSync("/proc")) {
183
- const pid = Number(entry);
184
- if (!Number.isInteger(pid) || pid <= 0) {
185
- continue;
186
- }
187
- try {
188
- // `stat` field 4 is ppid, but `comm` (field 2) may contain spaces or
189
- // parens, so parse from the last ')' and a weird comm cannot shift fields.
190
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
191
- const after = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
192
- const ppid = Number(after[1]);
193
- const cmd = readFileSync(`/proc/${pid}/cmdline`, "utf8")
194
- .split("\0")
195
- .filter(Boolean)
196
- .join(" ");
197
- if (Number.isInteger(ppid)) {
198
- rows.push({ pid, ppid, cmd });
199
- }
200
- } catch {
201
- // Process exited between readdir and read, so skip it.
202
- }
203
- }
204
- return rows;
205
- }
206
-
207
- /** Enumerate `(pid, ppid, rawCommand)` rows via `ps` (macOS / no `/proc`). */
208
- function listProcessRowsFromPs(): { pid: number; ppid: number; cmd: string }[] {
209
- // `-ww` disables column-width truncation. Without it, macOS `ps` clips the
210
- // command field to the terminal width, which can cut off the workerPath
211
- // argument and hide a genuine match. Same flag is used by
212
- // daemon-control.ts:123 for exactly this reason.
213
- const result = Bun.spawnSync({
214
- cmd: ["ps", "-A", "-ww", "-o", "pid=,ppid=,command="],
215
- stdout: "pipe",
216
- stderr: "ignore",
217
- });
218
- if (result.exitCode !== 0) {
219
- return [];
220
- }
221
- const rows: { pid: number; ppid: number; cmd: string }[] = [];
222
- for (const line of new TextDecoder().decode(result.stdout).split("\n")) {
223
- const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
224
- if (m) {
225
- rows.push({ pid: Number(m[1]), ppid: Number(m[2]), cmd: m[3].trim() });
226
- }
227
- }
228
- return rows;
161
+ return readRawProcessCommand(1)?.includes(DAEMON_ENTRYPOINT_MARKER) ?? false;
229
162
  }
230
163
 
231
164
  /**
@@ -248,22 +181,17 @@ export function listWorkerProcesses(
248
181
  workerPath: string,
249
182
  model?: string,
250
183
  ): WorkerProcess[] {
251
- let rows: { pid: number; ppid: number; cmd: string }[];
252
184
  try {
253
- rows = listProcessRowsFromProc();
185
+ return listProcessTable()
186
+ .filter(
187
+ (row) =>
188
+ row.command.includes(workerPath) &&
189
+ (!model || row.command.split(/\s+/).includes(model)),
190
+ )
191
+ .map(({ pid, ppid }) => ({ pid, ppid }));
254
192
  } catch {
255
- rows = listProcessRowsFromPs();
193
+ return [];
256
194
  }
257
- return rows
258
- .filter(
259
- (r) =>
260
- r.cmd.includes(workerPath) &&
261
- // An argv token, not a substring: `foo/bar-small` must not match a
262
- // `foo/bar-small-v2` worker, or a backend for the shorter name would
263
- // reclaim the longer one's live worker. Model names carry no spaces.
264
- (!model || r.cmd.split(/\s+/).includes(model)),
265
- )
266
- .map((r) => ({ pid: r.pid, ppid: r.ppid }));
267
195
  }
268
196
 
269
197
  /**
@@ -510,6 +438,7 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
510
438
 
511
439
  const proc = Bun.spawn({
512
440
  cmd: [bunPath, "--smol", workerPath, this.model, modelCacheDir],
441
+ windowsHide: true,
513
442
  env: { ...workerMemoryEnv(), ...workerComputeEnv() },
514
443
  stdin: "pipe",
515
444
  stdout: "pipe",
@@ -102,6 +102,7 @@ async function downloadAndExtract(
102
102
  // Extract tarball, stripping the leading "package/" directory
103
103
  const proc = Bun.spawn({
104
104
  cmd: ["tar", "xzf", tmpTar, "-C", targetDir, "--strip-components=1"],
105
+ windowsHide: true,
105
106
  stdout: "ignore",
106
107
  stderr: "pipe",
107
108
  });
@@ -58,6 +58,56 @@ describe("lifecycle-events-store", () => {
58
58
  });
59
59
  });
60
60
 
61
+ test("carries the optional attributes onto the wire payload", () => {
62
+ const event = recordLifecycleEvent("permission_prompt:bash", {
63
+ toolName: "bash",
64
+ riskLevel: "high",
65
+ riskThreshold: "low",
66
+ surface: "slack",
67
+ conversationId: "conv-xyz",
68
+ });
69
+ expect(event).not.toBeNull();
70
+
71
+ const rows = outboxRows();
72
+ expect(rows).toHaveLength(1);
73
+ // The conversation is stamped on the row too, so deleting the conversation
74
+ // deletes the pending event with it.
75
+ expect(rows[0]!.conversation_id).toBe("conv-xyz");
76
+ expect(JSON.parse(rows[0]!.payload)).toEqual({
77
+ type: "lifecycle",
78
+ daemon_event_id: event!.id,
79
+ event_name: "permission_prompt:bash",
80
+ recorded_at: event!.createdAt,
81
+ assistant_version: APP_VERSION,
82
+ tool_name: "bash",
83
+ risk_level: "high",
84
+ risk_threshold: "low",
85
+ surface: "slack",
86
+ conversation_id: "conv-xyz",
87
+ });
88
+ });
89
+
90
+ test("omits attributes that are unset or empty", () => {
91
+ // The wire schema requires at least one character, and one failing field
92
+ // makes the server drop the whole event.
93
+ const event = recordLifecycleEvent("permission_prompt:bash", {
94
+ toolName: "bash",
95
+ riskLevel: "high",
96
+ surface: "",
97
+ });
98
+
99
+ expect(JSON.parse(outboxRows()[0]!.payload)).toEqual({
100
+ type: "lifecycle",
101
+ daemon_event_id: event!.id,
102
+ event_name: "permission_prompt:bash",
103
+ recorded_at: event!.createdAt,
104
+ assistant_version: APP_VERSION,
105
+ tool_name: "bash",
106
+ risk_level: "high",
107
+ });
108
+ expect(outboxRows()[0]!.conversation_id).toBeNull();
109
+ });
110
+
61
111
  test("buildLifecycleTelemetryEvent stamps the record-time binary version", () => {
62
112
  expect(buildLifecycleTelemetryEvent("id-1", "hatch", 1234)).toEqual({
63
113
  type: "lifecycle",