@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
@@ -2,6 +2,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { beforeEach, describe, expect, mock, test } from "bun:test";
4
4
 
5
+ import type { AgentLoopExitReason } from "../../../../hooks/types.js";
5
6
  import { getWorkspaceDir } from "../paths.js";
6
7
 
7
8
  // ---------------------------------------------------------------------------
@@ -48,7 +49,14 @@ let priorRetroId: string | null = null;
48
49
  let priorRetroOwnerId = "src-conv-1";
49
50
  let priorRetroMessages: Array<{ role: string; content: string }> = [];
50
51
 
51
- let mockWakeResult: { invoked: boolean; reason?: string } = { invoked: true };
52
+ // `exitReason` mirrors what the real wake reports for a completed run: the
53
+ // model answered without asking for another tool. The finalizer reads it to
54
+ // tell a finished review from one something else cut short.
55
+ let mockWakeResult: {
56
+ invoked: boolean;
57
+ reason?: string;
58
+ exitReason?: AgentLoopExitReason;
59
+ } = { invoked: true, exitReason: "no_tool_calls" };
52
60
  let mockWakeThrows: Error | null = null;
53
61
  let wakeCalls: Array<{
54
62
  conversationId: string;
@@ -481,7 +489,7 @@ describe("memoryRetrospectiveJob", () => {
481
489
  priorRetroId = null;
482
490
  priorRetroOwnerId = "src-conv-1";
483
491
  priorRetroMessages = [];
484
- mockWakeResult = { invoked: true };
492
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
485
493
  mockWakeThrows = null;
486
494
  wakeCalls = [];
487
495
  deletedConversationIds = [];
@@ -822,7 +830,7 @@ describe("memoryRetrospectiveJob", () => {
822
830
  // -------------------------------------------------------------------------
823
831
 
824
832
  test("invoked wake with NO persisted run output: no_usable_output, cursor and log untouched, window retryable", async () => {
825
- mockWakeResult = { invoked: true };
833
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
826
834
  // The fork persisted nothing (e.g. a swallowed provider rejection or a
827
835
  // zero-output max-tokens stop).
828
836
  messagesByConversationId["fork-conv-1"] = [];
@@ -843,14 +851,19 @@ describe("memoryRetrospectiveJob", () => {
843
851
  value: 1,
844
852
  detail: {
845
853
  outcome: "no_usable_output",
846
- reason: "run persisted no memory-writing tool call",
854
+ // The failure reason names what was missing, so an operator reading
855
+ // the job row can tell a lost write from an unfinished review.
856
+ reason: "run committed neither a memory write nor a concluding reply",
847
857
  },
848
858
  },
849
859
  ]);
850
860
  });
851
861
 
852
- test("invoked wake whose run persisted only analysis text (no tool calls): no_usable_output, prior retrospective preserved", async () => {
853
- mockWakeResult = { invoked: true };
862
+ test("a run whose reply reads as a conclusion but that the provider cut short: no_usable_output, prior retrospective preserved", async () => {
863
+ // Same persisted text as a finished review, but the loop ended on the
864
+ // output-token ceiling rather than on the model's own stop, so the reply
865
+ // is a fragment of a review, not a verdict on the window.
866
+ mockWakeResult = { invoked: true, exitReason: "max_tokens_reached" };
854
867
  priorRetroId = "prior-retro-1";
855
868
  messagesByConversationId["fork-conv-1"] = [
856
869
  {
@@ -866,14 +879,38 @@ describe("memoryRetrospectiveJob", () => {
866
879
  const outcome = await memoryRetrospectiveJob(makeJob(), stubConfig);
867
880
 
868
881
  expect(outcome.kind).toBe("no_usable_output");
882
+ if (outcome.kind === "no_usable_output") {
883
+ expect(outcome.reason).toBe(
884
+ "run replied without saving anything, but ended on max_tokens_reached rather than a completed review",
885
+ );
886
+ }
869
887
  expect(stateUpserts).toHaveLength(0);
870
888
  // Only this run's own fork is deleted; the prior retrospective (the
871
889
  // dedup baseline for the retry) is preserved.
872
890
  expect(deletedConversationIds).toEqual(["fork-conv-1"]);
873
891
  });
874
892
 
875
- test("run-message load failure classifies as no_usable_output (fail-closed), not success", async () => {
893
+ test("a run the loop never reported a terminal exit for cannot advance on a reply alone", async () => {
894
+ // A checkpoint handoff runs no terminal exit event, so the run resumes
895
+ // elsewhere and this fork holds an unfinished review.
876
896
  mockWakeResult = { invoked: true };
897
+ messagesByConversationId["fork-conv-1"] = [
898
+ {
899
+ role: "assistant",
900
+ content: JSON.stringify([{ type: "text", text: "Nothing to save." }]),
901
+ createdAt: Date.parse("2026-05-11T10:20:00Z"),
902
+ metadata: null,
903
+ },
904
+ ];
905
+
906
+ const outcome = await memoryRetrospectiveJob(makeJob(), stubConfig);
907
+
908
+ expect(outcome.kind).toBe("no_usable_output");
909
+ expect(stateUpserts).toHaveLength(0);
910
+ });
911
+
912
+ test("run-message load failure classifies as no_usable_output (fail-closed), not success", async () => {
913
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
877
914
  // Fork-kind conversation with no stamps and no leading instruction row:
878
915
  // `loadRetrospectiveRunMessages` returns null (indeterminate).
879
916
  conversationOverrides["fork-conv-1"] = {
@@ -896,7 +933,7 @@ describe("memoryRetrospectiveJob", () => {
896
933
  });
897
934
 
898
935
  test("checkpoint-persisted saves on a fork-kind tail count as usable output (rebase-after-live cannot fake no-output)", async () => {
899
- mockWakeResult = { invoked: true };
936
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
900
937
  conversationOverrides["fork-conv-1"] = {
901
938
  source: "memory-retrospective-fork",
902
939
  forkParentMessageId: null,
@@ -945,7 +982,7 @@ describe("memoryRetrospectiveJob", () => {
945
982
  });
946
983
 
947
984
  test("scaffold_managed_skill counts as durable work even with zero remembers", async () => {
948
- mockWakeResult = { invoked: true };
985
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
949
986
  messagesByConversationId["fork-conv-1"] = [
950
987
  {
951
988
  role: "assistant",
@@ -982,7 +1019,7 @@ describe("memoryRetrospectiveJob", () => {
982
1019
  });
983
1020
 
984
1021
  test("a remember whose execution FAILED (is_error tool_result) is not durable evidence and its facts stay out of the log", async () => {
985
- mockWakeResult = { invoked: true };
1022
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
986
1023
  messagesByConversationId["fork-conv-1"] = [
987
1024
  {
988
1025
  role: "assistant",
@@ -1023,41 +1060,91 @@ describe("memoryRetrospectiveJob", () => {
1023
1060
  expect(lastRunAtBumps).toHaveLength(1);
1024
1061
  });
1025
1062
 
1026
- test("explicit no-findings reply advances the cursor without fabricating a memory write", async () => {
1027
- mockWakeResult = { invoked: true };
1063
+ // A finished review that found nothing advances on the SHAPE of the run,
1064
+ // never on the wording of the reply. Each phrasing below is a correct
1065
+ // conclusion the model reached on its own; the window it reviewed is
1066
+ // consumed, and the health counter records which kind of success it was.
1067
+ test.each([
1068
+ "Nothing new to save.",
1069
+ "Nothing new to save beyond those two facts.",
1070
+ "Nothing else new to save.",
1071
+ "Done.",
1072
+ "I reviewed the window. Nothing new to save. Here is my reasoning about why each item was already covered by prior passes.",
1073
+ ])(
1074
+ "a completed review that saved nothing advances the cursor: %s",
1075
+ async (reply) => {
1076
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
1077
+ messagesByConversationId["fork-conv-1"] = [
1078
+ {
1079
+ role: "assistant",
1080
+ content: JSON.stringify([{ type: "text", text: reply }]),
1081
+ createdAt: Date.parse("2026-05-11T10:20:00Z"),
1082
+ metadata: null,
1083
+ },
1084
+ ];
1085
+
1086
+ const outcome = await memoryRetrospectiveJob(makeJob(), stubConfig);
1087
+
1088
+ expect(outcome.kind).toBe("invoked");
1089
+ if (outcome.kind === "invoked") {
1090
+ expect(outcome.noFindings).toBe(true);
1091
+ }
1092
+ expect(stateUpserts).toHaveLength(1);
1093
+ expect(stateUpserts[0]!.lastProcessedMessageId).toBe("m3");
1094
+ expect(stateUpserts[0]!.rememberedLog).toEqual([]);
1095
+ expect(
1096
+ watchdogEvents.filter(
1097
+ (e) => e.checkName === "memory_retrospective_run",
1098
+ ),
1099
+ ).toEqual([
1100
+ {
1101
+ checkName: "memory_retrospective_run",
1102
+ value: 1,
1103
+ detail: { outcome: "invoked", noFindings: true },
1104
+ },
1105
+ ]);
1106
+ },
1107
+ );
1108
+
1109
+ test("narration followed by an empty final response has not concluded and does not advance", async () => {
1110
+ // The reply that proves an empty-handed review must be the run's final
1111
+ // word: text that went live mid-run followed by an empty last response
1112
+ // is an unfinished review, not a conclusion.
1113
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
1028
1114
  messagesByConversationId["fork-conv-1"] = [
1029
1115
  {
1030
1116
  role: "assistant",
1031
1117
  content: JSON.stringify([
1032
- { type: "text", text: "Nothing new to save." },
1118
+ { type: "text", text: "Let me look through the window carefully." },
1033
1119
  ]),
1034
1120
  createdAt: Date.parse("2026-05-11T10:20:00Z"),
1035
1121
  metadata: null,
1036
1122
  },
1123
+ {
1124
+ role: "assistant",
1125
+ content: JSON.stringify([]),
1126
+ createdAt: Date.parse("2026-05-11T10:20:05Z"),
1127
+ metadata: null,
1128
+ },
1037
1129
  ];
1038
1130
 
1039
1131
  const outcome = await memoryRetrospectiveJob(makeJob(), stubConfig);
1040
1132
 
1041
- // A reviewed-and-no-findings pass is a legitimate success: the mandated
1042
- // exact reply is the positive persisted artifact, the cursor advances,
1043
- // and the remembered log gains nothing.
1044
- expect(outcome.kind).toBe("invoked");
1045
- expect(stateUpserts).toHaveLength(1);
1046
- expect(stateUpserts[0]!.lastProcessedMessageId).toBe("m3");
1047
- expect(stateUpserts[0]!.rememberedLog).toEqual([]);
1133
+ expect(outcome.kind).toBe("no_usable_output");
1134
+ if (outcome.kind === "no_usable_output") {
1135
+ expect(outcome.reason).toBe(
1136
+ "run committed neither a memory write nor a concluding reply",
1137
+ );
1138
+ }
1139
+ expect(stateUpserts).toHaveLength(0);
1048
1140
  });
1049
1141
 
1050
- test("analysis prose that merely mentions the no-findings phrase does not advance", async () => {
1051
- mockWakeResult = { invoked: true };
1142
+ test("a run whose only text is whitespace has committed nothing and does not advance", async () => {
1143
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
1052
1144
  messagesByConversationId["fork-conv-1"] = [
1053
1145
  {
1054
1146
  role: "assistant",
1055
- content: JSON.stringify([
1056
- {
1057
- type: "text",
1058
- text: "I reviewed the window. Nothing new to save. Here is my reasoning about why each item was already covered by prior passes.",
1059
- },
1060
- ]),
1147
+ content: JSON.stringify([{ type: "text", text: " \n " }]),
1061
1148
  createdAt: Date.parse("2026-05-11T10:20:00Z"),
1062
1149
  metadata: null,
1063
1150
  },
@@ -1069,8 +1156,8 @@ describe("memoryRetrospectiveJob", () => {
1069
1156
  expect(stateUpserts).toHaveLength(0);
1070
1157
  });
1071
1158
 
1072
- test("no-findings reply cannot advance a run that attempted a save and failed", async () => {
1073
- mockWakeResult = { invoked: true };
1159
+ test("a spoken conclusion cannot advance a run that attempted a save and failed", async () => {
1160
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
1074
1161
  messagesByConversationId["fork-conv-1"] = [
1075
1162
  {
1076
1163
  role: "assistant",
@@ -1112,13 +1199,18 @@ describe("memoryRetrospectiveJob", () => {
1112
1199
 
1113
1200
  // The run demonstrably had findings (it attempted a save); the failed
1114
1201
  // write must stay retryable rather than being papered over by a
1115
- // no-findings claim.
1202
+ // no-findings claim, and the job row must say a write was lost.
1116
1203
  expect(outcome.kind).toBe("no_usable_output");
1204
+ if (outcome.kind === "no_usable_output") {
1205
+ expect(outcome.reason).toBe(
1206
+ "run attempted 1 memory write(s), none of which persisted a successful result",
1207
+ );
1208
+ }
1117
1209
  expect(stateUpserts).toHaveLength(0);
1118
1210
  });
1119
1211
 
1120
1212
  test("a remember tool_use with NO persisted tool_result is not durable evidence", async () => {
1121
- mockWakeResult = { invoked: true };
1213
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
1122
1214
  messagesByConversationId["fork-conv-1"] = [
1123
1215
  {
1124
1216
  role: "assistant",
@@ -1142,7 +1234,7 @@ describe("memoryRetrospectiveJob", () => {
1142
1234
  });
1143
1235
 
1144
1236
  test("retry after a no_usable_output run advances only once durable output exists", async () => {
1145
- mockWakeResult = { invoked: true };
1237
+ mockWakeResult = { invoked: true, exitReason: "no_tool_calls" };
1146
1238
  messagesByConversationId["fork-conv-1"] = [];
1147
1239
 
1148
1240
  const first = await memoryRetrospectiveJob(makeJob(), stubConfig);
@@ -1735,6 +1827,7 @@ describe("memoryRetrospectiveJob", () => {
1735
1827
  expect(wakeCalls[0]!.opts.toolContextPin).toEqual({
1736
1828
  hasNoClient: false,
1737
1829
  transportInterface: "web",
1830
+ clientOs: "web",
1738
1831
  requestOrigin: "memory_retrospective",
1739
1832
  });
1740
1833
  });
@@ -1771,7 +1864,11 @@ describe("memoryRetrospectiveJob", () => {
1771
1864
  id: "m2",
1772
1865
  createdAt: Date.parse("2026-05-11T10:05:00Z"),
1773
1866
  role: "user",
1774
- metadata: JSON.stringify({ userMessageInterface: "macos" }),
1867
+ metadata: JSON.stringify({
1868
+ userMessageInterface: "macos",
1869
+ client: { os: "macos" },
1870
+ clientOsFromRequest: true,
1871
+ }),
1775
1872
  },
1776
1873
  {
1777
1874
  id: "m3",
@@ -1792,6 +1889,7 @@ describe("memoryRetrospectiveJob", () => {
1792
1889
  expect(wakeCalls[0]!.opts.toolContextPin).toEqual({
1793
1890
  hasNoClient: false,
1794
1891
  transportInterface: "macos",
1892
+ clientOs: "macos",
1795
1893
  requestOrigin: "memory_retrospective",
1796
1894
  });
1797
1895
  });
@@ -1815,6 +1913,7 @@ describe("memoryRetrospectiveJob", () => {
1815
1913
  expect(wakeCalls[0]!.opts.toolContextPin).toEqual({
1816
1914
  hasNoClient: false,
1817
1915
  transportInterface: "macos",
1916
+ clientOs: "macos",
1818
1917
  requestOrigin: "memory_retrospective",
1819
1918
  });
1820
1919
  });
@@ -3,7 +3,6 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { describe, expect, test } from "bun:test";
5
5
 
6
- import { MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT } from "../memory-retrospective-constants.js";
7
6
  import {
8
7
  buildForkInstruction,
9
8
  type ForkInstructionArgs,
@@ -11,6 +10,14 @@ import {
11
10
  } from "../memory-retrospective-prompt.js";
12
11
  import { getWorkspaceDir } from "../paths.js";
13
12
 
13
+ /**
14
+ * The mandate sentence as the prompt module builds it. Spelled out here
15
+ * rather than imported so a reworded prompt fails these expectations instead
16
+ * of silently agreeing with itself.
17
+ */
18
+ const NO_FINDINGS_MANDATE =
19
+ "If nothing new is worth saving, say so briefly and stop.";
20
+
14
21
  function makeArgs(
15
22
  overrides: Partial<ForkInstructionArgs> = {},
16
23
  ): ForkInstructionArgs {
@@ -48,7 +55,7 @@ Two dedup sources to skip:
48
55
  1. Anything semantically captured in <already_remembered> above (from prior retrospective passes).
49
56
  2. Anything you already called \`remember\` on inline within your review window — those appear as \`tool_use\` blocks with \`name: "remember"\` in your history.
50
57
 
51
- For everything else in your review window, use the \`remember\` tool on facts, plans, decisions, preferences, names, dates, felt moments, corrections, commitments, or anything else concrete and worth carrying forward. When several facts are worth saving, pass them all as an array to a single \`remember\` call rather than calling it once per fact. If nothing new is worth saving, reply with exactly "Nothing new to save." and stop.
58
+ For everything else in your review window, use the \`remember\` tool on facts, plans, decisions, preferences, names, dates, felt moments, corrections, commitments, or anything else concrete and worth carrying forward. When several facts are worth saving, pass them all as an array to a single \`remember\` call rather than calling it once per fact. If nothing new is worth saving, say so briefly and stop.
52
59
  `);
53
60
  });
54
61
 
@@ -119,11 +126,7 @@ For everything else in your review window, use the \`remember\` tool on facts, p
119
126
  test("proc-to-skills inactive: no authoring section, instruction ends at the remember guidance", () => {
120
127
  const out = buildForkInstruction(makeArgs());
121
128
  expect(out).not.toContain("PROCEDURE");
122
- expect(
123
- out.endsWith(
124
- 'If nothing new is worth saving, reply with exactly "Nothing new to save." and stop.\n',
125
- ),
126
- ).toBe(true);
129
+ expect(out.endsWith(`${NO_FINDINGS_MANDATE}\n`)).toBe(true);
127
130
  });
128
131
 
129
132
  test("bundled template carries each placeholder exactly once", () => {
@@ -176,17 +179,13 @@ describe("promptOverridePath", () => {
176
179
  writeFileSync(overridePath, "Just remember the good parts.\n");
177
180
  expect(
178
181
  buildForkInstruction(makeArgs({ promptOverridePath: overridePath })),
179
- ).toBe(
180
- "Just remember the good parts.\n\n\n" +
181
- `If nothing new is worth saving, reply with exactly "${MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT}" and stop.`,
182
- );
182
+ ).toBe(`Just remember the good parts.\n\n\n${NO_FINDINGS_MANDATE}`);
183
183
  });
184
184
 
185
- test("an override that drops the no-findings mandate still carries the finalizer's exact-reply contract", () => {
186
- // The finalizer advances a no-findings window only on a persisted reply
187
- // that trims to exactly MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT. An
188
- // override author who never read that code would otherwise stall every
189
- // no-findings window forever, so the mandate must survive any override.
185
+ test("an override that tells a no-findings pass to stay silent still carries the reply mandate", () => {
186
+ // A pass that saves nothing advances only on a committed reply, so an
187
+ // override telling it to do nothing would stall every no-findings window
188
+ // forever. The mandate must survive any override.
190
189
  const overridePath = join(dir, "mandate-free.md");
191
190
  writeFileSync(
192
191
  overridePath,
@@ -195,13 +194,7 @@ describe("promptOverridePath", () => {
195
194
  const out = buildForkInstruction(
196
195
  makeArgs({ promptOverridePath: overridePath }),
197
196
  );
198
- expect(out).toContain(
199
- `reply with exactly "${MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT}"`,
200
- );
201
- // The mandate references the same constant the finalizer compares
202
- // against, so the two cannot drift; guard the constant's exact value here
203
- // to make an accidental rewording loud.
204
- expect(MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT).toBe("Nothing new to save.");
197
+ expect(out.endsWith(NO_FINDINGS_MANDATE)).toBe(true);
205
198
  });
206
199
 
207
200
  test("missing override file falls back to the bundled rendering", () => {
@@ -227,12 +220,9 @@ describe("promptOverridePath", () => {
227
220
  });
228
221
  });
229
222
 
230
- // The finalizer's explicit-no-findings gate matches persisted assistant text
231
- // against MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT by strict equality, and the
232
- // bundled instruction is what teaches the model to emit it. This guard fails
233
- // if either side drifts.
234
- test("bundled template mandates the exact no-findings sentinel the finalizer matches", () => {
235
- expect(RETROSPECTIVE_INSTRUCTION_TEMPLATE).toContain(
236
- `reply with exactly "${MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT}" and stop.`,
237
- );
223
+ // A pass that saves nothing advances its window only if it replies at all,
224
+ // and the bundled instruction is what asks for that reply. The wording is
225
+ // free; a template that stopped asking for a reply is not.
226
+ test("bundled template asks a no-findings pass to answer rather than stay silent", () => {
227
+ expect(RETROSPECTIVE_INSTRUCTION_TEMPLATE).toContain(NO_FINDINGS_MANDATE);
238
228
  });
@@ -17,9 +17,9 @@
17
17
  * - `invoked: true` alone must therefore NOT advance
18
18
  * `memory_retrospective_state`: the handler re-reads the fork's persisted
19
19
  * rows (`collectRetrospectiveRunEvidence` → `loadRetrospectiveRunMessages`
20
- * → `getMessages`) and only advances when a durable memory-writing
21
- * `tool_use` (`remember` / `scaffold_managed_skill`) was persisted by THIS
22
- * run.
20
+ * → `getMessages`) and only advances when THIS run persisted a durable
21
+ * memory-writing `tool_use` (`remember` / `scaffold_managed_skill`) or
22
+ * replied and stopped on its own (a completed no-findings review).
23
23
  */
24
24
 
25
25
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -481,16 +481,20 @@ describe("memoryRetrospectiveJob through the real wake + real agent loop", () =>
481
481
  expect(lastRunAtBumps).toHaveLength(0);
482
482
  });
483
483
 
484
- test("clean empty reply through the real loop does not advance", async () => {
485
- // Scenario C: the model answers with analysis text and no tool calls.
486
- // The wake persists the text tail (visible output), but with zero
487
- // durable memory-writing tool calls the gate must refuse to advance.
484
+ test("a text-only conclusion through the real loop advances as a no-findings pass", async () => {
485
+ // Scenario C: the model answers in its own words and stops without any
486
+ // tool call. The real loop exits on `no_tool_calls`, so the committed
487
+ // reply is a finished empty-handed review and the window is consumed.
488
488
  providerImpl = async () => textOnlyResponse("Nothing worth saving.");
489
489
 
490
490
  const outcome = await memoryRetrospectiveJob(makeJob(), stubConfig);
491
491
 
492
- expect(outcome.kind).toBe("no_usable_output");
492
+ expect(outcome.kind).toBe("invoked");
493
+ if (outcome.kind === "invoked") {
494
+ expect(outcome.noFindings).toBe(true);
495
+ }
493
496
  expect(providerCalls.length).toBeGreaterThanOrEqual(1);
494
- expect(stateUpserts).toHaveLength(0);
497
+ expect(stateUpserts).toHaveLength(1);
498
+ expect(stateUpserts[0]!.rememberedLog).toEqual([]);
495
499
  });
496
500
  });
@@ -24,7 +24,9 @@
24
24
  * 2. The failed window stays retryable: a later run over the same window
25
25
  * succeeds and advances state.
26
26
  * 3. A usable-output control advances state (cursor, remembered log) and
27
- * GCs the superseded prior retrospective.
27
+ * GCs the superseded prior retrospective, and a pass that reviewed its
28
+ * window and had nothing to save advances it too, in whatever words the
29
+ * model chose.
28
30
  * 4. A run whose checkpoint went live before a later rejection stays
29
31
  * `invoked: true` (its side effects landed and are honored).
30
32
  */
@@ -409,9 +411,8 @@ describe("memory retrospective wake state chain (real AgentLoop)", () => {
409
411
  expect(listRetroForksOf(fixture.sourceId)).toEqual([fixture.priorRetroId]);
410
412
 
411
413
  // (2) The window stays retryable: the same slice succeeds on retry.
412
- // The finalizer's usable-output gate accepts free text only when it is
413
- // exactly the mandated no-findings sentinel; a verified remember write
414
- // is the other accepted evidence. Use the sentinel for the control.
414
+ // The finalizer accepts a text-only reply as a completed empty-handed
415
+ // review; a verified remember write is the other accepted evidence.
415
416
  providerScript = [{ response: textResponse("Nothing new to save.") }];
416
417
  providerCallCount = 0;
417
418
  const retry = await runForkBasedRetrospective(
@@ -472,6 +473,31 @@ describe("memory retrospective wake state chain (real AgentLoop)", () => {
472
473
  ).toBe(fixture.newMessageId);
473
474
  });
474
475
 
476
+ test("a paraphrased empty-handed conclusion consumes the window", async () => {
477
+ const fixture = await stageChainFixture();
478
+
479
+ // The instruction asks a pass with nothing to save to say so; it does not
480
+ // dictate the sentence. The real loop ends this run on `no_tool_calls`,
481
+ // which is what proves the model reached its own conclusion.
482
+ providerScript = [
483
+ { response: textResponse("Nothing further to save, all covered above.") },
484
+ ];
485
+
486
+ const outcome = await runForkBasedRetrospective(
487
+ fixture.sourceId,
488
+ chainConfig,
489
+ );
490
+
491
+ expect(outcome.kind).toBe("invoked");
492
+ if (outcome.kind === "invoked") {
493
+ expect(outcome.noFindings).toBe(true);
494
+ }
495
+ const state = getRetrospectiveState(fixture.sourceId);
496
+ expect(state?.lastProcessedMessageId).toBe(fixture.newMessageId);
497
+ // A pass that saved nothing adds nothing to the dedup baseline.
498
+ expect(state?.rememberedLog).toEqual([PRIOR_FACT]);
499
+ });
500
+
475
501
  test("usable-output control: cursor advances, remembered log grows, prior retrospective is GC'd", async () => {
476
502
  const fixture = await stageChainFixture();
477
503
 
@@ -488,6 +514,7 @@ describe("memory retrospective wake state chain (real AgentLoop)", () => {
488
514
  expect(outcome.kind).toBe("invoked");
489
515
  if (outcome.kind === "invoked") {
490
516
  expect(outcome.cutoffMessageId).toBe(fixture.newMessageId);
517
+ expect(outcome.noFindings).toBe(false);
491
518
  }
492
519
 
493
520
  // (3) Real finalization against real rows: cursor advanced, this run's
@@ -78,14 +78,3 @@ export const MEMORY_RETROSPECTIVE_ORIGIN = "memory_retrospective";
78
78
  * set from turn 1, and matched by the permission checker's origin-scoped grant.
79
79
  */
80
80
  export const SKILL_MANAGEMENT_SKILL_ID = "skill-management";
81
-
82
- /**
83
- * The exact reply the fork instruction mandates when a reviewed window
84
- * contains nothing worth saving. The finalizer treats a persisted assistant
85
- * text block that trims to exactly this phrase (with no memory-writing tool
86
- * attempts in the run) as the positive artifact of a legitimate no-findings
87
- * review, advancing the cursor without fabricating a memory write. Compared
88
- * by strict whole-block equality so analysis prose that merely mentions the
89
- * phrase does not qualify.
90
- */
91
- export const MEMORY_RETROSPECTIVE_NO_FINDINGS_TEXT = "Nothing new to save.";
@@ -76,21 +76,27 @@ function isRetrospectiveInstructionRow(metadata: string | null): boolean {
76
76
  * Load the messages a retrospective run produced itself, given the
77
77
  * retrospective conversation's `source` kind:
78
78
  *
79
- * - **Fork-kind** rows carry the copied source prefix (the source's visible
80
- * tail), so only the post-fork tail (messages strictly after the fork
81
- * boundary) counts scanning the whole row would attribute the source
82
- * conversation's own turns to the retrospective. When no row carries a
83
- * `forkSourceMessageId` stamp, the fork is run-authored end-to-end only
84
- * if its first row is the run's own instruction message (the empty-prefix
85
- * tail-only fork); a stampless fork WITHOUT a leading instruction row is
86
- * indeterminate attributing it would mine copied source tool calls as
87
- * run output and degrades to "produced none".
79
+ * - **Fork-kind, referential**: the fork copies no prefix. Inherited
80
+ * history is read through lineage and keeps the source `conversationId`.
81
+ * When no copied-row stamp is present, a mixed-ownership list is that
82
+ * shape: rows owned by this conversation are the run, including when
83
+ * that set is empty. No separate conversation-row lookup is required.
84
+ * - **Fork-kind, cloning**: rows carry the copied source prefix (the
85
+ * source's visible tail), so only the post-fork tail (messages strictly
86
+ * after the fork boundary) counts. Scanning the whole list would
87
+ * attribute the source conversation's own turns to the retrospective.
88
+ * When no row carries a `forkSourceMessageId` stamp, the fork is
89
+ * run-authored end-to-end only if its first row is the run's own
90
+ * instruction message (the empty-prefix tail-only fork). A stampless
91
+ * list where every row is owned by this conversation is the
92
+ * indeterminate cloning shape: attributing it would mine copied source
93
+ * tool calls as run output, so the helper degrades to "produced none".
88
94
  * - **Legacy-kind** rows start empty, so every message is the run's own.
89
95
  *
90
96
  * Returns `null` when the run's output cannot be determined (message load
91
- * failure, or the indeterminate stampless shape above) — callers degrade
92
- * (empty dedup baseline / "no output"). Best-effort: failures are logged,
93
- * never thrown.
97
+ * failure, or the indeterminate stampless cloning shape above). Callers
98
+ * degrade (empty dedup baseline / "no output"). Best-effort: failures are
99
+ * logged, never thrown.
94
100
  */
95
101
  export async function loadRetrospectiveRunMessages(
96
102
  conversationId: string,
@@ -114,10 +120,18 @@ export async function loadRetrospectiveRunMessages(
114
120
  return messages;
115
121
  }
116
122
  if (isRetrospectiveInstructionRow(messages[0]?.metadata ?? null)) {
117
- // Empty copied prefix the run's instruction opens the conversation,
123
+ // Empty copied prefix. The run's instruction opens the conversation,
118
124
  // so every message is the run's own output.
119
125
  return messages;
120
126
  }
127
+ // Referential forks copy nothing. Lineage rows keep the source
128
+ // conversationId, so a mixed-ownership list is a reference fork and
129
+ // the owned rows are the run. A stampless list where every row is
130
+ // owned by this conversation is the indeterminate cloning shape.
131
+ const owned = messages.filter((m) => m.conversationId === conversationId);
132
+ if (owned.length < messages.length) {
133
+ return owned;
134
+ }
121
135
  log.warn(
122
136
  { retrospectiveConversationId: conversationId },
123
137
  "memory-retrospective: fork-kind retrospective has no forkSourceMessageId stamps and no leading instruction row; treating run as having produced none",