@vellumai/assistant 0.11.7 → 0.11.8-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -1,16 +1,19 @@
1
1
  /**
2
2
  * Credential metadata store.
3
3
  *
4
- * Reads stay on the workspace `metadata.json` file (sync). When a CES
5
- * record backend is injected, upserts and deletes also write-through to
6
- * CES so the CES catalog stays current.
4
+ * Production: CES owns identity + policy records. This module keeps a
5
+ * synchronous in-process cache so existing call sites stay sync, and
6
+ * write-throughs to CES when a record backend is injected.
7
+ *
8
+ * The cache is filled lazily: the first catalog read or write starts a
9
+ * single-flight CES `list()`. Leftover workspace `metadata.json` is not
10
+ * read. CES import (003) may leave that file in place; this module
11
+ * ignores it.
7
12
  *
8
13
  * Tests: `_setMetadataPath` keeps the file-backed store and skips CES
9
- * write-through.
14
+ * load and write-through.
10
15
  */
11
16
 
12
- import { join } from "node:path";
13
-
14
17
  import {
15
18
  credentialKey,
16
19
  StaticCredentialMetadataStore,
@@ -19,7 +22,6 @@ import type { CredentialRecord } from "@vellumai/service-contracts/credential-rp
19
22
 
20
23
  import type { CredentialRecordBackend } from "../../security/ces-rpc-record-backend.js";
21
24
  import { getLogger } from "../../util/logger.js";
22
- import { getDataDir } from "../../util/platform.js";
23
25
  import type { CredentialInjectionTemplate } from "./policy-types.js";
24
26
 
25
27
  const log = getLogger("credential-metadata-store");
@@ -48,15 +50,23 @@ export interface CredentialMetadata {
48
50
  // Singleton store instance
49
51
  // ---------------------------------------------------------------------------
50
52
 
53
+ const CES_MEMORY_PATH = "ces-in-memory-credential-metadata";
54
+
51
55
  let _store: StaticCredentialMetadataStore | undefined;
52
56
  let _overridePath: string | null = null;
53
57
  let _recordBackend: CredentialRecordBackend | undefined;
58
+ let _cesRecordsLoaded = false;
59
+ let _cesLoad: Promise<void> | undefined;
60
+ let _cesLoadGeneration = 0;
54
61
 
55
62
  function getStore(): StaticCredentialMetadataStore {
56
63
  if (!_store) {
57
- const path =
58
- _overridePath ?? join(getDataDir(), "credentials", "metadata.json");
59
- _store = new StaticCredentialMetadataStore(path);
64
+ if (_overridePath) {
65
+ _store = new StaticCredentialMetadataStore(_overridePath);
66
+ } else {
67
+ _store = new StaticCredentialMetadataStore(CES_MEMORY_PATH);
68
+ _store.useMemory([]);
69
+ }
60
70
  }
61
71
  return _store;
62
72
  }
@@ -76,7 +86,15 @@ function toRecord(metadata: CredentialMetadata): CredentialRecord {
76
86
  };
77
87
  }
78
88
 
79
- async function persistRecord(metadata: CredentialMetadata): Promise<void> {
89
+ function resetCesLoad(): void {
90
+ _cesRecordsLoaded = false;
91
+ _cesLoad = undefined;
92
+ _cesLoadGeneration += 1;
93
+ }
94
+
95
+ async function persistCredentialMetadata(
96
+ metadata: CredentialMetadata,
97
+ ): Promise<void> {
80
98
  if (!_recordBackend || _overridePath) {
81
99
  return;
82
100
  }
@@ -90,17 +108,81 @@ async function persistRecord(metadata: CredentialMetadata): Promise<void> {
90
108
  }
91
109
  }
92
110
 
93
- /** Await CES write-through for a record already in the local file store. */
94
- export async function persistCredentialMetadata(
95
- metadata: CredentialMetadata,
96
- ): Promise<void> {
97
- await persistRecord(metadata);
98
- }
99
-
100
111
  export function setCredentialRecordBackend(
101
112
  backend: CredentialRecordBackend | undefined,
102
113
  ): void {
103
114
  _recordBackend = backend;
115
+ resetCesLoad();
116
+ }
117
+
118
+ function ensureCesRecordsLoaded(): Promise<void> {
119
+ if (_overridePath || !_recordBackend || _cesRecordsLoaded) {
120
+ return Promise.resolve();
121
+ }
122
+ if (_cesLoad) {
123
+ return _cesLoad;
124
+ }
125
+ const generation = _cesLoadGeneration;
126
+ _cesLoad = loadCesRecords(generation).finally(() => {
127
+ if (_cesLoadGeneration === generation) {
128
+ _cesLoad = undefined;
129
+ }
130
+ });
131
+ return _cesLoad;
132
+ }
133
+
134
+ async function loadCesRecords(generation: number): Promise<void> {
135
+ try {
136
+ if (!_recordBackend || _overridePath || generation !== _cesLoadGeneration) {
137
+ return;
138
+ }
139
+ let available = false;
140
+ try {
141
+ available = _recordBackend.isAvailable();
142
+ } catch {
143
+ available = false;
144
+ }
145
+ if (!available) {
146
+ log.warn("CES record backend unavailable; keeping in-process cache");
147
+ return;
148
+ }
149
+
150
+ const remote = await _recordBackend.list();
151
+ if (generation !== _cesLoadGeneration) {
152
+ return;
153
+ }
154
+ if (remote === null) {
155
+ log.warn("CES record list failed; keeping in-process cache");
156
+ return;
157
+ }
158
+
159
+ const store = getStore();
160
+ const localByAccount = new Map(
161
+ store
162
+ .list()
163
+ .map((record) => [credentialKey(record.service, record.field), record]),
164
+ );
165
+ const merged = remote.map((entry) => {
166
+ const local = localByAccount.get(entry.account);
167
+ if (local) {
168
+ localByAccount.delete(entry.account);
169
+ return local;
170
+ }
171
+ return entry.record;
172
+ });
173
+ for (const leftover of localByAccount.values()) {
174
+ merged.push(leftover);
175
+ }
176
+ store.useMemory(merged);
177
+ _cesRecordsLoaded = true;
178
+ log.info({ count: remote.length }, "Loaded credential records from CES");
179
+ } catch (err) {
180
+ log.warn({ err }, "CES record load failed");
181
+ }
182
+ }
183
+
184
+ function touchCesCache(): void {
185
+ void ensureCesRecordsLoaded();
104
186
  }
105
187
 
106
188
  // ---------------------------------------------------------------------------
@@ -113,12 +195,15 @@ export function setCredentialRecordBackend(
113
195
  * so the operation fails cleanly before any side effects.
114
196
  */
115
197
  export function assertMetadataWritable(): void {
198
+ touchCesCache();
116
199
  getStore().assertWritable();
117
200
  }
118
201
 
119
202
  /**
120
203
  * Create or update a credential metadata record.
121
204
  * If a record with the same service+field exists, it is updated.
205
+ * When a CES record backend is attached, the upsert also write-throughs
206
+ * to CES.
122
207
  */
123
208
  export function upsertCredentialMetadata(
124
209
  service: string,
@@ -133,8 +218,9 @@ export function upsertCredentialMetadata(
133
218
  injectionTemplates?: CredentialInjectionTemplate[] | null;
134
219
  },
135
220
  ): CredentialMetadata {
221
+ touchCesCache();
136
222
  const record = getStore().upsert(service, field, policy) as CredentialMetadata;
137
- void persistRecord(record);
223
+ void persistCredentialMetadata(record);
138
224
  return record;
139
225
  }
140
226
 
@@ -145,6 +231,7 @@ export function getCredentialMetadata(
145
231
  service: string,
146
232
  field: string,
147
233
  ): CredentialMetadata | undefined {
234
+ touchCesCache();
148
235
  return getStore().getByServiceField(service, field) as
149
236
  | CredentialMetadata
150
237
  | undefined;
@@ -156,6 +243,7 @@ export function getCredentialMetadata(
156
243
  export function getCredentialMetadataById(
157
244
  credentialId: string,
158
245
  ): CredentialMetadata | undefined {
246
+ touchCesCache();
159
247
  return getStore().getById(credentialId) as CredentialMetadata | undefined;
160
248
  }
161
249
 
@@ -163,6 +251,7 @@ export function getCredentialMetadataById(
163
251
  * List all credential metadata records.
164
252
  */
165
253
  export function listCredentialMetadata(): CredentialMetadata[] {
254
+ touchCesCache();
166
255
  return getStore().list() as CredentialMetadata[];
167
256
  }
168
257
 
@@ -173,6 +262,7 @@ export function deleteCredentialMetadata(
173
262
  service: string,
174
263
  field: string,
175
264
  ): boolean {
265
+ touchCesCache();
176
266
  const deleted = getStore().delete(service, field);
177
267
  if (deleted && _recordBackend && !_overridePath) {
178
268
  void _recordBackend.delete(credentialKey(service, field));
@@ -180,9 +270,15 @@ export function deleteCredentialMetadata(
180
270
  return deleted;
181
271
  }
182
272
 
273
+ /** @internal Test-only: wait for the in-flight CES list to populate the cache. */
274
+ export function _ensureCesRecordsLoaded(): Promise<void> {
275
+ return ensureCesRecordsLoaded();
276
+ }
277
+
183
278
  /** @internal Test-only: override the metadata file path. */
184
279
  export function _setMetadataPath(path: string | null): void {
185
280
  _overridePath = path;
281
+ resetCesLoad();
186
282
  if (_store) {
187
283
  if (path) {
188
284
  _store.setPath(path);
@@ -37,7 +37,6 @@ import {
37
37
  import { getLogger } from "../../util/logger.js";
38
38
  import {
39
39
  assertMetadataWritable,
40
- persistCredentialMetadata,
41
40
  upsertCredentialMetadata,
42
41
  } from "./metadata-store.js";
43
42
  import type { CredentialInjectionTemplate } from "./policy-types.js";
@@ -162,7 +161,6 @@ export async function storeCredentialValue(
162
161
  allowedDomains: input.allowedDomains,
163
162
  injectionTemplates: input.injectionTemplates,
164
163
  });
165
- await persistCredentialMetadata(metadata);
166
164
  const { syncManualTokenConnection } =
167
165
  await import("../../oauth/manual-token-connection.js");
168
166
  await syncManualTokenConnection(service);
@@ -45,7 +45,7 @@ export const fileReadInputSchema = z.looseObject({
45
45
  max_chars: z
46
46
  .number()
47
47
  .describe(
48
- "Maximum number of characters to read. Defaults to 20000, which is also the ceiling. Text files only.",
48
+ "Maximum number of characters to read. Defaults to 100000, which is also the ceiling. Text files only.",
49
49
  )
50
50
  .optional()
51
51
  .catch(undefined),
@@ -61,7 +61,7 @@ export const fileReadInputSchema = z.looseObject({
61
61
  export const fileReadTool = {
62
62
  name: "file_read",
63
63
  description:
64
- "Read the contents of a file on your own machine. Text reads return the first 20000 characters unless you pass `max_chars`; when a read stops short the result says so, and `start_index` pages on from there. To find where something is in a large file, code_search is cheaper than paging through it. For image files (JPEG, PNG, GIF, WebP), returns the image for visual analysis. For audio files (MP3, WAV, OGG, FLAC, AAC, M4A), returns the audio for listening. Use host_file_read for files on your guardian's device instead.",
64
+ "Read the contents of a file on your own machine. Text reads return the first 100000 characters unless you pass `max_chars`; when a read stops short the result says so, and `start_index` pages on from there. To find where something is in a large file, code_search is cheaper than paging through it. For image files (JPEG, PNG, GIF, WebP), returns the image for visual analysis. For audio files (MP3, WAV, OGG, FLAC, AAC, M4A), returns the audio for listening. Use host_file_read for files on your guardian's device instead.",
65
65
  category: "filesystem",
66
66
  executionTarget: "sandbox",
67
67
  defaultRiskLevel: RiskLevel.Low,
@@ -53,7 +53,7 @@ export const hostFileReadInputSchema = z.looseObject({
53
53
  max_chars: z
54
54
  .number()
55
55
  .describe(
56
- "Maximum number of characters to read. Defaults to 20000, which is also the ceiling. Text files only.",
56
+ "Maximum number of characters to read. Defaults to 100000, which is also the ceiling. Text files only.",
57
57
  )
58
58
  .optional()
59
59
  .catch(undefined),
@@ -69,7 +69,7 @@ export const hostFileReadInputSchema = z.looseObject({
69
69
  export const hostFileReadTool = {
70
70
  name: "host_file_read",
71
71
  description:
72
- "Read the contents of a file on your guardian's device, including images (JPEG, PNG, GIF, WebP) and audio (MP3, WAV, OGG, FLAC, AAC, M4A). Text reads return the first 20000 characters unless you pass `max_chars`; when a read stops short the result says so, and `start_index` pages on from there. For files on your own machine, use file_read instead.",
72
+ "Read the contents of a file on your guardian's device, including images (JPEG, PNG, GIF, WebP) and audio (MP3, WAV, OGG, FLAC, AAC, M4A). Text reads return the first 100000 characters unless you pass `max_chars`; when a read stops short the result says so, and `start_index` pages on from there. For files on your own machine, use file_read instead.",
73
73
  category: "host-filesystem",
74
74
  executionTarget: "host",
75
75
  defaultRiskLevel: RiskLevel.Medium,
@@ -104,6 +104,10 @@ mock.module("../../daemon/host-bash-proxy.js", () => ({
104
104
  // Import under test — MUST come after mock.module calls.
105
105
  // ---------------------------------------------------------------------------
106
106
 
107
+ import {
108
+ MAX_OUTPUT_LENGTH,
109
+ OUTPUT_TRUNCATED_TAG,
110
+ } from "../shared/shell-output.js";
107
111
  import type { ToolContext, ToolExecutionResult } from "../types.js";
108
112
  import { hostShellTool } from "./host-shell.js";
109
113
 
@@ -362,4 +366,24 @@ describe("host_bash background lifecycle events — direct spawn path", () => {
362
366
  expect(completed[0]!.output).toContain("cancelled");
363
367
  expect(completed[0]!.output).not.toContain("failed");
364
368
  });
369
+
370
+ test("drops stdout past the cap without a recovery file", async () => {
371
+ await hostShellTool.execute(
372
+ { command: "yes", background: true },
373
+ makeContext(),
374
+ );
375
+
376
+ latestChild!.stdout.emit("data", Buffer.alloc(80_000, 0x78));
377
+ latestChild!.stdout.emit("data", Buffer.alloc(80_000, 0x79));
378
+ latestChild!.emit("close", 0);
379
+ await flush();
380
+
381
+ const completed = completedEvents();
382
+ expect(completed).toHaveLength(1);
383
+ const output = String(completed[0]!.output);
384
+ expect(output).toContain(OUTPUT_TRUNCATED_TAG);
385
+ expect(output).not.toContain("file=");
386
+ expect(output.length).toBeLessThan(MAX_OUTPUT_LENGTH + 80);
387
+ expect(output).not.toContain("y");
388
+ });
365
389
  });
@@ -40,7 +40,7 @@ import {
40
40
  removeBackgroundTool,
41
41
  } from "../background-tool-registry.js";
42
42
  import {
43
- formatShellOutput,
43
+ attachBoundedStdio,
44
44
  MAX_OUTPUT_LENGTH,
45
45
  } from "../shared/shell-output.js";
46
46
  import {
@@ -336,7 +336,7 @@ export const hostShellTool = {
336
336
  untrustedOutput: {
337
337
  content: output || "(no output)",
338
338
  source: "tool_result",
339
- // Preserve formatShellOutput's recovery marker (see shell.ts).
339
+ // Bounded output plus the truncation marker (see shell.ts).
340
340
  maxChars: MAX_OUTPUT_LENGTH * 2,
341
341
  },
342
342
  });
@@ -472,10 +472,10 @@ export const hostShellTool = {
472
472
  env: hostEnv,
473
473
  stdio: ["ignore", "pipe", "pipe"],
474
474
  detached: true,
475
+ windowsHide: true,
475
476
  });
476
477
 
477
- const stdoutChunks: Buffer[] = [];
478
- const stderrChunks: Buffer[] = [];
478
+ const collector = attachBoundedStdio(child);
479
479
  let timedOut = false;
480
480
  // Set when cancelled via the registry's cancel callback so the completion
481
481
  // event reports "cancelled" rather than "failed".
@@ -488,9 +488,6 @@ export const hostShellTool = {
488
488
  killTree();
489
489
  }, timeoutMs);
490
490
 
491
- child.stdout.on("data", (data: Buffer) => stdoutChunks.push(data));
492
- child.stderr.on("data", (data: Buffer) => stderrChunks.push(data));
493
-
494
491
  // Guard against double-wake: when spawn fails (e.g. invalid cwd),
495
492
  // Node emits both 'error' and 'close' for the same child process.
496
493
  // Only the first handler to fire should wake the agent.
@@ -502,15 +499,7 @@ export const hostShellTool = {
502
499
  }
503
500
  completed = true;
504
501
  clearTimeout(timer);
505
- const stdout = Buffer.concat(stdoutChunks).toString();
506
- const stderr = Buffer.concat(stderrChunks).toString();
507
- const result = formatShellOutput(
508
- stdout,
509
- stderr,
510
- code,
511
- timedOut,
512
- timeoutSec,
513
- );
502
+ const result = collector.format(code, timedOut, timeoutSec);
514
503
  // Cancel takes precedence over the SIGKILL-induced error result.
515
504
  const status = aborted
516
505
  ? "cancelled"
@@ -563,7 +552,7 @@ export const hostShellTool = {
563
552
  untrustedOutput: {
564
553
  content: output || "(no output)",
565
554
  source: "tool_result",
566
- // Preserve formatShellOutput's recovery marker (see shell.ts).
555
+ // Bounded output plus the truncation marker (see shell.ts).
567
556
  maxChars: MAX_OUTPUT_LENGTH * 2,
568
557
  },
569
558
  });
@@ -649,8 +638,6 @@ export const hostShellTool = {
649
638
  }
650
639
 
651
640
  return new Promise<ToolExecutionResult>((resolve) => {
652
- const stdoutChunks: Buffer[] = [];
653
- const stderrChunks: Buffer[] = [];
654
641
  let timedOut = false;
655
642
 
656
643
  const wrapped = buildShellInvocation(command);
@@ -659,6 +646,10 @@ export const hostShellTool = {
659
646
  env: hostEnv,
660
647
  stdio: ["ignore", "pipe", "pipe"],
661
648
  detached: true,
649
+ windowsHide: true,
650
+ });
651
+ const collector = attachBoundedStdio(child, {
652
+ onOutput: context.onOutput,
662
653
  });
663
654
 
664
655
  const killTree = () => terminateProcessTree(child);
@@ -678,29 +669,11 @@ export const hostShellTool = {
678
669
  }
679
670
  }
680
671
 
681
- child.stdout.on("data", (data: Buffer) => {
682
- stdoutChunks.push(data);
683
- context.onOutput?.(data.toString());
684
- });
685
-
686
- child.stderr.on("data", (data: Buffer) => {
687
- stderrChunks.push(data);
688
- context.onOutput?.(data.toString());
689
- });
690
-
691
672
  child.on("close", (code) => {
692
673
  clearTimeout(timer);
693
674
  context.signal?.removeEventListener("abort", onAbort);
694
675
 
695
- const stdout = Buffer.concat(stdoutChunks).toString();
696
- const stderr = Buffer.concat(stderrChunks).toString();
697
- const result = formatShellOutput(
698
- stdout,
699
- stderr,
700
- code,
701
- timedOut,
702
- timeoutSec,
703
- );
676
+ const result = collector.format(code, timedOut, timeoutSec);
704
677
 
705
678
  resolve({
706
679
  content: result.content,
@@ -78,12 +78,15 @@ function pathError(
78
78
  }
79
79
 
80
80
  /**
81
- * Characters returned by a read that names no `max_chars`. Stays under
82
- * `THRESHOLD_CHARS` in `context/post-turn-tool-result-truncation.ts`, which
83
- * spools any larger tool result to disk and replaces it inline with a short
84
- * stub, so a default read returns content rather than a stub.
81
+ * Characters returned by a read that names no `max_chars`, and the ceiling for
82
+ * one that does. File reads are exempt from the result-time spool pass
83
+ * (`RESULT_TIME_SPOOL_EXEMPT_TOOLS` in `context/tool-result-spool.ts`), so the
84
+ * full window reaches the model for the turn that read it; the post-turn pass
85
+ * compacts oversized results to recoverable stubs at turn end, and the
86
+ * `tool-result-truncate` plugin bounds any single result against the model's
87
+ * context window.
85
88
  */
86
- export const READ_CHAR_BUDGET = 20_000;
89
+ export const READ_CHAR_BUDGET = 100_000;
87
90
 
88
91
  /**
89
92
  * Trailing marker appended when a read stops short of the end of the file. A
@@ -169,8 +172,8 @@ export class FileSystemOps {
169
172
  try {
170
173
  const raw = await readFile(filePath, "utf-8");
171
174
 
172
- // A ceiling, not just a default: a larger window would be spooled to
173
- // disk and replaced with a stub, returning less than this.
175
+ // A ceiling, not just a default: an uncapped `max_chars` would put an
176
+ // arbitrarily large file inline in a single result.
174
177
  const maxChars = Math.min(
175
178
  READ_CHAR_BUDGET,
176
179
  Math.max(0, input.maxChars ?? READ_CHAR_BUDGET),
@@ -0,0 +1,104 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import {
5
+ attachBoundedStdio,
6
+ BoundedStdioCollector,
7
+ formatShellOutput,
8
+ MAX_OUTPUT_LENGTH,
9
+ OUTPUT_TRUNCATED_TAG,
10
+ } from "./shell-output.js";
11
+
12
+ describe("BoundedStdioCollector", () => {
13
+ test("keeps output under the cap", () => {
14
+ const collector = new BoundedStdioCollector();
15
+ collector.consume("stdout", Buffer.from("hello"));
16
+ const result = collector.format(0, false, 120);
17
+ expect(result.content).toBe("hello");
18
+ expect(collector.didTruncate).toBe(false);
19
+ expect(collector.keptByteLength).toBe(5);
20
+ });
21
+
22
+ test("caps a single oversized chunk without retaining the tail", () => {
23
+ const collector = new BoundedStdioCollector();
24
+ const huge = Buffer.alloc(MAX_OUTPUT_LENGTH + 50_000, 0x78);
25
+ collector.consume("stdout", huge);
26
+ huge.fill(0x79);
27
+ expect(collector.keptByteLength).toBe(MAX_OUTPUT_LENGTH);
28
+ expect(collector.didTruncate).toBe(true);
29
+ const result = collector.format(0, false, 120);
30
+ expect(result.content).toContain(OUTPUT_TRUNCATED_TAG);
31
+ expect(result.content).not.toContain("file=");
32
+ expect(result.content.startsWith("x".repeat(MAX_OUTPUT_LENGTH))).toBe(true);
33
+ expect(result.content.length).toBeLessThan(MAX_OUTPUT_LENGTH + 80);
34
+ });
35
+
36
+ test("shares the budget across stdout and stderr", () => {
37
+ const collector = new BoundedStdioCollector();
38
+ collector.consume("stdout", Buffer.alloc(MAX_OUTPUT_LENGTH - 10, 0x61));
39
+ collector.consume("stderr", Buffer.alloc(100, 0x62));
40
+ expect(collector.keptByteLength).toBe(MAX_OUTPUT_LENGTH);
41
+ expect(collector.didTruncate).toBe(true);
42
+ const result = collector.format(0, false, 120);
43
+ expect(result.content).toContain(OUTPUT_TRUNCATED_TAG);
44
+ expect(result.content.includes("b")).toBe(true);
45
+ });
46
+
47
+ test("ignores chunks after the cap", () => {
48
+ const collector = new BoundedStdioCollector();
49
+ collector.consume("stdout", Buffer.alloc(MAX_OUTPUT_LENGTH, 0x78));
50
+ const afterCap = collector.keptByteLength;
51
+ collector.consume("stdout", Buffer.alloc(10_000, 0x79));
52
+ collector.consume("stderr", Buffer.alloc(10_000, 0x7a));
53
+ expect(collector.keptByteLength).toBe(afterCap);
54
+ const result = collector.format(0, false, 120);
55
+ expect(result.content).not.toContain("y");
56
+ expect(result.content).not.toContain("z");
57
+ });
58
+
59
+ test("does not call onOutput for discarded bytes", () => {
60
+ const seen: string[] = [];
61
+ const collector = new BoundedStdioCollector();
62
+ collector.consume("stdout", Buffer.from("abc"), (text) => seen.push(text));
63
+ collector.consume("stdout", Buffer.alloc(MAX_OUTPUT_LENGTH, 0x78), (text) =>
64
+ seen.push(text),
65
+ );
66
+ collector.consume("stdout", Buffer.from("TAIL"), (text) => seen.push(text));
67
+ const forwarded = seen.join("");
68
+ expect(forwarded).not.toContain("TAIL");
69
+ expect(Buffer.byteLength(forwarded)).toBe(MAX_OUTPUT_LENGTH);
70
+ });
71
+ });
72
+
73
+ describe("attachBoundedStdio", () => {
74
+ test("drains further data events after the cap without growing kept bytes", () => {
75
+ const child = {
76
+ stdout: new EventEmitter(),
77
+ stderr: new EventEmitter(),
78
+ };
79
+ const collector = attachBoundedStdio(child);
80
+ child.stdout.emit("data", Buffer.alloc(MAX_OUTPUT_LENGTH + 1000, 0x78));
81
+ child.stdout.emit("data", Buffer.alloc(20_000, 0x79));
82
+ child.stderr.emit("data", Buffer.alloc(20_000, 0x7a));
83
+ expect(collector.keptByteLength).toBe(MAX_OUTPUT_LENGTH);
84
+ expect(collector.didTruncate).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe("formatShellOutput truncation", () => {
89
+ test("truncates an already-materialized oversized string without a file path", () => {
90
+ const longOutput = "x".repeat(30_000);
91
+ const result = formatShellOutput(longOutput, "", 0, false, 120);
92
+ expect(result.content).toContain(OUTPUT_TRUNCATED_TAG);
93
+ expect(result.content).not.toContain("file=");
94
+ expect(result.content.length).toBeLessThan(MAX_OUTPUT_LENGTH + 80);
95
+ });
96
+
97
+ test("honors an explicit truncated flag at the exact cap", () => {
98
+ const exact = "x".repeat(MAX_OUTPUT_LENGTH);
99
+ const result = formatShellOutput(exact, "", 0, false, 120, {
100
+ truncated: true,
101
+ });
102
+ expect(result.content).toContain(OUTPUT_TRUNCATED_TAG);
103
+ });
104
+ });