@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
@@ -42,6 +42,7 @@
42
42
 
43
43
  import {
44
44
  addMessage,
45
+ type AgentLoopExitReason,
45
46
  type ContentBlock,
46
47
  type ConversationRow,
47
48
  deleteConversation,
@@ -51,8 +52,10 @@ import {
51
52
  } from "@vellumai/plugin-api";
52
53
 
53
54
  import {
55
+ type ClientOs,
54
56
  type InterfaceId,
55
57
  isInteractiveInterface,
58
+ parseClientOs,
56
59
  parseInterfaceId,
57
60
  } from "../../../channels/types.js";
58
61
  import { isV3TierActive } from "../../../config/memory-v3-gate.js";
@@ -88,7 +91,6 @@ import {
88
91
  MEMORY_RETROSPECTIVE_FORK_SOURCE,
89
92
  MEMORY_RETROSPECTIVE_GROUP_ID,
90
93
  MEMORY_RETROSPECTIVE_INSTRUCTION_KIND,
91
- MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT,
92
94
  MEMORY_RETROSPECTIVE_ORIGIN,
93
95
  MEMORY_RETROSPECTIVE_SOURCE,
94
96
  SKILL_MANAGEMENT_SKILL_ID,
@@ -128,6 +130,14 @@ export const STALE_SOURCE_PROCESSING_OVERRIDE_MS = 6 * 60 * 60 * 1000;
128
130
  /** Watchdog check_name for the per-run retrospective outcome counter. */
129
131
  const MEMORY_RETROSPECTIVE_RUN_CHECK_NAME = "memory_retrospective_run";
130
132
 
133
+ /**
134
+ * The agent-loop exit that means the MODEL ended the run: it answered without
135
+ * asking for another tool. Under every other exit something ended the run for
136
+ * it, so whatever it had said by then is a fragment of a review rather than a
137
+ * verdict on the window.
138
+ */
139
+ const MODEL_DRIVEN_STOP_EXIT_REASON: AgentLoopExitReason = "no_tool_calls";
140
+
131
141
  export type MemoryRetrospectiveOutcome =
132
142
  | { kind: "disabled" }
133
143
  | { kind: "no_new_messages" }
@@ -142,6 +152,12 @@ export type MemoryRetrospectiveOutcome =
142
152
  cutoffMessageId: string;
143
153
  newMessageCount: number;
144
154
  followUpJobIds: string[];
155
+ /**
156
+ * The pass reviewed its window and had nothing durable to save, so it
157
+ * advanced the cursor without writing a memory. Separates the two
158
+ * shapes a healthy run takes, which the outcome kind alone conflates.
159
+ */
160
+ noFindings: boolean;
145
161
  };
146
162
 
147
163
  export async function memoryRetrospectiveJob(
@@ -176,14 +192,20 @@ export async function memoryRetrospectiveJob(
176
192
  // resolved model) is visible without log access. The emitter itself
177
193
  // never throws — the run's outcome must reach the jobs worker
178
194
  // regardless.
179
- const emitRunOutcome = (outcome: string, reason?: string): void => {
195
+ const emitRunOutcome = (
196
+ outcome: string,
197
+ detail?: { reason?: string; noFindings?: boolean },
198
+ ): void => {
180
199
  try {
181
200
  recordWatchdogEvent({
182
201
  checkName: MEMORY_RETROSPECTIVE_RUN_CHECK_NAME,
183
202
  value: 1,
184
203
  detail: {
185
204
  outcome,
186
- ...(reason ? { reason: reason.slice(0, 200) } : {}),
205
+ ...(detail?.reason ? { reason: detail.reason.slice(0, 200) } : {}),
206
+ ...(detail?.noFindings !== undefined
207
+ ? { noFindings: detail.noFindings }
208
+ : {}),
187
209
  },
188
210
  });
189
211
  } catch {
@@ -199,15 +221,17 @@ export async function memoryRetrospectiveJob(
199
221
  enforceUserActivityGate: true,
200
222
  });
201
223
  } catch (err) {
202
- emitRunOutcome("error", err instanceof Error ? err.message : String(err));
224
+ emitRunOutcome("error", {
225
+ reason: err instanceof Error ? err.message : String(err),
226
+ });
203
227
  throw err;
204
228
  }
205
- emitRunOutcome(
206
- outcome.kind,
207
- outcome.kind === "wake_failed" || outcome.kind === "no_usable_output"
208
- ? outcome.reason
209
- : undefined,
210
- );
229
+ emitRunOutcome(outcome.kind, {
230
+ ...(outcome.kind === "wake_failed" || outcome.kind === "no_usable_output"
231
+ ? { reason: outcome.reason }
232
+ : {}),
233
+ ...(outcome.kind === "invoked" ? { noFindings: outcome.noFindings } : {}),
234
+ });
211
235
  return outcome;
212
236
  }
213
237
 
@@ -396,16 +420,16 @@ export async function runForkBasedRetrospective(
396
420
  // advances to `cutoffMessageId`, causing the next retrospective to
397
421
  // reprocess (and potentially re-`remember`) those same turns.
398
422
  //
399
- // The fork copies only the source's visible tail and carries the inherited
400
- // compaction summary on its own row (with a fork-local compacted count of
401
- // 0). Compacted source ⇒ summary + tail visible to the agent natively.
423
+ // The fork is referential: it carries the inherited compaction summary
424
+ // and the source's hidden-prefix count, and reads the source's rows
425
+ // through the fork point. Compacted source ⇒ summary + tail visible to
426
+ // the agent natively.
402
427
  let forkConversationRow: Awaited<
403
428
  ReturnType<typeof forkConversationForRetrospective>
404
429
  >;
405
430
  try {
406
- // Async variant: the source message-row copy runs off the event loop in a
407
- // sqlite3 subprocess so this background pass cannot freeze the daemon's
408
- // event loop (health probes / gateway IPC) on a large database.
431
+ // Referential fork: one conversation row plus memory-state seeding.
432
+ // There is no source message-row copy.
409
433
  forkConversationRow = await forkConversationForRetrospective({
410
434
  conversationId: sourceConversationId,
411
435
  throughMessageId: cutoffMessageId,
@@ -493,6 +517,7 @@ export async function runForkBasedRetrospective(
493
517
  // persisted message — the wake's hint sandwich would only duplicate it.
494
518
  let wakeSucceeded = false;
495
519
  let failureReason: string | undefined;
520
+ let wakeExitReason: AgentLoopExitReason | undefined;
496
521
  let threw: unknown;
497
522
  try {
498
523
  const result = await wakeAgentForOpportunity({
@@ -562,6 +587,7 @@ export async function runForkBasedRetrospective(
562
587
  });
563
588
  wakeSucceeded = result.invoked;
564
589
  failureReason = result.reason;
590
+ wakeExitReason = result.exitReason;
565
591
  } catch (err) {
566
592
  threw = err;
567
593
  failureReason = err instanceof Error ? err.message : String(err);
@@ -576,15 +602,21 @@ export async function runForkBasedRetrospective(
576
602
  // went live, not that the run produced anything. The agent loop swallows
577
603
  // provider rejections into a normal no-output return, an exhausted output
578
604
  // budget can stop a run before any visible text or tool call, and a
579
- // model may reply with analysis (or nothing) without saving. Advancement
580
- // past the window therefore requires POSITIVE evidence from THIS run,
581
- // one of:
605
+ // model may stop mid-review without saving. Advancement past the window
606
+ // therefore requires POSITIVE evidence from THIS run, one of:
582
607
  // - a memory-writing tool call on the fork's post-boundary tail whose
583
608
  // execution verifiably succeeded (matching non-error tool_result), or
584
- // - the explicit no-findings reply the instruction mandates (an
585
- // assistant text block that is exactly the sentinel phrase), with no
586
- // memory-writing tool attempts at all; a run that attempted a save
587
- // and failed cannot advance by also claiming no findings.
609
+ // - a reviewed-and-nothing-to-save pass: the model ENDED the run by
610
+ // answering in its own words (its final assistant row carries text),
611
+ // attempted no memory write at all, and the loop ended because IT
612
+ // stopped asking for tools rather than because something cut the run
613
+ // short. A run that attempted a save and failed cannot advance by
614
+ // talking instead, a run whose narration went live but whose final
615
+ // response was empty has not concluded, and a run the provider or
616
+ // the output ceiling ended mid-review has not reviewed its window.
617
+ // The proof of an empty-handed review is the shape of the run, never the
618
+ // wording of the reply: a model that reaches its own end with nothing to
619
+ // write has reviewed the window, in whatever words it says so.
588
620
  // The evidence read is run-specific by construction
589
621
  // (`loadRetrospectiveRunMessages` scopes to rows after the fork
590
622
  // boundary), so a prior run's persisted saves can never satisfy it, and
@@ -596,8 +628,9 @@ export async function runForkBasedRetrospective(
596
628
  // remains retryable.
597
629
  const runEvidence = await collectRetrospectiveRunEvidence(forkId);
598
630
  const reviewedNoFindings =
599
- runEvidence.explicitNoFindings &&
600
- runEvidence.durableToolAttemptCount === 0;
631
+ runEvidence.committedTextReply &&
632
+ runEvidence.durableToolAttemptCount === 0 &&
633
+ wakeExitReason === MODEL_DRIVEN_STOP_EXIT_REASON;
601
634
  if (runEvidence.durableToolCallCount === 0 && !reviewedNoFindings) {
602
635
  log.warn(
603
636
  {
@@ -605,9 +638,12 @@ export async function runForkBasedRetrospective(
605
638
  forkId,
606
639
  newMessageCount: newMessages.length,
607
640
  durableToolAttempts: runEvidence.durableToolAttemptCount,
641
+ committedTextReply: runEvidence.committedTextReply,
642
+ exitReason: wakeExitReason ?? null,
608
643
  },
609
- "memory-retrospective (fork): run produced neither a verified durable write nor an explicit no-findings reply; leaving window retryable",
644
+ "memory-retrospective (fork): run produced neither a verified durable write nor a completed empty-handed review; leaving window retryable",
610
645
  );
646
+ failureReason = describeUnusableRun(runEvidence, wakeExitReason);
611
647
  } else {
612
648
  return await finalizeSuccessfulRetrospective({
613
649
  config,
@@ -618,11 +654,11 @@ export async function runForkBasedRetrospective(
618
654
  prior,
619
655
  priorRemembers,
620
656
  runRemembers: runEvidence.remembers,
657
+ noFindings: reviewedNoFindings,
621
658
  logFields: {
622
659
  kind: "fork",
623
660
  windowStartTimestamp,
624
661
  durationMs: Date.now() - startedAtMs,
625
- noFindings: reviewedNoFindings,
626
662
  },
627
663
  });
628
664
  }
@@ -642,12 +678,15 @@ export async function runForkBasedRetrospective(
642
678
  }
643
679
 
644
680
  if (wakeSucceeded) {
681
+ // Reached only through the evidence gate above, which set `failureReason`
682
+ // to why the run could not consume its window.
645
683
  return {
646
684
  kind: "no_usable_output",
647
- reason: failureReason ?? "run persisted no memory-writing tool call",
685
+ reason: failureReason,
648
686
  conversationId: forkId,
649
687
  };
650
688
  }
689
+
651
690
  return {
652
691
  kind: "wake_failed",
653
692
  reason: failureReason,
@@ -655,6 +694,26 @@ export async function runForkBasedRetrospective(
655
694
  };
656
695
  }
657
696
 
697
+ /**
698
+ * Why a run that went live still could not consume its window, phrased for
699
+ * the job row's `last_error` and the CLI. The distinction it draws is the one
700
+ * an operator needs: a lost memory write reads differently from a review that
701
+ * never reached a conclusion, and the fork is deleted on failure, so this
702
+ * string is what survives the run.
703
+ */
704
+ function describeUnusableRun(
705
+ evidence: { durableToolAttemptCount: number; committedTextReply: boolean },
706
+ exitReason: AgentLoopExitReason | undefined,
707
+ ): string {
708
+ if (evidence.durableToolAttemptCount > 0) {
709
+ return `run attempted ${evidence.durableToolAttemptCount} memory write(s), none of which persisted a successful result`;
710
+ }
711
+ if (!evidence.committedTextReply) {
712
+ return "run committed neither a memory write nor a concluding reply";
713
+ }
714
+ return `run replied without saving anything, but ended on ${exitReason ?? "no terminal exit"} rather than a completed review`;
715
+ }
716
+
658
717
  function enqueueFollowUpJobs(): string[] {
659
718
  const followUpJobIds: string[] = [];
660
719
  for (const jobType of FOLLOW_UP_JOB_TYPES) {
@@ -711,6 +770,8 @@ interface SourceParityPins {
711
770
  *
712
771
  * `toolContextPin.transportInterface` — the interface the source's most
713
772
  * recent live turns ran on (see {@link resolveSourceLiveInterface}).
773
+ * `toolContextPin.clientOs` is recovered from the same persisted user-message
774
+ * metadata, with the transport interface as a fallback.
714
775
  * `channelCapabilities` is left unset: desktop/web HTTP turns never set
715
776
  * channel capabilities, and for channel-routed sources (whose live turns do
716
777
  * carry them) every tool gate resolves identically under
@@ -745,6 +806,7 @@ function resolveSourceParityPins(
745
806
  // with an unmappable channel stay undefined (their live turns were
746
807
  // clientless either way).
747
808
  const transportInterface = recovered ?? (channelRouted ? undefined : "web");
809
+ const clientOs = resolveSourceLiveClientOs(sliceMessages, transportInterface);
748
810
  const hasNoClient =
749
811
  transportInterface == null || !isInteractiveInterface(transportInterface);
750
812
  const personaOverride: SystemPromptPersonaOverride = channelRouted
@@ -763,6 +825,7 @@ function resolveSourceParityPins(
763
825
  toolContextPin: {
764
826
  hasNoClient,
765
827
  transportInterface,
828
+ clientOs,
766
829
  requestOrigin: MEMORY_RETROSPECTIVE_ORIGIN,
767
830
  },
768
831
  };
@@ -814,6 +877,37 @@ function resolveSourceLiveInterface(
814
877
  );
815
878
  }
816
879
 
880
+ /** Pin the source's live client OS so OS-gated tools match on wake. */
881
+ function resolveSourceLiveClientOs(
882
+ sliceMessages: Array<{ role: string; metadata: string | null }>,
883
+ transportInterface: InterfaceId | undefined,
884
+ ): ClientOs | undefined {
885
+ for (let i = sliceMessages.length - 1; i >= 0; i--) {
886
+ const row = sliceMessages[i]!;
887
+ if (row.role !== "user" || !row.metadata) {
888
+ continue;
889
+ }
890
+ let meta: unknown;
891
+ try {
892
+ meta = JSON.parse(row.metadata);
893
+ } catch {
894
+ continue;
895
+ }
896
+ if (!meta || typeof meta !== "object") {
897
+ continue;
898
+ }
899
+ const { clientOsFromRequest, client } = meta as Record<string, unknown>;
900
+ if (clientOsFromRequest !== true || !client || typeof client !== "object") {
901
+ continue;
902
+ }
903
+ const clientOs = parseClientOs((client as Record<string, unknown>).os);
904
+ if (clientOs) {
905
+ return clientOs;
906
+ }
907
+ }
908
+ return parseClientOs(transportInterface) ?? undefined;
909
+ }
910
+
817
911
  type PriorRetrospective = NonNullable<
818
912
  ReturnType<typeof findMostRecentRetrospectiveFor>
819
913
  >;
@@ -862,6 +956,8 @@ async function finalizeSuccessfulRetrospective(args: {
862
956
  * advancement is exactly the evidence folded into the log.
863
957
  */
864
958
  runRemembers: string[];
959
+ /** Whether the run advanced on a reviewed-and-nothing-to-save pass. */
960
+ noFindings: boolean;
865
961
  /** Per-kind extras for the success log line (e.g. `kind`, fork anchor). */
866
962
  logFields: Record<string, unknown>;
867
963
  }): Promise<MemoryRetrospectiveOutcome> {
@@ -874,6 +970,7 @@ async function finalizeSuccessfulRetrospective(args: {
874
970
  prior,
875
971
  priorRemembers,
876
972
  runRemembers,
973
+ noFindings,
877
974
  logFields,
878
975
  } = args;
879
976
 
@@ -901,6 +998,7 @@ async function finalizeSuccessfulRetrospective(args: {
901
998
  cutoffMessageId,
902
999
  newMessageCount,
903
1000
  priorRememberCount: priorRemembers.length,
1001
+ noFindings,
904
1002
  ...logFields,
905
1003
  },
906
1004
  "memory-retrospective invoked",
@@ -911,6 +1009,7 @@ async function finalizeSuccessfulRetrospective(args: {
911
1009
  cutoffMessageId,
912
1010
  newMessageCount,
913
1011
  followUpJobIds,
1012
+ noFindings,
914
1013
  };
915
1014
  }
916
1015
 
@@ -1119,12 +1218,14 @@ async function collectRetrospectiveRunEvidence(
1119
1218
  /** Memory-writing tool calls the run attempted, regardless of outcome. */
1120
1219
  durableToolAttemptCount: number;
1121
1220
  /**
1122
- * The run replied with exactly the mandated no-findings sentinel text
1123
- * ({@link MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT}) in a persisted assistant
1124
- * text block. Strict whole-block equality: prose that merely mentions the
1125
- * phrase does not qualify, so an analysis-only reply stays unusable.
1221
+ * The run ENDED by answering in its own words: the last persisted
1222
+ * assistant row carries a text block with non-whitespace content. Any
1223
+ * wording qualifies, so a pass that found nothing durable proves it
1224
+ * reviewed the window by replying, not by reproducing a phrase; but the
1225
+ * reply must be the run's final word, so narration followed by an empty
1226
+ * last response does not qualify.
1126
1227
  */
1127
- explicitNoFindings: boolean;
1228
+ committedTextReply: boolean;
1128
1229
  }> {
1129
1230
  const conv = await getConversation(conversationId);
1130
1231
  const runMessages = await loadRetrospectiveRunMessages(
@@ -1136,7 +1237,7 @@ async function collectRetrospectiveRunEvidence(
1136
1237
  remembers: [],
1137
1238
  durableToolCallCount: 0,
1138
1239
  durableToolAttemptCount: 0,
1139
- explicitNoFindings: false,
1240
+ committedTextReply: false,
1140
1241
  };
1141
1242
  }
1142
1243
  const succeededIds = collectSuccessfulToolResultIds(runMessages);
@@ -1144,46 +1245,34 @@ async function collectRetrospectiveRunEvidence(
1144
1245
  remembers: extractRememberContents(runMessages, succeededIds),
1145
1246
  durableToolCallCount: countDurableToolUses(runMessages, succeededIds),
1146
1247
  durableToolAttemptCount: countDurableToolUses(runMessages, null),
1147
- explicitNoFindings: hasExplicitNoFindingsReply(runMessages),
1248
+ committedTextReply: hasCommittedTextReply(runMessages),
1148
1249
  };
1149
1250
  }
1150
1251
 
1151
1252
  /**
1152
- * Whether any persisted assistant row on the run's tail carries a text block
1153
- * that is exactly the no-findings sentinel (after trimming). The instruction
1154
- * template mandates this exact reply for a reviewed-and-nothing-to-save
1155
- * pass, making it the positive persisted artifact that distinguishes a
1156
- * legitimate no-findings review from an empty or unusable response.
1253
+ * Whether the LAST persisted assistant row on the run's tail carries a text
1254
+ * block with non-whitespace content. Paired with a model-driven stop and
1255
+ * zero memory-write attempts, that closing reply is the persisted artifact
1256
+ * of a pass that read its window and had nothing to save: the model spoke
1257
+ * and then chose to end the run. Reading only the final row separates it
1258
+ * from a run whose narration went live but whose actual conclusion was
1259
+ * empty, as well as from a response that committed nothing at all
1260
+ * (thinking-only output, an empty content array).
1157
1261
  */
1158
- function hasExplicitNoFindingsReply(messages: MessageLike[]): boolean {
1159
- for (const msg of messages) {
1262
+ function hasCommittedTextReply(messages: MessageLike[]): boolean {
1263
+ for (let i = messages.length - 1; i >= 0; i--) {
1264
+ const msg = messages[i]!;
1160
1265
  if (msg.role !== "assistant") {
1161
1266
  continue;
1162
1267
  }
1163
- let blocks: unknown = msg.content;
1164
- if (typeof blocks === "string") {
1165
- try {
1166
- blocks = JSON.parse(blocks);
1167
- } catch {
1168
- continue;
1169
- }
1170
- }
1171
- if (!Array.isArray(blocks)) {
1172
- continue;
1173
- }
1174
- for (const block of blocks) {
1175
- if (!block || typeof block !== "object") {
1176
- continue;
1177
- }
1178
- const b = block as Record<string, unknown>;
1179
- if (
1180
- b.type === "text" &&
1181
- typeof b.text === "string" &&
1182
- b.text.trim() === MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT
1183
- ) {
1184
- return true;
1185
- }
1268
+ const blocks = parseMessageBlocks(msg);
1269
+ if (blocks === null) {
1270
+ return false;
1186
1271
  }
1272
+ return blocks.some(
1273
+ (b) =>
1274
+ b.type === "text" && typeof b.text === "string" && b.text.trim() !== "",
1275
+ );
1187
1276
  }
1188
1277
  return false;
1189
1278
  }
@@ -1199,22 +1288,7 @@ function collectSuccessfulToolResultIds(messages: MessageLike[]): Set<string> {
1199
1288
  if (msg.role !== "user") {
1200
1289
  continue;
1201
1290
  }
1202
- let blocks: unknown = msg.content;
1203
- if (typeof blocks === "string") {
1204
- try {
1205
- blocks = JSON.parse(blocks);
1206
- } catch {
1207
- continue;
1208
- }
1209
- }
1210
- if (!Array.isArray(blocks)) {
1211
- continue;
1212
- }
1213
- for (const block of blocks) {
1214
- if (!block || typeof block !== "object") {
1215
- continue;
1216
- }
1217
- const b = block as Record<string, unknown>;
1291
+ for (const b of parseMessageBlocks(msg) ?? []) {
1218
1292
  // guard:allow-tool-result-only: success evidence for locally-executed
1219
1293
  // durable memory tools; server-side web_search_tool_result never
1220
1294
  // corresponds to a durable write and carries no is_error flag.
@@ -1246,22 +1320,7 @@ function countDurableToolUses(
1246
1320
  if (msg.role !== "assistant") {
1247
1321
  continue;
1248
1322
  }
1249
- let blocks: unknown = msg.content;
1250
- if (typeof blocks === "string") {
1251
- try {
1252
- blocks = JSON.parse(blocks);
1253
- } catch {
1254
- continue;
1255
- }
1256
- }
1257
- if (!Array.isArray(blocks)) {
1258
- continue;
1259
- }
1260
- for (const block of blocks) {
1261
- if (!block || typeof block !== "object") {
1262
- continue;
1263
- }
1264
- const b = block as Record<string, unknown>;
1323
+ for (const b of parseMessageBlocks(msg) ?? []) {
1265
1324
  if (
1266
1325
  b.type === "tool_use" &&
1267
1326
  DURABLE_RETROSPECTIVE_TOOLS.has(String(b.name)) &&
@@ -1280,6 +1339,32 @@ interface MessageLike {
1280
1339
  content: string | ContentBlock[];
1281
1340
  }
1282
1341
 
1342
+ /**
1343
+ * Parse a message row's content into its block objects, or `null` when the
1344
+ * content is malformed (unparseable JSON, not an array). Non-object entries
1345
+ * are dropped. Every evidence reader in this module goes through this so
1346
+ * malformed rows degrade the same way everywhere: skipped, not propagated.
1347
+ */
1348
+ function parseMessageBlocks(
1349
+ msg: MessageLike,
1350
+ ): Record<string, unknown>[] | null {
1351
+ let blocks: unknown = msg.content;
1352
+ if (typeof blocks === "string") {
1353
+ try {
1354
+ blocks = JSON.parse(blocks);
1355
+ } catch {
1356
+ return null;
1357
+ }
1358
+ }
1359
+ if (!Array.isArray(blocks)) {
1360
+ return null;
1361
+ }
1362
+ return blocks.filter(
1363
+ (block): block is Record<string, unknown> =>
1364
+ typeof block === "object" && block !== null,
1365
+ );
1366
+ }
1367
+
1283
1368
  /**
1284
1369
  * Scan an array of message rows for `tool_use` blocks where `name` is
1285
1370
  * `"remember"` and return the `input.content` strings in order. Robust to
@@ -1294,22 +1379,7 @@ function extractRememberContents(
1294
1379
  if (msg.role !== "assistant") {
1295
1380
  continue;
1296
1381
  }
1297
- let blocks: unknown = msg.content;
1298
- if (typeof blocks === "string") {
1299
- try {
1300
- blocks = JSON.parse(blocks);
1301
- } catch {
1302
- continue;
1303
- }
1304
- }
1305
- if (!Array.isArray(blocks)) {
1306
- continue;
1307
- }
1308
- for (const block of blocks) {
1309
- if (!block || typeof block !== "object") {
1310
- continue;
1311
- }
1312
- const b = block as Record<string, unknown>;
1382
+ for (const b of parseMessageBlocks(msg) ?? []) {
1313
1383
  if (b.type !== "tool_use") {
1314
1384
  continue;
1315
1385
  }
@@ -27,7 +27,6 @@
27
27
  */
28
28
 
29
29
  import { getLogger } from "./logging.js";
30
- import { MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT } from "./memory-retrospective-constants.js";
31
30
  import { getWorkspaceDir } from "./paths.js";
32
31
  import { loadPromptOverride } from "./prompt-override.js";
33
32
 
@@ -45,11 +44,13 @@ function neutralizeSentinels(s: string): string {
45
44
  }
46
45
 
47
46
  /**
48
- * The sentence mandating the exact no-findings reply. Built from
49
- * {@link MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT} so the instruction and the
50
- * finalizer's acceptance check can never drift apart.
47
+ * The sentence asking a pass that found nothing to say so and stop. The
48
+ * finalizer reads the SHAPE of such a run (a committed reply, no memory-write
49
+ * attempts, a model-driven stop), so the wording is free; what the sentence
50
+ * buys is a run that answers at all rather than committing nothing.
51
51
  */
52
- const NO_FINDINGS_MANDATE = `If nothing new is worth saving, reply with exactly "${MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT}" and stop.`;
52
+ const NO_FINDINGS_MANDATE =
53
+ "If nothing new is worth saving, say so briefly and stop.";
53
54
 
54
55
  /**
55
56
  * Bundled fork-instruction template. Exported so tests (and any future
@@ -149,8 +150,8 @@ export interface ForkInstructionArgs {
149
150
  * the shared {@link loadPromptOverride}), else the bundled
150
151
  * {@link RETROSPECTIVE_INSTRUCTION_TEMPLATE}; both get the same placeholder
151
152
  * substitution. An override additionally gets the no-findings mandate
152
- * appended after its body: the exact-reply sentinel is the finalizer's
153
- * advancement contract, so it holds regardless of what the override says.
153
+ * appended after its body: a no-findings pass advances only by replying,
154
+ * so the ask-for-a-reply holds regardless of what the override says.
154
155
  */
155
156
  export function buildForkInstruction({
156
157
  windowStartTimestamp,
@@ -199,12 +200,11 @@ export function buildForkInstruction({
199
200
  if (override == null) {
200
201
  return rendered;
201
202
  }
202
- // The finalizer recognizes a no-findings review only by the exact
203
- // MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT reply, so the mandate is part of
204
- // the advancement contract, not prompt styling: an override body that
205
- // drops it would leave every no-findings window permanently retryable.
206
- // Appended outside the override so a custom prompt cannot break the
207
- // contract.
203
+ // A pass that saves nothing advances only on a committed reply, so the
204
+ // mandate is part of the advancement contract, not prompt styling: an
205
+ // override body that leaves a no-findings pass free to answer with nothing
206
+ // at all would leave those windows permanently retryable. Appended outside
207
+ // the override so a custom prompt cannot break the contract.
208
208
  return `${rendered}\n\n${NO_FINDINGS_MANDATE}`;
209
209
  }
210
210
 
@@ -7,7 +7,7 @@ import { join } from "node:path";
7
7
 
8
8
  import { z } from "zod";
9
9
 
10
- import { getEffectiveProfilesForProvider } from "../../../../config/default-profile-catalog.js";
10
+ import { getUserSelectableProfilesForProvider } from "../../../../config/default-profile-catalog.js";
11
11
  import { loadConfig } from "../../../../config/loader.js";
12
12
  import { usesConceptPageMemory } from "../../../../config/memory-v3-gate.js";
13
13
  import type { AssistantConfig } from "../../../../config/types.js";
@@ -523,7 +523,7 @@ export async function handleSimulateRouter({
523
523
  // through (the resolver tolerates missing override-profile references by
524
524
  // design, but the playground wants the user to know they typo'd).
525
525
  if (profileOverride !== undefined) {
526
- const profiles = getEffectiveProfilesForProvider(
526
+ const profiles = getUserSelectableProfilesForProvider(
527
527
  liveConfig.llm?.profiles,
528
528
  liveConfig.llm?.defaultProvider ?? null,
529
529
  );
@@ -1138,7 +1138,7 @@ describe("memoryV2ConsolidateJob — consecutive-failure state", () => {
1138
1138
  ok: false,
1139
1139
  error: new Error("provider refused"),
1140
1140
  errorKind: "model_provider",
1141
- ...(failureCode !== undefined ? { failureCode } : {}),
1141
+ ...(failureCode !== undefined ? { turnFailure: { failureCode } } : {}),
1142
1142
  });
1143
1143
  }
1144
1144
 
@@ -486,12 +486,14 @@ export async function memoryV2ConsolidateJob(
486
486
  // everything else (network blip, model hiccup, timeout) is transient
487
487
  // and stays on the short curve.
488
488
  const failureKind: ConsolidationFailureKind =
489
- runResult.failureCode === "PROVIDER_BILLING" ? "billing" : "transient";
489
+ runResult.turnFailure?.failureCode === "PROVIDER_BILLING"
490
+ ? "billing"
491
+ : "transient";
490
492
  log.error(
491
493
  {
492
494
  conversationId: runResult.conversationId,
493
495
  errorKind: runResult.errorKind,
494
- failureCode: runResult.failureCode,
496
+ failureCode: runResult.turnFailure?.failureCode,
495
497
  failureKind,
496
498
  err: runResult.error?.message,
497
499
  },