@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
@@ -9,11 +9,11 @@
9
9
  * process that is actually parented to it.
10
10
  */
11
11
 
12
- import { execFile } from "node:child_process";
13
- import { readdirSync, readFileSync } from "node:fs";
14
- import { promisify } from "node:util";
15
-
16
- const execFileAsync = promisify(execFile);
12
+ import {
13
+ listProcessTableAsync,
14
+ type ProcessTableRow,
15
+ readRawProcessCommand,
16
+ } from "./process-table.js";
17
17
 
18
18
  /**
19
19
  * Whether a process runs plugin-owned code or is the daemon itself / one of its
@@ -52,6 +52,12 @@ export interface ProcTreeNode {
52
52
  children: ProcTreeNode[];
53
53
  }
54
54
 
55
+ interface ProcessTreeBuilder {
56
+ build: (pid: number) => ProcTreeNode;
57
+ byPid: Map<number, ProcInfo>;
58
+ visited: Set<number>;
59
+ }
60
+
55
61
  /** Interpreters whose script argument is more descriptive than argv[0]. */
56
62
  const RUNTIMES = new Set([
57
63
  "bun",
@@ -64,7 +70,14 @@ const RUNTIMES = new Set([
64
70
  "env",
65
71
  ]);
66
72
 
67
- const basename = (p: string): string => p.split("/").pop() || p;
73
+ const basename = (p: string): string => p.split(/[\\/]/).pop() || p;
74
+
75
+ function splitCommand(command: string): string[] {
76
+ return Array.from(
77
+ command.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g),
78
+ (match) => match[1] ?? match[2] ?? match[3],
79
+ );
80
+ }
68
81
 
69
82
  /** Script extensions whose path we summarize as `<parent>-<file>`. */
70
83
  const SCRIPT_EXT_RE = /\.(ts|js|mjs|cjs|py)$/;
@@ -76,7 +89,7 @@ const SCRIPT_EXT_RE = /\.(ts|js|mjs|cjs|py)$/;
76
89
  * filename when the script sits at the filesystem root.
77
90
  */
78
91
  function scriptName(scriptPath: string): string {
79
- const parts = scriptPath.split("/").filter(Boolean);
92
+ const parts = scriptPath.split(/[\\/]/).filter(Boolean);
80
93
  const file = parts[parts.length - 1].replace(SCRIPT_EXT_RE, "");
81
94
  const parent = parts.length >= 2 ? parts[parts.length - 2] : "";
82
95
  return parent ? `${parent}-${file}` : file;
@@ -93,13 +106,14 @@ function scriptName(scriptPath: string): string {
93
106
  * (`/…/vellum-qdrant`) keep their bare executable name.
94
107
  */
95
108
  export function deriveName(command: string): string {
96
- const tokens = command.trim().split(/\s+/).filter(Boolean);
109
+ const tokens = splitCommand(command.trim());
97
110
  if (tokens.length === 0) {
98
111
  return "(unknown)";
99
112
  }
100
113
 
101
114
  const argv0 = basename(tokens[0]);
102
- if (RUNTIMES.has(argv0)) {
115
+ const runtimeName = argv0.replace(/\.exe$/i, "").toLowerCase();
116
+ if (RUNTIMES.has(runtimeName)) {
103
117
  const args = tokens.slice(1);
104
118
  const script = args.find((t) => SCRIPT_EXT_RE.test(t));
105
119
  if (script) {
@@ -109,7 +123,7 @@ export function deriveName(command: string): string {
109
123
  // "what was run" rather than an opaque `bun`. Flags are dropped as noise.
110
124
  const meaningful = args.filter((t) => !t.startsWith("-"));
111
125
  if (meaningful.length > 0) {
112
- return `${argv0} ${meaningful.join(" ")}`;
126
+ return `${runtimeName} ${meaningful.join(" ")}`;
113
127
  }
114
128
  }
115
129
  return argv0;
@@ -126,12 +140,8 @@ export function deriveName(command: string): string {
126
140
  * {@link deriveName} descriptor.
127
141
  */
128
142
  export function readProcessCommand(pid: number): string | null {
129
- try {
130
- const raw = readFileSync(`/proc/${pid}/cmdline`, "utf8");
131
- return deriveName(raw.split("\0").filter(Boolean).join(" "));
132
- } catch {
133
- return null;
134
- }
143
+ const command = readRawProcessCommand(pid);
144
+ return command == null ? null : deriveName(command);
135
145
  }
136
146
 
137
147
  /**
@@ -143,7 +153,8 @@ export function readProcessCommand(pid: number): string | null {
143
153
  * (`.../plugins/defaults/<name>/`). The `plugins-data/` state directory is
144
154
  * deliberately not matched — the slash after `plugins` excludes it.
145
155
  */
146
- const PLUGIN_PATH_RE = /(?:^|\/)plugins\/([^/\s]+)(?:\/([^/\s]+))?/;
156
+ const PLUGIN_PATH_RE =
157
+ /(?:^|[\\/])plugins[\\/]([^\\/\s]+)(?:[\\/]([^\\/\s]+))?/;
147
158
 
148
159
  /**
149
160
  * Classify whether a raw command line belongs to plugin-owned code, and if so
@@ -169,122 +180,23 @@ export function deriveOrigin(command: string): ProcessOrigin {
169
180
  return `plugin:${name}`;
170
181
  }
171
182
 
172
- /**
173
- * Parse a `/proc/<pid>/stat` line into its leading fields. `comm` (the
174
- * executable name) may itself contain spaces and parentheses, so the fixed
175
- * fields are read relative to the final `)` rather than by naive splitting.
176
- * Returns null if the line is malformed.
177
- */
178
- function parseProcStat(content: string): { comm: string; ppid: number } | null {
179
- const lparen = content.indexOf("(");
180
- const rparen = content.lastIndexOf(")");
181
- if (lparen === -1 || rparen === -1 || rparen < lparen) {
182
- return null;
183
- }
184
- const comm = content.slice(lparen + 1, rparen);
185
- // After ")" come: " <state> <ppid> …" — split the remainder on spaces.
186
- const rest = content.slice(rparen + 2).split(" ");
187
- const ppid = Number(rest[1]);
188
- if (!Number.isInteger(ppid)) {
189
- return null;
190
- }
191
- return { comm, ppid };
192
- }
193
-
194
- /** Read the live process table from Linux `/proc`. Throws if `/proc` is absent. */
195
- function listProcessesFromProc(): ProcInfo[] {
196
- const entries = readdirSync("/proc");
197
- const procs: ProcInfo[] = [];
198
- for (const entry of entries) {
199
- const pid = Number(entry);
200
- if (!Number.isInteger(pid) || pid <= 0) {
201
- continue;
202
- }
203
-
204
- let ppid: number;
205
- let comm: string;
206
- try {
207
- const parsed = parseProcStat(readFileSync(`/proc/${pid}/stat`, "utf8"));
208
- if (!parsed) {
209
- continue;
210
- }
211
- ({ ppid, comm } = parsed);
212
- } catch {
213
- // Process exited between readdir and read — skip.
214
- continue;
215
- }
216
-
217
- // `/proc/<pid>/cmdline` is NUL-delimited and empty for kernel threads.
218
- // Redact the raw command line via deriveName to strip secrets (tokens,
219
- // API keys, database URLs) that are commonly passed as process arguments.
220
- // The raw command line is read here but never stored — only the derived
221
- // safe descriptor is kept.
222
- let command = comm;
223
- // Origin is derived from the raw command path (which carries the plugin
224
- // directory), so it is classified before deriveName redacts the path away.
225
- let origin: ProcessOrigin = "workspace";
226
- try {
227
- const raw = readFileSync(`/proc/${pid}/cmdline`, "utf8");
228
- const joined = raw.split("\0").filter(Boolean).join(" ");
229
- if (joined) {
230
- command = deriveName(joined);
231
- origin = deriveOrigin(joined);
232
- }
233
- } catch {
234
- // Fall back to comm.
235
- }
236
-
237
- procs.push({ pid, ppid, command, origin });
238
- }
239
- return procs;
240
- }
241
-
242
- /** Read the live process table via the `ps` command (macOS / no `/proc`). */
243
- async function listProcessesFromPs(): Promise<ProcInfo[]> {
244
- const { stdout } = await execFileAsync(
245
- "ps",
246
- ["-A", "-o", "pid=,ppid=,command="],
247
- { maxBuffer: 8 * 1024 * 1024 },
248
- );
249
-
250
- const procs: ProcInfo[] = [];
251
- for (const line of stdout.split("\n")) {
252
- const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
253
- if (!m) {
254
- continue;
255
- }
256
- const raw = m[3].trim();
257
- procs.push({
258
- pid: Number(m[1]),
259
- ppid: Number(m[2]),
260
- command: deriveName(raw),
261
- origin: deriveOrigin(raw),
262
- });
263
- }
264
- return procs;
265
- }
266
-
267
183
  /**
268
184
  * Enumerate the live process table as `(pid, ppid, command)` rows. Prefers
269
- * Linux `/proc`; falls back to `ps` when `/proc` is unavailable (e.g. macOS).
185
+ * Linux `/proc`, uses `ps` on macOS, and queries Win32_Process on Windows.
270
186
  */
271
- export async function listProcesses(): Promise<ProcInfo[]> {
272
- try {
273
- return listProcessesFromProc();
274
- } catch {
275
- return listProcessesFromPs();
276
- }
187
+ export async function listProcesses(
188
+ rows?: ProcessTableRow[],
189
+ ): Promise<ProcInfo[]> {
190
+ const processRows = rows ?? (await listProcessTableAsync());
191
+ return processRows.map(({ pid, ppid, command }) => ({
192
+ pid,
193
+ ppid,
194
+ command: deriveName(command),
195
+ origin: deriveOrigin(command),
196
+ }));
277
197
  }
278
198
 
279
- /**
280
- * Build the process subtree rooted at `rootPid` from a flat process list.
281
- * Children are ordered by PID. Self-references and already-visited PIDs are
282
- * skipped so a malformed table cannot produce an infinite tree.
283
- */
284
- export function buildProcessTree(
285
- procs: ProcInfo[],
286
- rootPid: number,
287
- ): ProcTreeNode {
199
+ function createProcessTreeBuilder(procs: ProcInfo[]): ProcessTreeBuilder {
288
200
  const byPid = new Map<number, ProcInfo>();
289
201
  const childrenOf = new Map<number, number[]>();
290
202
  for (const p of procs) {
@@ -316,5 +228,49 @@ export function buildProcessTree(
316
228
  };
317
229
  };
318
230
 
319
- return build(rootPid);
231
+ return { build, byPid, visited };
232
+ }
233
+
234
+ /**
235
+ * Build the process subtree rooted at `rootPid` from a flat process list.
236
+ * Children are ordered by PID. Self-references and already-visited PIDs are
237
+ * skipped so a malformed table cannot produce an infinite tree.
238
+ */
239
+ export function buildProcessTree(
240
+ procs: ProcInfo[],
241
+ rootPid: number,
242
+ ): ProcTreeNode {
243
+ return createProcessTreeBuilder(procs).build(rootPid);
244
+ }
245
+
246
+ /** Build a synthetic system root containing every enumerated process once. */
247
+ export function buildSystemProcessTree(procs: ProcInfo[]): ProcTreeNode {
248
+ const { build, byPid, visited } = createProcessTreeBuilder(procs);
249
+ const children: ProcTreeNode[] = [];
250
+ const orderedProcesses = [...byPid.values()].sort((a, b) => a.pid - b.pid);
251
+
252
+ for (const processInfo of orderedProcesses) {
253
+ if (
254
+ processInfo.ppid !== processInfo.pid &&
255
+ !byPid.has(processInfo.ppid) &&
256
+ !visited.has(processInfo.pid)
257
+ ) {
258
+ children.push(build(processInfo.pid));
259
+ }
260
+ }
261
+
262
+ // Malformed process tables can contain parent cycles with no natural root.
263
+ for (const processInfo of orderedProcesses) {
264
+ if (!visited.has(processInfo.pid)) {
265
+ children.push(build(processInfo.pid));
266
+ }
267
+ }
268
+
269
+ return {
270
+ pid: 0,
271
+ name: "system",
272
+ command: "",
273
+ origin: "workspace",
274
+ children,
275
+ };
320
276
  }
@@ -25,6 +25,26 @@ export function isVisionNotSupportedError(message: string): boolean {
25
25
  return VISION_NOT_SUPPORTED_PATTERNS.some((re) => re.test(message));
26
26
  }
27
27
 
28
+ // Provider prose from server-side chat-template renderers that failed to
29
+ // serialize the request into the model's prompt format. Emitted by
30
+ // vLLM/Jinja-style template engines behind OpenAI-compatible endpoints, e.g.
31
+ // Together serving MiniMax M3: "Failed to apply chat template: invalid
32
+ // operation: object is not callable (in chat:22)".
33
+ export const CHAT_TEMPLATE_FAILURE_PATTERNS = [
34
+ /appl\w* (?:the )?chat[ _-]?template/i,
35
+ /chat[ _-]?template (?:error|render)/i,
36
+ ];
37
+
38
+ /**
39
+ * Whether a provider error message indicates the endpoint's server-side chat
40
+ * template failed to render the request. These endpoints typically only
41
+ * handle plain-string message content, so structured shapes (content-parts
42
+ * arrays, tool payloads) are what trip the renderer.
43
+ */
44
+ export function isChatTemplateFailureError(message: string): boolean {
45
+ return CHAT_TEMPLATE_FAILURE_PATTERNS.some((re) => re.test(message));
46
+ }
47
+
28
48
  // Vendor-neutral (OpenRouter/Anthropic-style) credit-exhaustion prose. Also
29
49
  // covers per-key spend caps: OpenRouter returns 403 "Key limit exceeded" when a
30
50
  // key's configured credit limit is reached — a billing/budget condition that
package/src/util/spawn.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  * Shared spawn-with-timeout helper used by media-processing and transcribe tools.
3
3
  */
4
4
 
5
+ import { delimiter } from "node:path";
6
+
5
7
  /** Full video preprocessing: mpdecimate analysis, frame extraction, palette analysis. Longest operation. */
6
8
  export const FFMPEG_PREPROCESS_TIMEOUT_MS = 600_000;
7
9
 
@@ -29,9 +31,14 @@ export function spawnWithTimeout(
29
31
  stderr: "pipe",
30
32
  env: {
31
33
  ...process.env,
32
- PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin"]
34
+ PATH: [
35
+ process.env.PATH,
36
+ ...(process.platform === "darwin"
37
+ ? ["/opt/homebrew/bin", "/usr/local/bin"]
38
+ : []),
39
+ ]
33
40
  .filter(Boolean)
34
- .join(":"),
41
+ .join(delimiter),
35
42
  },
36
43
  });
37
44
  const timer = setTimeout(() => {
@@ -247,6 +247,51 @@ describe("watch retrospective", () => {
247
247
  );
248
248
  });
249
249
 
250
+ test("puts the report last in the turn, after the skill it hands off to", async () => {
251
+ const summary = recordSession(["renaming the export"]);
252
+ const { calls, dispatch } = recordingDispatch();
253
+
254
+ await runWatchRetro(summary, { dispatch });
255
+
256
+ const { prompt } = calls[0]!;
257
+ // The report is what the user is shown when a session ends, so it has to
258
+ // be the turn's last prose. A client renders the final text block as the
259
+ // response and folds everything before it into collapsed intermediate
260
+ // work, so a `skill_load` issued after the report demotes the report to
261
+ // an "Earlier activity" row the user has to unfold to read.
262
+ expect(prompt).toContain("Load the `skill-management` skill first");
263
+ expect(
264
+ prompt.indexOf("Load the `skill-management` skill first"),
265
+ ).toBeLessThan(prompt.indexOf("What I need from you"));
266
+ expect(prompt).toContain("That report is the last thing you do this turn");
267
+ // A sign-off is another text block after the report, which puts the report
268
+ // back on the wrong side of the same rule, so it is refused by name.
269
+ expect(prompt).toContain("no sign-off");
270
+ expect(prompt).toContain("no further tool call");
271
+ });
272
+
273
+ test("writes the report even when the skill it hands off to will not load", async () => {
274
+ const summary = recordSession(["filing the receipt"]);
275
+ const { calls, dispatch } = recordingDispatch();
276
+
277
+ await runWatchRetro(summary, { dispatch });
278
+
279
+ const { prompt } = calls[0]!;
280
+ // `skill-management` is a selector, and a managed or workspace skill of the
281
+ // same id replaces the bundled one in the catalog. Putting the load ahead
282
+ // of the report is what lets a shadow, or the refusal a clientless wake
283
+ // gives an inline-command load, land before the user has been told
284
+ // anything. Neither may become the retro: the session is recorded and the
285
+ // account of it is what the user is owed.
286
+ expect(prompt).toContain(
287
+ "Write the report below whether or not that load succeeds",
288
+ );
289
+ expect(prompt).toContain("do not report on the load and do not retry it");
290
+ // The failure is still named, so a missing handoff does not read as a
291
+ // report that simply chose not to ask about authoring.
292
+ expect(prompt).toContain("could not open the skill-authoring flow");
293
+ });
294
+
250
295
  test("authors nothing until the user has confirmed", async () => {
251
296
  const summary = recordSession(["exporting the sheet"]);
252
297
  const { calls, dispatch } = recordingDispatch();
@@ -605,7 +650,9 @@ describe("watch retrospective", () => {
605
650
  expect(payload).toBeGreaterThan(opened);
606
651
  expect(payload).toBeLessThan(closed);
607
652
  // The retrospective's own instructions sit outside it.
608
- expect(prompt.indexOf("Write back to the user")).toBeGreaterThan(closed);
653
+ expect(
654
+ prompt.indexOf("Load the `skill-management` skill first"),
655
+ ).toBeGreaterThan(closed);
609
656
 
610
657
  // One real envelope, so nothing in the recording can pass for a second.
611
658
  expect(prompt.match(/<external_content/g)).toHaveLength(1);
@@ -76,16 +76,58 @@ const WATCH_RETRO_WAKE_SOURCE = "watch-retro";
76
76
  * gap between those two is the whole risk of turning a demonstration into a
77
77
  * skill. `skill-management` will not scaffold until that step is settled
78
78
  * either, so an unasked one stalls the flow it was meant to feed.
79
+ *
80
+ * **The skill loads before the report is written, and nothing follows the
81
+ * report.** This is about where the report lands on screen, not about the
82
+ * order the work happens in. A client renders an assistant turn as its final
83
+ * prose plus the intermediate work that led there, and the web transcript
84
+ * collapses that intermediate half into an "Earlier activity" disclosure that
85
+ * a settled turn shows closed (`finalResponseGroupIndex` in
86
+ * `transcript-message-body.tsx`). Everything before the turn's last non-empty
87
+ * text block is intermediate by that rule, including prose. Writing the report
88
+ * first and loading the skill after it puts a `skill_load` and whatever the
89
+ * model says once the skill is in hand *after* the report, which demotes the
90
+ * report to intermediate work and hides it behind a collapsed row next to the
91
+ * reasoning rows. The user presses stop on a session and is shown a folded-up
92
+ * link where the account of it should be.
93
+ *
94
+ * So the load comes first and the report is last. `skill-management` opens on
95
+ * "Ask before doing anything", so a turn that loads it and then asks is
96
+ * following it rather than jumping the flow, and the report is the alignment
97
+ * its first step calls for. The instruction that nothing follows the report is
98
+ * as load-bearing as the ordering: one trailing line of sign-off is another
99
+ * text block after it, and the report is intermediate again.
100
+ *
101
+ * **The report does not depend on the load succeeding.** `skill-management` is
102
+ * a selector, not a fixed definition: `loadSkillCatalog` lets a managed or
103
+ * workspace skill of the same id replace the bundled one, and
104
+ * `resolveSkillSelector` hands back whichever entry won. So the load can come
105
+ * back as someone else's skill, or as a refusal, and putting it ahead of the
106
+ * report is what makes either one land before the user has been told anything.
107
+ * A refusal is the likelier of the two here: this wake is `clientless`, and an
108
+ * inline-command load with no human present is denied outright
109
+ * (`permissions/checker.ts`, `isDynamicSkillLoadInvocation`). The bundled skill
110
+ * carries no such expansions, so the ordinary path is unaffected, but a shadow
111
+ * that carries them is denied on arrival.
112
+ *
113
+ * Neither outcome is allowed to become the retro. The session was recorded, the
114
+ * timeline is already in this prompt, and the account of it is the one thing the
115
+ * user is owed for having pressed stop; a permission error where that account
116
+ * should be is the same empty thread the `surfaceConversation` guard exists to
117
+ * prevent, just with prose in it. So the instructions write the report either
118
+ * way and say the handoff did not happen, rather than reporting on the load.
79
119
  */
80
- const RETRO_INSTRUCTIONS = `Write back to the user in two sections, in this order, both as level-2 headings.
120
+ const RETRO_INSTRUCTIONS = `Load the \`skill-management\` skill first, before you write anything, and follow it. Do not author or scaffold a skill until the four points that step names are settled: the report below is the alignment its first step calls for.
121
+
122
+ Write the report below whether or not that load succeeds. If it fails, is refused, or comes back as something other than the skill-management flow, do not report on the load and do not retry it: write the report, and end it with one line saying you could not open the skill-authoring flow, so the user knows the handoff is the part that did not happen.
123
+
124
+ Then write back to the user in two sections, in this order, both as level-2 headings. That report is the last thing you do this turn, and nothing follows it: no sign-off, no note about what you loaded, no further tool call. Their answers come back as an ordinary reply and the flow picks up from there.
81
125
 
82
126
  First, "What I need from you". The questions you cannot answer from the recording, numbered, most consequential first. Each one concrete enough to answer in a sentence, and each one about something you are genuinely guessing at: a value you could not read, a choice whose rule you could not infer, a step you only saw the result of. Always ask what they would say to start this task, in their own words, because the recording cannot tell you that. Ask about the done condition if it is unclear. Always confirm any destructive or irreversible step, even one the recording showed plainly: watching someone do a thing once is not agreement to have it done again unattended, and this is the one place the rule below does not apply. Otherwise do not ask them to confirm something the recording already showed you.
83
127
 
84
128
  Second, "What I saw". Open with one sentence naming the task and what it is for, on its own and not as a list item. Then the steps in order beneath it, one line each and concrete enough to follow, carrying no purpose of their own. This is the record your questions sit on top of, so state it rather than asking about it.
85
129
 
86
- Open on the first heading. No preamble, no announcing what you are about to do, no narrating which skills you are loading.
87
-
88
- Then load the \`skill-management\` skill and follow it, treating the answers to your questions as the alignment its first step calls for. Do not author or scaffold a skill until the four points that step names are settled. Correct your reading against whatever they tell you. If they decide this is not worth keeping, say so and stop.`;
130
+ Open on the first heading. No preamble, no announcing what you are about to do, no narrating which skills you are loading. Correct your reading against whatever they tell you. If they decide this is not worth keeping, say so and stop.`;
89
131
 
90
132
  /**
91
133
  * Told to the model whenever the render was bounded, naming the bound that
@@ -12,6 +12,7 @@ import { join } from "node:path";
12
12
  import { promisify } from "node:util";
13
13
 
14
14
  import { getConfig } from "../config/loader.js";
15
+ import { isFileUnheld } from "../util/file-use.js";
15
16
  import { getLogger } from "../util/logger.js";
16
17
  import { Mutex } from "../util/mutex.js";
17
18
  import { PromiseGuard } from "../util/promise-guard.js";
@@ -419,20 +420,9 @@ export class WorkspaceGitService {
419
420
  return;
420
421
  }
421
422
 
422
- try {
423
- // lsof exits non-zero when no process holds the file, which rejects the
424
- // promise and falls through to removal below.
425
- const { stdout } = await execFileAsync("lsof", ["-t", lockPath], {
426
- timeout: 3000,
427
- });
428
- if (stdout.length > 0) {
429
- log.debug("index.lock held by an active process, skipping removal");
430
- return;
431
- }
432
- } catch {
433
- // lsof unavailable or errored — fall through to remove.
434
- // On platforms without lsof this degrades to unconditional removal,
435
- // which is the same as the previous behavior.
423
+ if (!(await isFileUnheld(lockPath))) {
424
+ log.debug("Could not prove index.lock is stale, skipping removal");
425
+ return;
436
426
  }
437
427
 
438
428
  try {