vellum 0.0.16 → 0.2.0

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 (838) hide show
  1. package/.dockerignore +27 -0
  2. package/.env.example +22 -0
  3. package/Dockerfile +99 -0
  4. package/Dockerfile.sandbox +5 -0
  5. package/README.md +150 -3
  6. package/bun.lock +1768 -0
  7. package/bunfig.toml +2 -0
  8. package/docs/skills.md +158 -0
  9. package/drizzle/0000_dizzy_maggott.sql +301 -0
  10. package/drizzle/meta/0000_snapshot.json +1999 -0
  11. package/drizzle/meta/_journal.json +13 -0
  12. package/drizzle.config.ts +7 -0
  13. package/eslint.config.mjs +17 -0
  14. package/hook-templates/debug-prompt-logger/hook.json +7 -0
  15. package/hook-templates/debug-prompt-logger/run.sh +68 -0
  16. package/knip.json +9 -0
  17. package/package.json +60 -10
  18. package/scripts/ipc/check-contract-inventory.ts +104 -0
  19. package/scripts/ipc/check-swift-decoder-drift.ts +163 -0
  20. package/scripts/ipc/generate-swift.ts +492 -0
  21. package/scripts/test-filesystem-tools.sh +48 -0
  22. package/scripts/test.sh +122 -0
  23. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2079 -0
  24. package/src/__tests__/account-registry.test.ts +244 -0
  25. package/src/__tests__/active-skill-tools.test.ts +378 -0
  26. package/src/__tests__/agent-loop-thinking.test.ts +81 -0
  27. package/src/__tests__/agent-loop.test.ts +1135 -0
  28. package/src/__tests__/anthropic-provider.test.ts +778 -0
  29. package/src/__tests__/app-builder-tool-scripts.test.ts +290 -0
  30. package/src/__tests__/app-bundler.test.ts +313 -0
  31. package/src/__tests__/app-executors.test.ts +613 -0
  32. package/src/__tests__/app-open-proxy.test.ts +62 -0
  33. package/src/__tests__/asset-materialize-tool.test.ts +451 -0
  34. package/src/__tests__/asset-search-tool.test.ts +476 -0
  35. package/src/__tests__/assistant-attachment-directive.test.ts +401 -0
  36. package/src/__tests__/assistant-attachments.test.ts +437 -0
  37. package/src/__tests__/assistant-event-hub.test.ts +226 -0
  38. package/src/__tests__/assistant-event.test.ts +123 -0
  39. package/src/__tests__/attachments-store.test.ts +547 -0
  40. package/src/__tests__/attachments.test.ts +134 -0
  41. package/src/__tests__/audit-log-rotation.test.ts +154 -0
  42. package/src/__tests__/browser-fill-credential.test.ts +309 -0
  43. package/src/__tests__/browser-manager.test.ts +203 -0
  44. package/src/__tests__/browser-runtime-check.test.ts +55 -0
  45. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +67 -0
  46. package/src/__tests__/browser-skill-endstate.test.ts +198 -0
  47. package/src/__tests__/bundle-scanner.test.ts +313 -0
  48. package/src/__tests__/checker.test.ts +3856 -0
  49. package/src/__tests__/clarification-resolver.test.ts +159 -0
  50. package/src/__tests__/classifier.test.ts +67 -0
  51. package/src/__tests__/claude-code-skill-regression.test.ts +127 -0
  52. package/src/__tests__/claude-code-tool-profiles.test.ts +88 -0
  53. package/src/__tests__/cli-discover.test.ts +85 -0
  54. package/src/__tests__/cli.test.ts +81 -0
  55. package/src/__tests__/clipboard.test.ts +80 -0
  56. package/src/__tests__/commit-guarantee.test.ts +335 -0
  57. package/src/__tests__/computer-use-session-compaction.test.ts +132 -0
  58. package/src/__tests__/computer-use-session-lifecycle.test.ts +293 -0
  59. package/src/__tests__/computer-use-session-working-dir.test.ts +117 -0
  60. package/src/__tests__/computer-use-skill-baseline.test.ts +74 -0
  61. package/src/__tests__/computer-use-skill-endstate.test.ts +89 -0
  62. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +217 -0
  63. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +107 -0
  64. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +54 -0
  65. package/src/__tests__/config-schema.test.ts +720 -0
  66. package/src/__tests__/conflict-store.test.ts +329 -0
  67. package/src/__tests__/connection-policy.test.ts +102 -0
  68. package/src/__tests__/context-memory-e2e.test.ts +434 -0
  69. package/src/__tests__/context-token-estimator.test.ts +135 -0
  70. package/src/__tests__/context-window-manager.test.ts +376 -0
  71. package/src/__tests__/contradiction-checker.test.ts +216 -0
  72. package/src/__tests__/conversation-store.test.ts +614 -0
  73. package/src/__tests__/credential-broker-browser-fill.test.ts +517 -0
  74. package/src/__tests__/credential-broker-server-use.test.ts +554 -0
  75. package/src/__tests__/credential-broker.test.ts +167 -0
  76. package/src/__tests__/credential-host-pattern-match.test.ts +104 -0
  77. package/src/__tests__/credential-metadata-store.test.ts +779 -0
  78. package/src/__tests__/credential-policy-validate.test.ts +121 -0
  79. package/src/__tests__/credential-resolve.test.ts +328 -0
  80. package/src/__tests__/credential-security-e2e.test.ts +352 -0
  81. package/src/__tests__/credential-security-invariants.test.ts +563 -0
  82. package/src/__tests__/credential-selection.test.ts +354 -0
  83. package/src/__tests__/credential-vault.test.ts +852 -0
  84. package/src/__tests__/daemon-assistant-events.test.ts +164 -0
  85. package/src/__tests__/daemon-server-session-init.test.ts +522 -0
  86. package/src/__tests__/delete-managed-skill-tool.test.ts +97 -0
  87. package/src/__tests__/diff.test.ts +121 -0
  88. package/src/__tests__/domain-normalize.test.ts +112 -0
  89. package/src/__tests__/domain-policy.test.ts +124 -0
  90. package/src/__tests__/doordash-client.test.ts +186 -0
  91. package/src/__tests__/doordash-session.test.ts +143 -0
  92. package/src/__tests__/dynamic-page-surface.test.ts +91 -0
  93. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +132 -0
  94. package/src/__tests__/edit-engine.test.ts +180 -0
  95. package/src/__tests__/email-cli.test.ts +283 -0
  96. package/src/__tests__/encrypted-store.test.ts +332 -0
  97. package/src/__tests__/entity-extractor.test.ts +190 -0
  98. package/src/__tests__/ephemeral-permissions.test.ts +312 -0
  99. package/src/__tests__/evaluate-typescript-tool.test.ts +286 -0
  100. package/src/__tests__/event-bus.test.ts +222 -0
  101. package/src/__tests__/file-edit-tool.test.ts +122 -0
  102. package/src/__tests__/file-ops-service.test.ts +330 -0
  103. package/src/__tests__/file-read-tool.test.ts +75 -0
  104. package/src/__tests__/file-write-tool.test.ts +113 -0
  105. package/src/__tests__/fixtures/credential-security-fixtures.ts +181 -0
  106. package/src/__tests__/fixtures/media-reuse-fixtures.ts +126 -0
  107. package/src/__tests__/fixtures/mock-signup-server.ts +387 -0
  108. package/src/__tests__/fixtures/proxy-fixtures.ts +147 -0
  109. package/src/__tests__/fuzzy-match-property.test.ts +216 -0
  110. package/src/__tests__/fuzzy-match.test.ts +138 -0
  111. package/src/__tests__/gemini-image-service.test.ts +261 -0
  112. package/src/__tests__/gemini-provider.test.ts +651 -0
  113. package/src/__tests__/get-weather.test.ts +318 -0
  114. package/src/__tests__/gmail-integration.test.ts +73 -0
  115. package/src/__tests__/handlers-cu-observation-blob.test.ts +351 -0
  116. package/src/__tests__/handlers-ipc-blob-probe.test.ts +190 -0
  117. package/src/__tests__/handlers-slack-config.test.ts +199 -0
  118. package/src/__tests__/handlers-task-submit-slash.test.ts +38 -0
  119. package/src/__tests__/headless-browser-interactions.test.ts +536 -0
  120. package/src/__tests__/headless-browser-navigate.test.ts +211 -0
  121. package/src/__tests__/headless-browser-read-tools.test.ts +261 -0
  122. package/src/__tests__/headless-browser-snapshot.test.ts +185 -0
  123. package/src/__tests__/history-repair-observability.test.ts +56 -0
  124. package/src/__tests__/history-repair.test.ts +510 -0
  125. package/src/__tests__/home-base-bootstrap.test.ts +77 -0
  126. package/src/__tests__/hooks-blocking.test.ts +128 -0
  127. package/src/__tests__/hooks-cli.test.ts +144 -0
  128. package/src/__tests__/hooks-config.test.ts +93 -0
  129. package/src/__tests__/hooks-discovery.test.ts +199 -0
  130. package/src/__tests__/hooks-integration.test.ts +189 -0
  131. package/src/__tests__/hooks-manager.test.ts +187 -0
  132. package/src/__tests__/hooks-runner.test.ts +178 -0
  133. package/src/__tests__/hooks-settings.test.ts +154 -0
  134. package/src/__tests__/hooks-templates.test.ts +137 -0
  135. package/src/__tests__/hooks-ts-runner.test.ts +125 -0
  136. package/src/__tests__/hooks-watch.test.ts +100 -0
  137. package/src/__tests__/host-file-edit-tool.test.ts +104 -0
  138. package/src/__tests__/host-file-read-tool.test.ts +61 -0
  139. package/src/__tests__/host-file-write-tool.test.ts +77 -0
  140. package/src/__tests__/host-shell-tool.test.ts +311 -0
  141. package/src/__tests__/intent-routing.test.ts +255 -0
  142. package/src/__tests__/ipc-blob-store.test.ts +315 -0
  143. package/src/__tests__/ipc-contract-inventory.test.ts +54 -0
  144. package/src/__tests__/ipc-contract.test.ts +74 -0
  145. package/src/__tests__/ipc-protocol.test.ts +113 -0
  146. package/src/__tests__/ipc-snapshot.test.ts +1560 -0
  147. package/src/__tests__/ipc-validate.test.ts +357 -0
  148. package/src/__tests__/key-migration.test.ts +183 -0
  149. package/src/__tests__/keychain.test.ts +258 -0
  150. package/src/__tests__/llm-usage-store.test.ts +226 -0
  151. package/src/__tests__/managed-skill-lifecycle.test.ts +257 -0
  152. package/src/__tests__/managed-store.test.ts +608 -0
  153. package/src/__tests__/media-generate-image.test.ts +238 -0
  154. package/src/__tests__/media-reuse-story.e2e.test.ts +676 -0
  155. package/src/__tests__/media-visibility-policy.test.ts +141 -0
  156. package/src/__tests__/memory-context-benchmark.test.ts +235 -0
  157. package/src/__tests__/memory-lifecycle-e2e.test.ts +481 -0
  158. package/src/__tests__/memory-query-builder.test.ts +59 -0
  159. package/src/__tests__/memory-recall-quality.test.ts +846 -0
  160. package/src/__tests__/memory-regressions.experimental.test.ts +538 -0
  161. package/src/__tests__/memory-regressions.test.ts +4238 -0
  162. package/src/__tests__/memory-retrieval-budget.test.ts +49 -0
  163. package/src/__tests__/migration-cli-flows.test.ts +169 -0
  164. package/src/__tests__/migration-ordering.test.ts +249 -0
  165. package/src/__tests__/mock-signup-server.test.ts +528 -0
  166. package/src/__tests__/onboarding-starter-tasks.test.ts +166 -0
  167. package/src/__tests__/onboarding-template-contract.test.ts +58 -0
  168. package/src/__tests__/openai-provider.test.ts +753 -0
  169. package/src/__tests__/parser.test.ts +472 -0
  170. package/src/__tests__/path-classifier.test.ts +73 -0
  171. package/src/__tests__/path-policy.test.ts +435 -0
  172. package/src/__tests__/platform-move-helper.test.ts +99 -0
  173. package/src/__tests__/platform-socket-path.test.ts +52 -0
  174. package/src/__tests__/platform-workspace-migration.test.ts +1000 -0
  175. package/src/__tests__/platform.test.ts +131 -0
  176. package/src/__tests__/prebuilt-home-base-seed.test.ts +71 -0
  177. package/src/__tests__/pricing.test.ts +256 -0
  178. package/src/__tests__/profile-compiler.test.ts +373 -0
  179. package/src/__tests__/provider-registry-ollama.test.ts +16 -0
  180. package/src/__tests__/proxy-approval-callback.test.ts +601 -0
  181. package/src/__tests__/ratelimit.test.ts +297 -0
  182. package/src/__tests__/registry.test.ts +487 -0
  183. package/src/__tests__/reminder-store.test.ts +220 -0
  184. package/src/__tests__/reminder.test.ts +263 -0
  185. package/src/__tests__/request-file-tool.test.ts +158 -0
  186. package/src/__tests__/run-orchestrator.test.ts +200 -0
  187. package/src/__tests__/runtime-attachment-metadata.test.ts +190 -0
  188. package/src/__tests__/runtime-runs-http.test.ts +451 -0
  189. package/src/__tests__/runtime-runs.test.ts +273 -0
  190. package/src/__tests__/sandbox-diagnostics.test.ts +408 -0
  191. package/src/__tests__/sandbox-host-parity.test.ts +950 -0
  192. package/src/__tests__/scaffold-managed-skill-tool.test.ts +253 -0
  193. package/src/__tests__/script-proxy-certs.test.ts +90 -0
  194. package/src/__tests__/script-proxy-connect-tunnel.test.ts +177 -0
  195. package/src/__tests__/script-proxy-decision-trace.test.ts +156 -0
  196. package/src/__tests__/script-proxy-http-forwarder.test.ts +281 -0
  197. package/src/__tests__/script-proxy-injection-runtime.test.ts +401 -0
  198. package/src/__tests__/script-proxy-mitm-handler.test.ts +407 -0
  199. package/src/__tests__/script-proxy-policy-runtime.test.ts +287 -0
  200. package/src/__tests__/script-proxy-policy.test.ts +310 -0
  201. package/src/__tests__/script-proxy-rewrite-specificity.test.ts +135 -0
  202. package/src/__tests__/script-proxy-router.test.ts +180 -0
  203. package/src/__tests__/script-proxy-session-manager.test.ts +382 -0
  204. package/src/__tests__/script-proxy-session-runtime.test.ts +113 -0
  205. package/src/__tests__/secret-allowlist.test.ts +229 -0
  206. package/src/__tests__/secret-ingress-handler.test.ts +99 -0
  207. package/src/__tests__/secret-onetime-send.test.ts +130 -0
  208. package/src/__tests__/secret-prompt-log-hygiene.test.ts +106 -0
  209. package/src/__tests__/secret-response-routing.test.ts +93 -0
  210. package/src/__tests__/secret-scanner-executor.test.ts +348 -0
  211. package/src/__tests__/secret-scanner.test.ts +857 -0
  212. package/src/__tests__/secure-keys.test.ts +323 -0
  213. package/src/__tests__/server-history-render.test.ts +430 -0
  214. package/src/__tests__/session-abort-tool-results.test.ts +240 -0
  215. package/src/__tests__/session-conflict-gate.test.ts +697 -0
  216. package/src/__tests__/session-error.test.ts +341 -0
  217. package/src/__tests__/session-evictor.test.ts +188 -0
  218. package/src/__tests__/session-load-history-repair.test.ts +222 -0
  219. package/src/__tests__/session-pre-run-repair.test.ts +213 -0
  220. package/src/__tests__/session-profile-injection.test.ts +444 -0
  221. package/src/__tests__/session-provider-retry-repair.test.ts +306 -0
  222. package/src/__tests__/session-queue.test.ts +1462 -0
  223. package/src/__tests__/session-runtime-assembly.test.ts +315 -0
  224. package/src/__tests__/session-runtime-workspace.test.ts +183 -0
  225. package/src/__tests__/session-skill-tools.test.ts +2431 -0
  226. package/src/__tests__/session-slash-known.test.ts +368 -0
  227. package/src/__tests__/session-slash-queue.test.ts +288 -0
  228. package/src/__tests__/session-slash-unknown.test.ts +271 -0
  229. package/src/__tests__/session-tool-setup-app-refresh.test.ts +473 -0
  230. package/src/__tests__/session-tool-setup-memory-scope.test.ts +140 -0
  231. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +140 -0
  232. package/src/__tests__/session-undo.test.ts +75 -0
  233. package/src/__tests__/session-workspace-cache-state.test.ts +246 -0
  234. package/src/__tests__/session-workspace-injection.test.ts +327 -0
  235. package/src/__tests__/session-workspace-tool-tracking.test.ts +240 -0
  236. package/src/__tests__/shared-filesystem-errors.test.ts +78 -0
  237. package/src/__tests__/shell-credential-ref.test.ts +187 -0
  238. package/src/__tests__/shell-parser-fuzz.test.ts +544 -0
  239. package/src/__tests__/shell-parser-property.test.ts +433 -0
  240. package/src/__tests__/shell-tool-proxy-mode.test.ts +272 -0
  241. package/src/__tests__/signup-e2e.test.ts +352 -0
  242. package/src/__tests__/size-guard.test.ts +117 -0
  243. package/src/__tests__/skill-include-graph.test.ts +303 -0
  244. package/src/__tests__/skill-load-tool.test.ts +409 -0
  245. package/src/__tests__/skill-script-runner-host.test.ts +489 -0
  246. package/src/__tests__/skill-script-runner-sandbox.test.ts +349 -0
  247. package/src/__tests__/skill-tool-factory.test.ts +252 -0
  248. package/src/__tests__/skill-tool-manifest.test.ts +658 -0
  249. package/src/__tests__/skill-version-hash.test.ts +182 -0
  250. package/src/__tests__/skills.test.ts +597 -0
  251. package/src/__tests__/slash-commands-catalog.test.ts +86 -0
  252. package/src/__tests__/slash-commands-parser.test.ts +119 -0
  253. package/src/__tests__/slash-commands-resolver.test.ts +193 -0
  254. package/src/__tests__/slash-commands-rewrite.test.ts +39 -0
  255. package/src/__tests__/starter-bundle.test.ts +136 -0
  256. package/src/__tests__/starter-task-flow.test.ts +143 -0
  257. package/src/__tests__/subagent-manager-notify.test.ts +372 -0
  258. package/src/__tests__/subagent-tools.test.ts +118 -0
  259. package/src/__tests__/subagent-types.test.ts +78 -0
  260. package/src/__tests__/swarm-orchestrator.test.ts +428 -0
  261. package/src/__tests__/swarm-plan-validator.test.ts +330 -0
  262. package/src/__tests__/swarm-recursion.test.ts +165 -0
  263. package/src/__tests__/swarm-router-planner.test.ts +208 -0
  264. package/src/__tests__/swarm-session-integration.test.ts +274 -0
  265. package/src/__tests__/swarm-tool.test.ts +145 -0
  266. package/src/__tests__/swarm-worker-backend.test.ts +129 -0
  267. package/src/__tests__/swarm-worker-runner.test.ts +272 -0
  268. package/src/__tests__/system-prompt.test.ts +461 -0
  269. package/src/__tests__/task-compiler.test.ts +283 -0
  270. package/src/__tests__/task-runner.test.ts +215 -0
  271. package/src/__tests__/task-scheduler.test.ts +216 -0
  272. package/src/__tests__/task-tools.test.ts +602 -0
  273. package/src/__tests__/terminal-sandbox-docker.test.ts +1064 -0
  274. package/src/__tests__/terminal-sandbox.integration.test.ts +178 -0
  275. package/src/__tests__/terminal-sandbox.test.ts +202 -0
  276. package/src/__tests__/test-support/browser-skill-harness.ts +90 -0
  277. package/src/__tests__/test-support/computer-use-skill-harness.ts +45 -0
  278. package/src/__tests__/tool-audit-listener.test.ts +112 -0
  279. package/src/__tests__/tool-domain-event-publisher.test.ts +251 -0
  280. package/src/__tests__/tool-executor-lifecycle-events.test.ts +516 -0
  281. package/src/__tests__/tool-executor-redaction.test.ts +289 -0
  282. package/src/__tests__/tool-executor.test.ts +1971 -0
  283. package/src/__tests__/tool-metrics-listener.test.ts +225 -0
  284. package/src/__tests__/tool-notification-listener.test.ts +49 -0
  285. package/src/__tests__/tool-policy.test.ts +54 -0
  286. package/src/__tests__/tool-profiling-listener.test.ts +268 -0
  287. package/src/__tests__/tool-result-truncation.test.ts +217 -0
  288. package/src/__tests__/tool-trace-listener.test.ts +226 -0
  289. package/src/__tests__/top-level-renderer.test.ts +121 -0
  290. package/src/__tests__/top-level-scanner.test.ts +141 -0
  291. package/src/__tests__/trace-emitter.test.ts +173 -0
  292. package/src/__tests__/trust-store.test.ts +2030 -0
  293. package/src/__tests__/turn-commit.test.ts +219 -0
  294. package/src/__tests__/url-safety.test.ts +418 -0
  295. package/src/__tests__/weather-skill-regression.test.ts +225 -0
  296. package/src/__tests__/web-fetch.test.ts +869 -0
  297. package/src/__tests__/web-search.test.ts +584 -0
  298. package/src/__tests__/workspace-git-service.test.ts +750 -0
  299. package/src/__tests__/workspace-heartbeat-service.test.ts +347 -0
  300. package/src/__tests__/workspace-lifecycle.test.ts +292 -0
  301. package/src/agent/attachments.ts +35 -0
  302. package/src/agent/loop.ts +500 -0
  303. package/src/agent/message-types.ts +17 -0
  304. package/src/autonomy/autonomy-resolver.ts +60 -0
  305. package/src/autonomy/autonomy-store.ts +122 -0
  306. package/src/autonomy/disposition-mapper.ts +31 -0
  307. package/src/autonomy/index.ts +11 -0
  308. package/src/autonomy/types.ts +39 -0
  309. package/src/bundler/app-bundler.ts +274 -0
  310. package/src/bundler/bundle-scanner.ts +535 -0
  311. package/src/bundler/bundle-signer.ts +124 -0
  312. package/src/bundler/manifest.ts +21 -0
  313. package/src/bundler/signature-verifier.ts +184 -0
  314. package/src/cli/autonomy.ts +188 -0
  315. package/src/cli/contacts.ts +149 -0
  316. package/src/cli/doordash.ts +824 -0
  317. package/src/cli/email-guardrails.ts +200 -0
  318. package/src/cli/email.ts +405 -0
  319. package/src/cli/main-screen.tsx +155 -0
  320. package/src/cli.ts +935 -0
  321. package/src/config/bundled-skills/.gitkeep +0 -0
  322. package/src/config/bundled-skills/agentmail/SKILL.md +128 -0
  323. package/src/config/bundled-skills/agentmail/icon.svg +21 -0
  324. package/src/config/bundled-skills/app-builder/SKILL.md +1348 -0
  325. package/src/config/bundled-skills/app-builder/TOOLS.json +279 -0
  326. package/src/config/bundled-skills/app-builder/icon.svg +9 -0
  327. package/src/config/bundled-skills/app-builder/tools/app-create.ts +15 -0
  328. package/src/config/bundled-skills/app-builder/tools/app-delete.ts +10 -0
  329. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +11 -0
  330. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +10 -0
  331. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +18 -0
  332. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +11 -0
  333. package/src/config/bundled-skills/app-builder/tools/app-list.ts +10 -0
  334. package/src/config/bundled-skills/app-builder/tools/app-query.ts +10 -0
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +20 -0
  336. package/src/config/bundled-skills/browser/SKILL.md +28 -0
  337. package/src/config/bundled-skills/browser/TOOLS.json +234 -0
  338. package/src/config/bundled-skills/browser/tools/browser-click.ts +9 -0
  339. package/src/config/bundled-skills/browser/tools/browser-close.ts +9 -0
  340. package/src/config/bundled-skills/browser/tools/browser-extract.ts +9 -0
  341. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +9 -0
  342. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +9 -0
  343. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +9 -0
  344. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +9 -0
  345. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +9 -0
  346. package/src/config/bundled-skills/browser/tools/browser-type.ts +9 -0
  347. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +9 -0
  348. package/src/config/bundled-skills/claude-code/SKILL.md +50 -0
  349. package/src/config/bundled-skills/claude-code/TOOLS.json +40 -0
  350. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +9 -0
  351. package/src/config/bundled-skills/computer-use/SKILL.md +17 -0
  352. package/src/config/bundled-skills/computer-use/TOOLS.json +326 -0
  353. package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +9 -0
  354. package/src/config/bundled-skills/computer-use/tools/computer-use-done.ts +9 -0
  355. package/src/config/bundled-skills/computer-use/tools/computer-use-double-click.ts +9 -0
  356. package/src/config/bundled-skills/computer-use/tools/computer-use-drag.ts +9 -0
  357. package/src/config/bundled-skills/computer-use/tools/computer-use-key.ts +9 -0
  358. package/src/config/bundled-skills/computer-use/tools/computer-use-open-app.ts +9 -0
  359. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +9 -0
  360. package/src/config/bundled-skills/computer-use/tools/computer-use-respond.ts +9 -0
  361. package/src/config/bundled-skills/computer-use/tools/computer-use-right-click.ts +9 -0
  362. package/src/config/bundled-skills/computer-use/tools/computer-use-run-applescript.ts +9 -0
  363. package/src/config/bundled-skills/computer-use/tools/computer-use-scroll.ts +9 -0
  364. package/src/config/bundled-skills/computer-use/tools/computer-use-type-text.ts +9 -0
  365. package/src/config/bundled-skills/computer-use/tools/computer-use-wait.ts +9 -0
  366. package/src/config/bundled-skills/google-calendar/SKILL.md +51 -0
  367. package/src/config/bundled-skills/google-calendar/TOOLS.json +108 -0
  368. package/src/config/bundled-skills/google-calendar/calendar-client.ts +165 -0
  369. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +21 -0
  370. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +42 -0
  371. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +13 -0
  372. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +30 -0
  373. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +41 -0
  374. package/src/config/bundled-skills/google-calendar/tools/shared.ts +18 -0
  375. package/src/config/bundled-skills/google-calendar/types.ts +97 -0
  376. package/src/config/bundled-skills/image-studio/SKILL.md +32 -0
  377. package/src/config/bundled-skills/image-studio/TOOLS.json +42 -0
  378. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +137 -0
  379. package/src/config/bundled-skills/messaging/SKILL.md +126 -0
  380. package/src/config/bundled-skills/messaging/TOOLS.json +357 -0
  381. package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +23 -0
  382. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +23 -0
  383. package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +25 -0
  384. package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +26 -0
  385. package/src/config/bundled-skills/messaging/tools/gmail-label.ts +25 -0
  386. package/src/config/bundled-skills/messaging/tools/gmail-trash.ts +23 -0
  387. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +84 -0
  388. package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +18 -0
  389. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +124 -0
  390. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +16 -0
  391. package/src/config/bundled-skills/messaging/tools/messaging-draft.ts +49 -0
  392. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +21 -0
  393. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +25 -0
  394. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +28 -0
  395. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +29 -0
  396. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +22 -0
  397. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +27 -0
  398. package/src/config/bundled-skills/messaging/tools/shared.ts +71 -0
  399. package/src/config/bundled-skills/messaging/tools/slack-add-reaction.ts +25 -0
  400. package/src/config/bundled-skills/messaging/tools/slack-leave-channel.ts +23 -0
  401. package/src/config/bundled-skills/self-upgrade/SKILL.md +74 -0
  402. package/src/config/bundled-skills/start-the-day/SKILL.md +70 -0
  403. package/src/config/bundled-skills/start-the-day/icon.svg +13 -0
  404. package/src/config/bundled-skills/weather/SKILL.md +37 -0
  405. package/src/config/bundled-skills/weather/TOOLS.json +32 -0
  406. package/src/config/bundled-skills/weather/icon.svg +24 -0
  407. package/src/config/bundled-skills/weather/tools/get-weather.ts +9 -0
  408. package/src/config/computer-use-prompt.ts +97 -0
  409. package/src/config/defaults.ts +186 -0
  410. package/src/config/loader.ts +336 -0
  411. package/src/config/schema.ts +1004 -0
  412. package/src/config/skill-state.ts +95 -0
  413. package/src/config/skills.ts +972 -0
  414. package/src/config/system-prompt.ts +927 -0
  415. package/src/config/templates/BOOTSTRAP.md +70 -0
  416. package/src/config/templates/IDENTITY.md +18 -0
  417. package/src/config/templates/LOOKS.md +25 -0
  418. package/src/config/templates/SOUL.md +37 -0
  419. package/src/config/templates/USER.md +19 -0
  420. package/src/config/types.ts +32 -0
  421. package/src/config/vellum-skills/deploy-fullstack-vercel/SKILL.md +179 -0
  422. package/src/config/vellum-skills/document-writer/SKILL.md +195 -0
  423. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +194 -0
  424. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +147 -0
  425. package/src/config/vellum-skills/telegram-setup/SKILL.md +105 -0
  426. package/src/contacts/contact-store.ts +410 -0
  427. package/src/contacts/index.ts +11 -0
  428. package/src/contacts/types.ts +28 -0
  429. package/src/context/token-estimator.ts +108 -0
  430. package/src/context/tool-result-truncation.ts +128 -0
  431. package/src/context/window-manager.ts +531 -0
  432. package/src/daemon/assistant-attachments.ts +679 -0
  433. package/src/daemon/classifier.ts +108 -0
  434. package/src/daemon/computer-use-session.ts +900 -0
  435. package/src/daemon/connection-policy.ts +41 -0
  436. package/src/daemon/handlers/apps.ts +446 -0
  437. package/src/daemon/handlers/computer-use.ts +181 -0
  438. package/src/daemon/handlers/config.ts +434 -0
  439. package/src/daemon/handlers/diagnostics.ts +334 -0
  440. package/src/daemon/handlers/documents.ts +184 -0
  441. package/src/daemon/handlers/home-base.ts +73 -0
  442. package/src/daemon/handlers/index.ts +355 -0
  443. package/src/daemon/handlers/misc.ts +323 -0
  444. package/src/daemon/handlers/open-bundle-handler.ts +80 -0
  445. package/src/daemon/handlers/publish.ts +182 -0
  446. package/src/daemon/handlers/sessions.ts +486 -0
  447. package/src/daemon/handlers/shared.ts +533 -0
  448. package/src/daemon/handlers/skills.ts +487 -0
  449. package/src/daemon/handlers/subagents.ts +122 -0
  450. package/src/daemon/handlers/work-items.ts +176 -0
  451. package/src/daemon/handlers.ts +17 -0
  452. package/src/daemon/history-repair.ts +214 -0
  453. package/src/daemon/ipc-blob-store.ts +231 -0
  454. package/src/daemon/ipc-contract-inventory.json +407 -0
  455. package/src/daemon/ipc-contract-inventory.ts +126 -0
  456. package/src/daemon/ipc-contract.ts +2102 -0
  457. package/src/daemon/ipc-protocol.ts +70 -0
  458. package/src/daemon/ipc-validate.ts +171 -0
  459. package/src/daemon/lifecycle.ts +503 -0
  460. package/src/daemon/main.ts +15 -0
  461. package/src/daemon/media-visibility-policy.ts +57 -0
  462. package/src/daemon/ride-shotgun-handler.ts +244 -0
  463. package/src/daemon/server.ts +1085 -0
  464. package/src/daemon/session-attachments.ts +173 -0
  465. package/src/daemon/session-conflict-gate.ts +219 -0
  466. package/src/daemon/session-dynamic-profile.ts +63 -0
  467. package/src/daemon/session-error.ts +269 -0
  468. package/src/daemon/session-evictor.ts +196 -0
  469. package/src/daemon/session-history.ts +437 -0
  470. package/src/daemon/session-memory.ts +212 -0
  471. package/src/daemon/session-process.ts +264 -0
  472. package/src/daemon/session-queue-manager.ts +81 -0
  473. package/src/daemon/session-runtime-assembly.ts +395 -0
  474. package/src/daemon/session-skill-tools.ts +237 -0
  475. package/src/daemon/session-slash.ts +302 -0
  476. package/src/daemon/session-surfaces.ts +624 -0
  477. package/src/daemon/session-tool-setup.ts +286 -0
  478. package/src/daemon/session-usage.ts +74 -0
  479. package/src/daemon/session-workspace.ts +19 -0
  480. package/src/daemon/session.ts +1651 -0
  481. package/src/daemon/trace-emitter.ts +82 -0
  482. package/src/daemon/watch-handler.ts +274 -0
  483. package/src/doordash/client.ts +905 -0
  484. package/src/doordash/queries.ts +1312 -0
  485. package/src/doordash/query-extractor.ts +93 -0
  486. package/src/doordash/session.ts +82 -0
  487. package/src/email/provider.ts +117 -0
  488. package/src/email/providers/agentmail.ts +317 -0
  489. package/src/email/providers/index.ts +58 -0
  490. package/src/email/service.ts +303 -0
  491. package/src/email/types.ts +126 -0
  492. package/src/events/bus.ts +157 -0
  493. package/src/events/domain-events.ts +83 -0
  494. package/src/events/index.ts +18 -0
  495. package/src/events/tool-audit-listener.ts +80 -0
  496. package/src/events/tool-domain-event-publisher.ts +111 -0
  497. package/src/events/tool-metrics-listener.ts +159 -0
  498. package/src/events/tool-notification-listener.ts +17 -0
  499. package/src/events/tool-profiling-listener.ts +158 -0
  500. package/src/events/tool-trace-listener.ts +75 -0
  501. package/src/export/formatter.ts +96 -0
  502. package/src/followups/followup-store.ts +166 -0
  503. package/src/followups/index.ts +10 -0
  504. package/src/followups/types.ts +23 -0
  505. package/src/gallery/default-gallery.ts +795 -0
  506. package/src/gallery/gallery-manifest.ts +24 -0
  507. package/src/home-base/app-link-store.ts +82 -0
  508. package/src/home-base/bootstrap.ts +66 -0
  509. package/src/home-base/prebuilt/index.html +662 -0
  510. package/src/home-base/prebuilt/seed-metadata.json +21 -0
  511. package/src/home-base/prebuilt/seed.ts +101 -0
  512. package/src/home-base/prebuilt-home-base-updater.ts +30 -0
  513. package/src/hooks/cli.ts +163 -0
  514. package/src/hooks/config.ts +88 -0
  515. package/src/hooks/discovery.ts +110 -0
  516. package/src/hooks/manager.ts +128 -0
  517. package/src/hooks/runner.ts +123 -0
  518. package/src/hooks/templates.ts +52 -0
  519. package/src/hooks/types.ts +72 -0
  520. package/src/index.ts +1194 -0
  521. package/src/instrument.ts +60 -0
  522. package/src/logfire.ts +99 -0
  523. package/src/media/gemini-image-service.ts +136 -0
  524. package/src/memory/account-store.ts +108 -0
  525. package/src/memory/admin.ts +211 -0
  526. package/src/memory/app-store.ts +556 -0
  527. package/src/memory/attachments-store.ts +453 -0
  528. package/src/memory/channel-delivery-store.ts +368 -0
  529. package/src/memory/checkpoints.ts +52 -0
  530. package/src/memory/clarification-resolver.ts +297 -0
  531. package/src/memory/conflict-store.ts +342 -0
  532. package/src/memory/contradiction-checker.ts +329 -0
  533. package/src/memory/conversation-key-store.ts +127 -0
  534. package/src/memory/conversation-store.ts +469 -0
  535. package/src/memory/db.ts +1105 -0
  536. package/src/memory/embedding-backend.ts +229 -0
  537. package/src/memory/embedding-gemini.ts +52 -0
  538. package/src/memory/embedding-local.ts +75 -0
  539. package/src/memory/embedding-ollama.ts +55 -0
  540. package/src/memory/embedding-openai.ts +25 -0
  541. package/src/memory/entity-extractor.ts +471 -0
  542. package/src/memory/fingerprint.ts +20 -0
  543. package/src/memory/indexer.ts +156 -0
  544. package/src/memory/items-extractor.ts +460 -0
  545. package/src/memory/job-handlers/backfill.ts +139 -0
  546. package/src/memory/job-handlers/cleanup.ts +58 -0
  547. package/src/memory/job-handlers/conflict.ts +99 -0
  548. package/src/memory/job-handlers/embedding.ts +61 -0
  549. package/src/memory/job-handlers/extraction.ts +123 -0
  550. package/src/memory/job-handlers/index-maintenance.ts +54 -0
  551. package/src/memory/job-handlers/summarization.ts +286 -0
  552. package/src/memory/job-utils.ts +170 -0
  553. package/src/memory/jobs-store.ts +400 -0
  554. package/src/memory/jobs-worker.ts +274 -0
  555. package/src/memory/llm-request-log-store.ts +45 -0
  556. package/src/memory/llm-usage-store.ts +62 -0
  557. package/src/memory/message-content.ts +54 -0
  558. package/src/memory/profile-compiler.ts +160 -0
  559. package/src/memory/published-pages-store.ts +137 -0
  560. package/src/memory/qdrant-client.ts +366 -0
  561. package/src/memory/qdrant-manager.ts +242 -0
  562. package/src/memory/query-builder.ts +45 -0
  563. package/src/memory/retrieval-budget.ts +30 -0
  564. package/src/memory/retriever.ts +653 -0
  565. package/src/memory/runs-store.ts +211 -0
  566. package/src/memory/schema.ts +529 -0
  567. package/src/memory/search/entity.ts +298 -0
  568. package/src/memory/search/formatting.ts +207 -0
  569. package/src/memory/search/lexical.ts +227 -0
  570. package/src/memory/search/ranking.ts +401 -0
  571. package/src/memory/search/semantic.ts +121 -0
  572. package/src/memory/search/types.ts +137 -0
  573. package/src/memory/segmenter.ts +68 -0
  574. package/src/memory/shared-app-links-store.ts +138 -0
  575. package/src/memory/tool-usage-store.ts +62 -0
  576. package/src/messaging/activity-analyzer.ts +76 -0
  577. package/src/messaging/draft-store.ts +88 -0
  578. package/src/messaging/index.ts +3 -0
  579. package/src/messaging/provider-types.ts +80 -0
  580. package/src/messaging/provider.ts +43 -0
  581. package/src/messaging/providers/gmail/adapter.ts +193 -0
  582. package/src/messaging/providers/gmail/client.ts +204 -0
  583. package/src/messaging/providers/gmail/types.ts +90 -0
  584. package/src/messaging/providers/slack/adapter.ts +202 -0
  585. package/src/messaging/providers/slack/client.ts +198 -0
  586. package/src/messaging/providers/slack/types.ts +119 -0
  587. package/src/messaging/registry.ts +34 -0
  588. package/src/messaging/style-analyzer.ts +158 -0
  589. package/src/messaging/thread-summarizer.ts +310 -0
  590. package/src/messaging/triage-engine.ts +321 -0
  591. package/src/messaging/types.ts +55 -0
  592. package/src/permissions/checker.ts +636 -0
  593. package/src/permissions/defaults.ts +243 -0
  594. package/src/permissions/prompter.ts +102 -0
  595. package/src/permissions/secret-prompter.ts +114 -0
  596. package/src/permissions/trust-store.ts +584 -0
  597. package/src/permissions/types.ts +62 -0
  598. package/src/playbooks/index.ts +2 -0
  599. package/src/playbooks/playbook-compiler.ts +90 -0
  600. package/src/playbooks/types.ts +55 -0
  601. package/src/providers/anthropic/client.ts +751 -0
  602. package/src/providers/failover.ts +129 -0
  603. package/src/providers/fireworks/client.ts +20 -0
  604. package/src/providers/gemini/client.ts +285 -0
  605. package/src/providers/ollama/client.ts +30 -0
  606. package/src/providers/openai/client.ts +337 -0
  607. package/src/providers/ratelimit.ts +93 -0
  608. package/src/providers/registry.ts +138 -0
  609. package/src/providers/retry.ts +106 -0
  610. package/src/providers/stream-timeout.ts +38 -0
  611. package/src/providers/types.ts +109 -0
  612. package/src/runtime/assistant-event-hub.ts +120 -0
  613. package/src/runtime/assistant-event.ts +82 -0
  614. package/src/runtime/http-server.ts +478 -0
  615. package/src/runtime/http-types.ts +68 -0
  616. package/src/runtime/routes/app-routes.ts +174 -0
  617. package/src/runtime/routes/attachment-routes.ts +134 -0
  618. package/src/runtime/routes/channel-routes.ts +342 -0
  619. package/src/runtime/routes/conversation-routes.ts +349 -0
  620. package/src/runtime/routes/run-routes.ts +223 -0
  621. package/src/runtime/routes/secret-routes.ts +76 -0
  622. package/src/runtime/run-orchestrator.ts +206 -0
  623. package/src/schedule/schedule-store.ts +452 -0
  624. package/src/schedule/scheduler.ts +168 -0
  625. package/src/security/encrypted-store.ts +238 -0
  626. package/src/security/keychain.ts +252 -0
  627. package/src/security/oauth2.ts +241 -0
  628. package/src/security/redaction.ts +89 -0
  629. package/src/security/secret-allowlist.ts +118 -0
  630. package/src/security/secret-ingress.ts +57 -0
  631. package/src/security/secret-scanner.ts +543 -0
  632. package/src/security/secure-keys.ts +180 -0
  633. package/src/security/token-manager.ts +141 -0
  634. package/src/services/published-app-updater.ts +69 -0
  635. package/src/services/vercel-deploy.ts +73 -0
  636. package/src/skills/active-skill-tools.ts +81 -0
  637. package/src/skills/clawhub.ts +414 -0
  638. package/src/skills/include-graph.ts +146 -0
  639. package/src/skills/managed-store.ts +233 -0
  640. package/src/skills/path-classifier.ts +128 -0
  641. package/src/skills/slash-commands.ts +174 -0
  642. package/src/skills/tool-manifest.ts +165 -0
  643. package/src/skills/version-hash.ts +110 -0
  644. package/src/slack/slack-webhook.ts +61 -0
  645. package/src/subagent/index.ts +19 -0
  646. package/src/subagent/manager.ts +477 -0
  647. package/src/subagent/types.ts +69 -0
  648. package/src/swarm/backend-claude-code.ts +90 -0
  649. package/src/swarm/index.ts +44 -0
  650. package/src/swarm/limits.ts +37 -0
  651. package/src/swarm/orchestrator.ts +279 -0
  652. package/src/swarm/plan-validator.ts +151 -0
  653. package/src/swarm/router-planner.ts +100 -0
  654. package/src/swarm/router-prompts.ts +36 -0
  655. package/src/swarm/synthesizer.ts +62 -0
  656. package/src/swarm/types.ts +62 -0
  657. package/src/swarm/worker-backend.ts +121 -0
  658. package/src/swarm/worker-prompts.ts +78 -0
  659. package/src/swarm/worker-runner.ts +164 -0
  660. package/src/tasks/SPEC.md +133 -0
  661. package/src/tasks/candidate-store.ts +86 -0
  662. package/src/tasks/ephemeral-permissions.ts +41 -0
  663. package/src/tasks/task-compiler.ts +198 -0
  664. package/src/tasks/task-runner.ts +85 -0
  665. package/src/tasks/task-scheduler.ts +20 -0
  666. package/src/tasks/task-store.ts +127 -0
  667. package/src/tools/apps/definitions.ts +59 -0
  668. package/src/tools/apps/executors.ts +313 -0
  669. package/src/tools/apps/open-proxy.ts +43 -0
  670. package/src/tools/apps/registry.ts +16 -0
  671. package/src/tools/assets/materialize.ts +218 -0
  672. package/src/tools/assets/search.ts +396 -0
  673. package/src/tools/browser/__tests__/auth-cache.test.ts +219 -0
  674. package/src/tools/browser/__tests__/auth-detector.test.ts +362 -0
  675. package/src/tools/browser/__tests__/jit-auth.test.ts +189 -0
  676. package/src/tools/browser/auth-cache.ts +149 -0
  677. package/src/tools/browser/auth-detector.ts +347 -0
  678. package/src/tools/browser/browser-execution.ts +979 -0
  679. package/src/tools/browser/browser-handoff.ts +79 -0
  680. package/src/tools/browser/browser-manager.ts +715 -0
  681. package/src/tools/browser/browser-screencast.ts +217 -0
  682. package/src/tools/browser/headless-browser.ts +450 -0
  683. package/src/tools/browser/jit-auth.ts +51 -0
  684. package/src/tools/browser/network-recorder.ts +348 -0
  685. package/src/tools/browser/network-recording-types.ts +49 -0
  686. package/src/tools/browser/recording-store.ts +49 -0
  687. package/src/tools/browser/runtime-check.ts +43 -0
  688. package/src/tools/claude-code/claude-code.ts +232 -0
  689. package/src/tools/computer-use/definitions.ts +443 -0
  690. package/src/tools/computer-use/registry.ts +22 -0
  691. package/src/tools/computer-use/request-computer-control.ts +53 -0
  692. package/src/tools/computer-use/skill-proxy-bridge.ts +28 -0
  693. package/src/tools/contacts/contact-merge.ts +87 -0
  694. package/src/tools/contacts/contact-search.ts +102 -0
  695. package/src/tools/contacts/contact-upsert.ts +137 -0
  696. package/src/tools/contacts/index.ts +4 -0
  697. package/src/tools/credentials/account-registry.ts +127 -0
  698. package/src/tools/credentials/broker-types.ts +107 -0
  699. package/src/tools/credentials/broker.ts +372 -0
  700. package/src/tools/credentials/domain-policy.ts +51 -0
  701. package/src/tools/credentials/host-pattern-match.ts +60 -0
  702. package/src/tools/credentials/metadata-store.ts +335 -0
  703. package/src/tools/credentials/policy-types.ts +52 -0
  704. package/src/tools/credentials/policy-validate.ts +80 -0
  705. package/src/tools/credentials/resolve.ts +122 -0
  706. package/src/tools/credentials/selection.ts +159 -0
  707. package/src/tools/credentials/tool-policy.ts +25 -0
  708. package/src/tools/credentials/vault.ts +641 -0
  709. package/src/tools/document/document-tool.ts +165 -0
  710. package/src/tools/document/editor-template.ts +237 -0
  711. package/src/tools/document/index.ts +5 -0
  712. package/src/tools/executor.ts +825 -0
  713. package/src/tools/filesystem/edit.ts +127 -0
  714. package/src/tools/filesystem/fuzzy-match.ts +202 -0
  715. package/src/tools/filesystem/read.ts +71 -0
  716. package/src/tools/filesystem/view-image.ts +199 -0
  717. package/src/tools/filesystem/write.ts +79 -0
  718. package/src/tools/followups/followup_create.ts +118 -0
  719. package/src/tools/followups/followup_list.ts +100 -0
  720. package/src/tools/followups/followup_resolve.ts +91 -0
  721. package/src/tools/followups/index.ts +3 -0
  722. package/src/tools/host-filesystem/edit.ts +125 -0
  723. package/src/tools/host-filesystem/read.ts +80 -0
  724. package/src/tools/host-filesystem/write.ts +76 -0
  725. package/src/tools/host-terminal/cli-discover.ts +179 -0
  726. package/src/tools/host-terminal/host-shell.ts +181 -0
  727. package/src/tools/memory/definitions.ts +69 -0
  728. package/src/tools/memory/handlers.ts +245 -0
  729. package/src/tools/memory/register.ts +66 -0
  730. package/src/tools/network/domain-normalize.ts +85 -0
  731. package/src/tools/network/script-proxy/certs.ts +237 -0
  732. package/src/tools/network/script-proxy/connect-tunnel.ts +82 -0
  733. package/src/tools/network/script-proxy/http-forwarder.ts +151 -0
  734. package/src/tools/network/script-proxy/index.ts +28 -0
  735. package/src/tools/network/script-proxy/logging.ts +196 -0
  736. package/src/tools/network/script-proxy/mitm-handler.ts +269 -0
  737. package/src/tools/network/script-proxy/policy.ts +152 -0
  738. package/src/tools/network/script-proxy/router.ts +60 -0
  739. package/src/tools/network/script-proxy/server.ts +136 -0
  740. package/src/tools/network/script-proxy/session-manager.ts +534 -0
  741. package/src/tools/network/script-proxy/types.ts +125 -0
  742. package/src/tools/network/url-safety.ts +227 -0
  743. package/src/tools/network/web-fetch.ts +701 -0
  744. package/src/tools/network/web-search.ts +319 -0
  745. package/src/tools/playbooks/index.ts +5 -0
  746. package/src/tools/playbooks/playbook-create.ts +140 -0
  747. package/src/tools/playbooks/playbook-delete.ts +76 -0
  748. package/src/tools/playbooks/playbook-list.ts +101 -0
  749. package/src/tools/playbooks/playbook-update.ts +159 -0
  750. package/src/tools/registry.ts +297 -0
  751. package/src/tools/reminder/reminder-store.ts +148 -0
  752. package/src/tools/reminder/reminder.ts +153 -0
  753. package/src/tools/schedule/create.ts +86 -0
  754. package/src/tools/schedule/delete.ts +54 -0
  755. package/src/tools/schedule/list.ts +88 -0
  756. package/src/tools/schedule/update.ts +97 -0
  757. package/src/tools/shared/filesystem/edit-engine.ts +56 -0
  758. package/src/tools/shared/filesystem/errors.ts +85 -0
  759. package/src/tools/shared/filesystem/file-ops-service.ts +215 -0
  760. package/src/tools/shared/filesystem/format-diff.ts +35 -0
  761. package/src/tools/shared/filesystem/path-policy.ts +125 -0
  762. package/src/tools/shared/filesystem/size-guard.ts +41 -0
  763. package/src/tools/shared/filesystem/types.ts +80 -0
  764. package/src/tools/shared/shell-output.ts +52 -0
  765. package/src/tools/skills/delete-managed.ts +60 -0
  766. package/src/tools/skills/load.ts +139 -0
  767. package/src/tools/skills/sandbox-runner.ts +279 -0
  768. package/src/tools/skills/scaffold-managed.ts +150 -0
  769. package/src/tools/skills/script-contract.ts +6 -0
  770. package/src/tools/skills/skill-script-runner.ts +86 -0
  771. package/src/tools/skills/skill-tool-factory.ts +64 -0
  772. package/src/tools/skills/vellum-catalog.ts +217 -0
  773. package/src/tools/subagent/abort.ts +62 -0
  774. package/src/tools/subagent/index.ts +5 -0
  775. package/src/tools/subagent/message.ts +72 -0
  776. package/src/tools/subagent/read.ts +98 -0
  777. package/src/tools/subagent/spawn.ts +85 -0
  778. package/src/tools/subagent/status.ts +74 -0
  779. package/src/tools/swarm/delegate.ts +182 -0
  780. package/src/tools/system/request-permission.ts +98 -0
  781. package/src/tools/tasks/index.ts +25 -0
  782. package/src/tools/tasks/task-delete.ts +69 -0
  783. package/src/tools/tasks/task-list.ts +65 -0
  784. package/src/tools/tasks/task-run.ts +125 -0
  785. package/src/tools/tasks/task-save.ts +79 -0
  786. package/src/tools/tasks/work-item-enqueue.ts +176 -0
  787. package/src/tools/tasks/work-item-list.ts +86 -0
  788. package/src/tools/terminal/backends/docker.ts +372 -0
  789. package/src/tools/terminal/backends/native.ts +188 -0
  790. package/src/tools/terminal/backends/types.ts +26 -0
  791. package/src/tools/terminal/evaluate-typescript.ts +275 -0
  792. package/src/tools/terminal/parser.ts +393 -0
  793. package/src/tools/terminal/safe-env.ts +37 -0
  794. package/src/tools/terminal/sandbox-diagnostics.ts +149 -0
  795. package/src/tools/terminal/sandbox.ts +44 -0
  796. package/src/tools/terminal/shell.ts +257 -0
  797. package/src/tools/tool-manifest.ts +250 -0
  798. package/src/tools/types.ts +177 -0
  799. package/src/tools/ui-surface/definitions.ts +232 -0
  800. package/src/tools/ui-surface/registry.ts +14 -0
  801. package/src/tools/watch/screen-watch.ts +128 -0
  802. package/src/tools/watch/watch-state.ts +119 -0
  803. package/src/tools/watcher/create.ts +110 -0
  804. package/src/tools/watcher/delete.ts +53 -0
  805. package/src/tools/watcher/digest.ts +84 -0
  806. package/src/tools/watcher/list.ts +90 -0
  807. package/src/tools/watcher/update.ts +102 -0
  808. package/src/tools/weather/service.ts +551 -0
  809. package/src/usage/actors.ts +24 -0
  810. package/src/usage/types.ts +38 -0
  811. package/src/util/clipboard.ts +33 -0
  812. package/src/util/content-id.ts +16 -0
  813. package/src/util/diff.ts +181 -0
  814. package/src/util/errors.ts +129 -0
  815. package/src/util/logger.ts +243 -0
  816. package/src/util/platform.ts +607 -0
  817. package/src/util/pricing.ts +150 -0
  818. package/src/util/spinner.ts +51 -0
  819. package/src/util/time.ts +16 -0
  820. package/src/util/xml.ts +4 -0
  821. package/src/version.ts +3 -0
  822. package/src/watcher/constants.ts +11 -0
  823. package/src/watcher/engine.ts +199 -0
  824. package/src/watcher/provider-registry.ts +15 -0
  825. package/src/watcher/provider-types.ts +48 -0
  826. package/src/watcher/providers/gmail.ts +198 -0
  827. package/src/watcher/providers/google-calendar.ts +228 -0
  828. package/src/watcher/providers/slack.ts +128 -0
  829. package/src/watcher/watcher-store.ts +418 -0
  830. package/src/work-items/work-item-store.ts +91 -0
  831. package/src/workspace/git-service.ts +620 -0
  832. package/src/workspace/heartbeat-service.ts +288 -0
  833. package/src/workspace/top-level-renderer.ts +19 -0
  834. package/src/workspace/top-level-scanner.ts +41 -0
  835. package/src/workspace/turn-commit.ts +122 -0
  836. package/tsconfig.json +21 -0
  837. package/LICENSE +0 -674
  838. package/dist/cli.js +0 -569
@@ -0,0 +1,1651 @@
1
+ import { v4 as uuid } from 'uuid';
2
+ import type { Message, ContentBlock, ImageContent } from '../providers/types.js';
3
+ import type { ServerMessage, UsageStats, UserMessageAttachment, SurfaceType, SurfaceData, DynamicPageSurfaceData } from './ipc-protocol.js';
4
+ import { repairHistory, deepRepairHistory } from './history-repair.js';
5
+ import { AgentLoop } from '../agent/loop.js';
6
+ import type { CheckpointDecision } from '../agent/loop.js';
7
+ import type { Provider } from '../providers/types.js';
8
+ import { createUserMessage, createAssistantMessage } from '../agent/message-types.js';
9
+ import * as conversationStore from '../memory/conversation-store.js';
10
+ import { PermissionPrompter } from '../permissions/prompter.js';
11
+ import { SecretPrompter } from '../permissions/secret-prompter.js';
12
+ import { ToolExecutor } from '../tools/executor.js';
13
+ import type { UserDecision } from '../permissions/types.js';
14
+ import { getConfig } from '../config/loader.js';
15
+ import { getLogger } from '../util/logger.js';
16
+ import { TraceEmitter } from './trace-emitter.js';
17
+ import { classifySessionError, isUserCancellation, isContextTooLarge, buildSessionErrorMessage } from './session-error.js';
18
+ import { EventBus } from '../events/bus.js';
19
+ import type { AssistantDomainEvents } from '../events/domain-events.js';
20
+ import {
21
+ registerWatchStartNotifier,
22
+ unregisterWatchStartNotifier,
23
+ registerWatchCommentaryNotifier,
24
+ unregisterWatchCommentaryNotifier,
25
+ registerWatchCompletionNotifier,
26
+ unregisterWatchCompletionNotifier,
27
+ pruneWatchSessions,
28
+ } from '../tools/watch/watch-state.js';
29
+ import type { WatchSession } from '../tools/watch/watch-state.js';
30
+ import { lastCommentaryBySession, lastSummaryBySession } from './watch-handler.js';
31
+ import { createToolDomainEventPublisher } from '../events/tool-domain-event-publisher.js';
32
+ import { registerToolMetricsLoggingListener } from '../events/tool-metrics-listener.js';
33
+ import { registerToolNotificationListener } from '../events/tool-notification-listener.js';
34
+ import { registerToolTraceListener } from '../events/tool-trace-listener.js';
35
+ import { createToolAuditListener } from '../events/tool-audit-listener.js';
36
+ import { ToolProfiler, registerToolProfilingListener } from '../events/tool-profiling-listener.js';
37
+ import {
38
+ ContextWindowManager,
39
+ createContextSummaryMessage,
40
+ getSummaryFromContextMessage,
41
+ } from '../context/window-manager.js';
42
+ import { getHookManager } from '../hooks/manager.js';
43
+ import {
44
+ stripMemoryRecallMessages,
45
+ } from '../memory/retriever.js';
46
+ import { getApp, listAppFiles } from '../memory/app-store.js';
47
+ import { ConflictGate } from './session-conflict-gate.js';
48
+ import { stripDynamicProfileMessages } from './session-dynamic-profile.js';
49
+ import { MessageQueue } from './session-queue-manager.js';
50
+ import type { QueueDrainReason } from './session-queue-manager.js';
51
+ import {
52
+ applyRuntimeInjections,
53
+ stripActiveSurfaceContext,
54
+ stripWorkspaceTopLevelContext,
55
+ stripChannelCapabilityContext,
56
+ } from './session-runtime-assembly.js';
57
+ import type {
58
+ ActiveSurfaceContext,
59
+ ChannelCapabilities,
60
+ } from './session-runtime-assembly.js';
61
+ import {
62
+ cleanAssistantContent,
63
+ drainDirectiveDisplayBuffer,
64
+ type DirectiveRequest,
65
+ type AssistantAttachmentDraft,
66
+ } from './assistant-attachments.js';
67
+ import {
68
+ handleSurfaceAction as handleSurfaceActionImpl,
69
+ handleSurfaceUndo as handleSurfaceUndoImpl,
70
+ } from './session-surfaces.js';
71
+ import { prepareMemoryContext } from './session-memory.js';
72
+ import {
73
+ approveHostAttachmentRead,
74
+ formatAttachmentWarnings,
75
+ resolveAssistantAttachments,
76
+ } from './session-attachments.js';
77
+ import {
78
+ consolidateAssistantMessages,
79
+ undo as undoImpl,
80
+ regenerate as regenerateImpl,
81
+ type HistorySessionContext,
82
+ } from './session-history.js';
83
+ import { recordUsage } from './session-usage.js';
84
+ import { recordRequestLog } from '../memory/llm-request-log-store.js';
85
+ import { isProviderOrderingError } from './session-slash.js';
86
+ import { refreshWorkspaceTopLevelContextIfNeeded as refreshWorkspaceImpl } from './session-workspace.js';
87
+ import type { UsageActor } from '../usage/actors.js';
88
+ import {
89
+ drainQueue as drainQueueImpl,
90
+ processMessage as processMessageImpl,
91
+ type ProcessSessionContext,
92
+ } from './session-process.js';
93
+ import {
94
+ buildToolDefinitions,
95
+ createToolExecutor,
96
+ type ToolSetupContext,
97
+ } from './session-tool-setup.js';
98
+ import { unregisterSessionSender } from '../tools/browser/browser-screencast.js';
99
+ import { projectSkillTools, resetSkillToolProjection } from './session-skill-tools.js';
100
+ import { commitTurnChanges } from '../workspace/turn-commit.js';
101
+ import { getWorkspaceGitService } from '../workspace/git-service.js';
102
+
103
+ export interface SessionMemoryPolicy {
104
+ scopeId: string;
105
+ includeDefaultFallback: boolean;
106
+ strictSideEffects: boolean;
107
+ }
108
+
109
+ export const DEFAULT_MEMORY_POLICY: Readonly<SessionMemoryPolicy> = Object.freeze({
110
+ scopeId: 'default',
111
+ includeDefaultFallback: false,
112
+ strictSideEffects: false,
113
+ });
114
+
115
+ const log = getLogger('session');
116
+ const RETRY_KEEP_LATEST_MEDIA_BLOCKS = 3;
117
+ const MAX_MEDIA_STUB_TEXT = 2_000;
118
+
119
+ export { MAX_QUEUE_DEPTH, type QueueDrainReason, type QueuePolicy } from './session-queue-manager.js';
120
+ export { findLastUndoableUserMessageIndex } from './session-history.js';
121
+
122
+ export class Session {
123
+ public readonly conversationId: string;
124
+ private provider: Provider;
125
+ /** @internal — exposed for session-history.ts module functions. */
126
+ messages: Message[] = [];
127
+ private agentLoop: AgentLoop;
128
+ /** @internal — exposed for session-history.ts module functions. */
129
+ processing = false;
130
+ private stale = false;
131
+ /** @internal — exposed for session-history.ts module functions. */
132
+ abortController: AbortController | null = null;
133
+ private prompter: PermissionPrompter;
134
+ private secretPrompter: SecretPrompter;
135
+ private executor: ToolExecutor;
136
+ private profiler: ToolProfiler;
137
+ /** @internal — exposed for session-surfaces.ts module functions. */
138
+ sendToClient: (msg: ServerMessage) => void;
139
+ /** Broadcast a message to all connected sockets (not just this session's client). */
140
+ private broadcastToAllClients?: (msg: ServerMessage) => void;
141
+ private eventBus = new EventBus<AssistantDomainEvents>();
142
+ /** @internal — exposed for session-workspace.ts module functions. */
143
+ workingDir: string;
144
+ /** @internal — exposed for session-tool-setup.ts module functions. */
145
+ sandboxOverride?: boolean;
146
+ /** @internal — per-turn allowed tool set, read by the tool executor closure. */
147
+ allowedToolNames?: Set<string>;
148
+ /** @internal — request-scoped skill IDs preactivated via slash resolution. */
149
+ preactivatedSkillIds?: string[];
150
+ /** Core tool names (computed once in constructor), always allowed regardless of skill state. */
151
+ private coreToolNames: Set<string>;
152
+ /** Per-session tracking of previously active skill IDs and their version hashes for projection diffing. */
153
+ private readonly skillProjectionState = new Map<string, string>();
154
+ /** @internal — exposed for session-usage.ts module functions. */
155
+ usageStats: UsageStats = { inputTokens: 0, outputTokens: 0, estimatedCost: 0 };
156
+ private readonly systemPrompt: string;
157
+ private contextWindowManager: ContextWindowManager;
158
+ private contextCompactedMessageCount = 0;
159
+ private contextCompactedAt: number | null = null;
160
+ /** @internal — exposed for session-history.ts module functions. */
161
+ currentRequestId?: string;
162
+ /** @internal — exposed for session-usage.ts module functions. */
163
+ assistantId: string | null = null;
164
+ private conflictGate = new ConflictGate();
165
+ /** @internal — exposed for session-tool-setup.ts to propagate into ToolContext. */
166
+ hasNoClient = false;
167
+ /** @internal — exposed for session-process.ts module functions. */
168
+ readonly queue = new MessageQueue();
169
+ /** @internal — exposed for session-process.ts module functions. */
170
+ currentActiveSurfaceId?: string;
171
+ /** @internal — exposed for session-process.ts module functions. */
172
+ currentPage?: string;
173
+ private channelCapabilities?: ChannelCapabilities;
174
+ /** @internal — exposed for session-surfaces.ts module functions. */
175
+ pendingSurfaceActions = new Map<string, {
176
+ surfaceType: SurfaceType;
177
+ }>();
178
+ /** @internal */ lastSurfaceAction = new Map<string, { actionId: string; data?: Record<string, unknown> }>();
179
+ /** @internal */ surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>();
180
+ /** @internal Per-surface undo stack: stores previous HTML strings for workspace refinement undo. */
181
+ surfaceUndoStacks = new Map<string, string[]>();
182
+ /** @internal Surfaces created during the current agent loop turn, to be persisted with the message. */
183
+ currentTurnSurfaces: Array<{ surfaceId: string; surfaceType: SurfaceType; title?: string; data: SurfaceData; actions?: Array<{ id: string; label: string; style?: string }>; display?: string }> = [];
184
+ /** @internal */ onEscalateToComputerUse?: (task: string, sourceSessionId: string) => boolean;
185
+ /** @internal — exposed for session-workspace.ts module functions. */
186
+ workspaceTopLevelContext: string | null = null;
187
+ /** @internal — exposed for session-workspace.ts module functions. */
188
+ workspaceTopLevelDirty = true;
189
+ public readonly traceEmitter: TraceEmitter;
190
+ public memoryPolicy: SessionMemoryPolicy;
191
+ /** Monotonically increasing turn counter for turn-boundary commits. */
192
+ private turnCount = 0;
193
+
194
+ /** Resolved assistant attachment drafts from the most recent exchange. */
195
+ public lastAssistantAttachments: AssistantAttachmentDraft[] = [];
196
+ /** Warnings from directive parsing/resolution for the most recent exchange. */
197
+ public lastAttachmentWarnings: string[] = [];
198
+
199
+ constructor(
200
+ conversationId: string,
201
+ provider: Provider,
202
+ systemPrompt: string,
203
+ maxTokens: number,
204
+ sendToClient: (msg: ServerMessage) => void,
205
+ workingDir: string,
206
+ broadcastToAllClients?: (msg: ServerMessage) => void,
207
+ memoryPolicy?: SessionMemoryPolicy,
208
+ ) {
209
+ this.conversationId = conversationId;
210
+ this.systemPrompt = systemPrompt;
211
+ this.provider = provider;
212
+ this.workingDir = workingDir;
213
+ this.sendToClient = sendToClient;
214
+ this.broadcastToAllClients = broadcastToAllClients;
215
+ this.memoryPolicy = memoryPolicy ? { ...memoryPolicy } : { ...DEFAULT_MEMORY_POLICY };
216
+ this.traceEmitter = new TraceEmitter(conversationId, sendToClient);
217
+ this.prompter = new PermissionPrompter(sendToClient);
218
+ this.secretPrompter = new SecretPrompter(sendToClient);
219
+
220
+ registerWatchStartNotifier(conversationId, (session: WatchSession) => {
221
+ this.sendToClient({
222
+ type: 'watch_started',
223
+ sessionId: conversationId,
224
+ watchId: session.watchId,
225
+ durationSeconds: session.durationSeconds,
226
+ intervalSeconds: session.intervalSeconds,
227
+ });
228
+ });
229
+
230
+ registerWatchCommentaryNotifier(conversationId, (_session: WatchSession) => {
231
+ const commentary = lastCommentaryBySession.get(conversationId);
232
+ if (commentary) {
233
+ lastCommentaryBySession.delete(conversationId);
234
+ this.sendToClient({
235
+ type: 'assistant_text_delta',
236
+ text: commentary,
237
+ sessionId: conversationId,
238
+ });
239
+ this.sendToClient({
240
+ type: 'message_complete',
241
+ sessionId: conversationId,
242
+ });
243
+ }
244
+ });
245
+
246
+ registerWatchCompletionNotifier(conversationId, (_session: WatchSession) => {
247
+ const summary = lastSummaryBySession.get(conversationId);
248
+ if (summary) {
249
+ lastSummaryBySession.delete(conversationId);
250
+ this.sendToClient({
251
+ type: 'assistant_text_delta',
252
+ text: summary,
253
+ sessionId: conversationId,
254
+ });
255
+ this.sendToClient({
256
+ type: 'message_complete',
257
+ sessionId: conversationId,
258
+ });
259
+ }
260
+ });
261
+
262
+ this.executor = new ToolExecutor(this.prompter);
263
+ this.profiler = new ToolProfiler();
264
+ registerToolMetricsLoggingListener(this.eventBus);
265
+ registerToolNotificationListener(this.eventBus, (msg) => this.sendToClient(msg));
266
+ registerToolTraceListener(this.eventBus, this.traceEmitter);
267
+ registerToolProfilingListener(this.eventBus, this.profiler);
268
+ const auditToolLifecycleEvent = createToolAuditListener();
269
+ const publishToolDomainEvent = createToolDomainEventPublisher(this.eventBus);
270
+ const handleToolLifecycleEvent = (event: import('../tools/types.js').ToolLifecycleEvent) => {
271
+ auditToolLifecycleEvent(event);
272
+ return publishToolDomainEvent(event);
273
+ };
274
+
275
+ const toolDefs = buildToolDefinitions();
276
+ this.coreToolNames = new Set(toolDefs.map((d) => d.name));
277
+ const toolExecutor = createToolExecutor(
278
+ this.executor,
279
+ this.prompter,
280
+ this.secretPrompter,
281
+ this as ToolSetupContext,
282
+ handleToolLifecycleEvent,
283
+ broadcastToAllClients,
284
+ );
285
+
286
+ const config = getConfig();
287
+ // Build a resolveTools callback that merges base tool definitions with
288
+ // dynamically projected skill tools on each agent turn. Also updates
289
+ // allowedToolNames so newly-activated skill tools aren't blocked by
290
+ // the executor's stale gate.
291
+ const resolveTools = toolDefs.length > 0
292
+ ? (history: Message[]) => {
293
+ const projection = projectSkillTools(history, {
294
+ preactivatedSkillIds: this.preactivatedSkillIds,
295
+ previouslyActiveSkillIds: this.skillProjectionState,
296
+ });
297
+ const turnAllowed = new Set(this.coreToolNames);
298
+ for (const name of projection.allowedToolNames) {
299
+ turnAllowed.add(name);
300
+ }
301
+ this.allowedToolNames = turnAllowed;
302
+ return [...toolDefs, ...projection.toolDefinitions];
303
+ }
304
+ : undefined;
305
+
306
+ this.agentLoop = new AgentLoop(
307
+ provider,
308
+ systemPrompt,
309
+ { maxTokens, maxInputTokens: config.contextWindow.maxInputTokens, thinking: config.thinking },
310
+ toolDefs.length > 0 ? toolDefs : undefined,
311
+ toolDefs.length > 0 ? toolExecutor : undefined,
312
+ resolveTools,
313
+ );
314
+ this.contextWindowManager = new ContextWindowManager(
315
+ provider,
316
+ systemPrompt,
317
+ config.contextWindow,
318
+ );
319
+
320
+ void getHookManager().trigger('session-start', {
321
+ sessionId: this.conversationId,
322
+ workingDir: this.workingDir,
323
+ });
324
+ }
325
+
326
+ async loadFromDb(): Promise<void> {
327
+ const dbMessages = conversationStore.getMessages(this.conversationId);
328
+
329
+ const conv = conversationStore.getConversation(this.conversationId);
330
+ const contextSummary = conv?.contextSummary?.trim() || null;
331
+ this.contextCompactedMessageCount = Math.max(
332
+ 0,
333
+ Math.min(conv?.contextCompactedMessageCount ?? 0, dbMessages.length),
334
+ );
335
+ this.contextCompactedAt = conv?.contextCompactedAt ?? null;
336
+
337
+ const parsedMessages: Message[] = dbMessages
338
+ .slice(this.contextCompactedMessageCount)
339
+ .map((m) => {
340
+ const role = m.role as 'user' | 'assistant';
341
+ let content: ContentBlock[];
342
+ try {
343
+ const parsed = JSON.parse(m.content);
344
+ content = Array.isArray(parsed) ? parsed : [{ type: 'text', text: m.content }];
345
+ } catch {
346
+ log.warn({ conversationId: this.conversationId, messageId: m.id }, 'Invalid JSON in persisted message content, replacing with safe text block');
347
+ content = [{ type: 'text', text: m.content }];
348
+ }
349
+ return { role, content };
350
+ });
351
+
352
+ const { messages: repairedMessages, stats } = repairHistory(parsedMessages);
353
+ if (stats.assistantToolResultsMigrated > 0 || stats.missingToolResultsInserted > 0 || stats.orphanToolResultsDowngraded > 0 || stats.consecutiveSameRoleMerged > 0) {
354
+ log.warn({ conversationId: this.conversationId, phase: 'load', ...stats }, 'Repaired persisted history');
355
+ }
356
+ this.messages = repairedMessages;
357
+
358
+ if (contextSummary) {
359
+ this.messages.unshift(createContextSummaryMessage(contextSummary));
360
+ }
361
+
362
+ if (conv) {
363
+ this.usageStats = {
364
+ inputTokens: conv.totalInputTokens,
365
+ outputTokens: conv.totalOutputTokens,
366
+ estimatedCost: conv.totalEstimatedCost,
367
+ };
368
+ }
369
+
370
+ log.info({ conversationId: this.conversationId, count: this.messages.length }, 'Loaded messages from DB');
371
+ }
372
+
373
+ updateClient(sendToClient: (msg: ServerMessage) => void, hasNoClient = false): void {
374
+ this.sendToClient = sendToClient;
375
+ this.hasNoClient = hasNoClient;
376
+ this.prompter.updateSender(sendToClient);
377
+ this.secretPrompter.updateSender(sendToClient);
378
+ this.traceEmitter.updateSender(sendToClient);
379
+ }
380
+
381
+ setSandboxOverride(enabled: boolean | undefined): void {
382
+ this.sandboxOverride = enabled;
383
+ }
384
+
385
+ /**
386
+ * Set a callback for when a text_qa session escalates to computer use
387
+ * via the `computer_use_request_control` tool.
388
+ */
389
+ setEscalationHandler(handler: (task: string, sourceSessionId: string) => boolean): void {
390
+ this.onEscalateToComputerUse = handler;
391
+ }
392
+
393
+ hasEscalationHandler(): boolean {
394
+ return this.onEscalateToComputerUse !== undefined;
395
+ }
396
+
397
+ /**
398
+ * Redirect the user to the secure credential prompt after an ingress block.
399
+ * If the user enters a value, it is stored in the vault (or injected as
400
+ * transient) so the credential is available for later tool use.
401
+ *
402
+ * @param onComplete Called after the prompt resolves (success, cancel, or
403
+ * timeout) so the caller can clean up ephemeral resources like placeholder
404
+ * conversations.
405
+ */
406
+ redirectToSecurePrompt(detectedTypes: string[], onComplete?: () => void): void {
407
+ const service = 'detected';
408
+ const field = detectedTypes.join(',');
409
+ this.secretPrompter.prompt(
410
+ service, field,
411
+ 'Secure Credential Entry',
412
+ 'Your message contained a secret. Please enter it here instead — it will be stored securely and never sent to the AI.',
413
+ undefined, this.conversationId,
414
+ ).then(async (result) => {
415
+ if (!result.value) return; // user cancelled or timed out
416
+
417
+ const { setSecureKey } = await import('../security/secure-keys.js');
418
+ const { upsertCredentialMetadata } = await import('../tools/credentials/metadata-store.js');
419
+
420
+ if (result.delivery === 'transient_send') {
421
+ const { credentialBroker } = await import('../tools/credentials/broker.js');
422
+ credentialBroker.injectTransient(service, field, result.value);
423
+ try { upsertCredentialMetadata(service, field, {}); } catch {}
424
+ log.info({ service, field, delivery: 'transient_send' }, 'Ingress redirect: transient credential injected');
425
+ } else {
426
+ const key = `credential:${service}:${field}`;
427
+ const stored = setSecureKey(key, result.value);
428
+ if (stored) {
429
+ try { upsertCredentialMetadata(service, field, {}); } catch {}
430
+ log.info({ service, field }, 'Ingress redirect: credential stored');
431
+ } else {
432
+ log.warn({ service, field }, 'Ingress redirect: secure storage write failed');
433
+ }
434
+ }
435
+ }).catch(() => { /* prompt timeout or cancel is fine */ }).finally(() => {
436
+ onComplete?.();
437
+ });
438
+ }
439
+
440
+ isProcessing(): boolean {
441
+ return this.processing;
442
+ }
443
+
444
+ markStale(): void {
445
+ this.stale = true;
446
+ }
447
+
448
+ isStale(): boolean {
449
+ return this.stale;
450
+ }
451
+
452
+ abort(): void {
453
+ if (this.processing) {
454
+ log.info({ conversationId: this.conversationId }, 'Aborting in-flight processing');
455
+ this.abortController?.abort();
456
+ this.prompter.dispose();
457
+ this.secretPrompter.dispose();
458
+ this.pendingSurfaceActions.clear();
459
+ this.surfaceState.clear();
460
+ unregisterWatchStartNotifier(this.conversationId);
461
+ unregisterWatchCommentaryNotifier(this.conversationId);
462
+ unregisterWatchCompletionNotifier(this.conversationId);
463
+ pruneWatchSessions(this.conversationId);
464
+
465
+ // Clear queued messages and notify each caller with a session-scoped
466
+ // cancel event so other sessions do not receive cross-thread errors.
467
+ for (const queued of this.queue) {
468
+ queued.onEvent({ type: 'generation_cancelled', sessionId: this.conversationId });
469
+ }
470
+ this.queue.clear();
471
+ }
472
+ }
473
+
474
+ /** Abort and permanently tear down this session. Call when removing from the sessions map. */
475
+ dispose(): void {
476
+ void getHookManager().trigger('session-end', {
477
+ sessionId: this.conversationId,
478
+ });
479
+ this.abort();
480
+ unregisterSessionSender(this.conversationId);
481
+ resetSkillToolProjection(this.skillProjectionState);
482
+ this.eventBus.dispose();
483
+
484
+ // Release heavy in-memory data so GC can reclaim it even if stale
485
+ // closure references (e.g. from buildEventHandler / onCheckpoint)
486
+ // keep this Session object reachable.
487
+ this.messages = [];
488
+ this.profiler.clear();
489
+ this.surfaceUndoStacks.clear();
490
+ this.currentTurnSurfaces = [];
491
+ this.pendingSurfaceActions.clear();
492
+ this.surfaceState.clear();
493
+ this.lastSurfaceAction.clear();
494
+ this.workspaceTopLevelContext = null;
495
+ }
496
+
497
+ /**
498
+ * Enqueue a message if the session is busy, or indicate it should be
499
+ * processed immediately. Returns `{ queued: true }` if the message was
500
+ * added to the queue, `{ queued: false, rejected: true }` if the queue
501
+ * is full, or `{ queued: false }` if the caller should invoke
502
+ * `processMessage` directly.
503
+ */
504
+ enqueueMessage(
505
+ content: string,
506
+ attachments: UserMessageAttachment[],
507
+ onEvent: (msg: ServerMessage) => void,
508
+ requestId: string,
509
+ activeSurfaceId?: string,
510
+ currentPage?: string,
511
+ ): { queued: boolean; rejected?: boolean; requestId: string } {
512
+ if (!this.processing) {
513
+ return { queued: false, requestId };
514
+ }
515
+
516
+ const pushed = this.queue.push({ content, attachments, requestId, onEvent, activeSurfaceId, currentPage });
517
+ if (!pushed) {
518
+ return { queued: false, rejected: true, requestId };
519
+ }
520
+ return { queued: true, requestId };
521
+ }
522
+
523
+ getQueueDepth(): number {
524
+ return this.queue.length;
525
+ }
526
+
527
+ /**
528
+ * Returns true if there are messages waiting in the queue.
529
+ */
530
+ hasQueuedMessages(): boolean {
531
+ return !this.queue.isEmpty;
532
+ }
533
+
534
+ /**
535
+ * Remove a queued message by requestId. Returns true if the message was found
536
+ * and removed, false if the requestId was not in the queue.
537
+ */
538
+ removeQueuedMessage(requestId: string): boolean {
539
+ return this.queue.removeByRequestId(requestId) !== undefined;
540
+ }
541
+
542
+ /**
543
+ * Returns true if the session is currently processing and there are queued
544
+ * messages waiting. This is the predicate used to decide whether to yield
545
+ * at a turn boundary (checkpoint handoff).
546
+ */
547
+ canHandoffAtCheckpoint(): boolean {
548
+ return this.processing && this.hasQueuedMessages();
549
+ }
550
+
551
+ hasPendingConfirmation(requestId: string): boolean {
552
+ return this.prompter.hasPendingRequest(requestId);
553
+ }
554
+
555
+ hasPendingSecret(requestId: string): boolean {
556
+ return this.secretPrompter.hasPendingRequest(requestId);
557
+ }
558
+
559
+ handleConfirmationResponse(
560
+ requestId: string,
561
+ decision: UserDecision,
562
+ selectedPattern?: string,
563
+ selectedScope?: string,
564
+ ): void {
565
+ this.prompter.resolveConfirmation(requestId, decision, selectedPattern, selectedScope);
566
+ }
567
+
568
+ handleSecretResponse(requestId: string, value?: string, delivery?: 'store' | 'transient_send'): void {
569
+ this.secretPrompter.resolveSecret(requestId, value, delivery);
570
+ }
571
+
572
+ /**
573
+ * Bind a runtime assistant ID to this session.
574
+ * IPC-only desktop sessions can leave this unset and use a local scope.
575
+ */
576
+ setAssistantId(assistantId: string): void {
577
+ this.assistantId = assistantId;
578
+ }
579
+
580
+ setChannelCapabilities(caps: ChannelCapabilities): void {
581
+ this.channelCapabilities = caps;
582
+ }
583
+
584
+ private async approveHostAttachmentReadImpl(filePath: string): Promise<boolean> {
585
+ return approveHostAttachmentRead(filePath, this.workingDir, this.prompter, this.conversationId, this.hasNoClient);
586
+ }
587
+
588
+ /**
589
+ * Persist a user message and mark the session as processing.
590
+ * Returns the messageId immediately without running the agent loop.
591
+ * After calling this, call `runAgentLoop` to continue processing.
592
+ */
593
+ persistUserMessage(
594
+ content: string,
595
+ attachments: UserMessageAttachment[],
596
+ requestId?: string,
597
+ ): string {
598
+ if (this.processing) {
599
+ throw new Error('Session is already processing a message');
600
+ }
601
+
602
+ if (!content.trim() && attachments.length === 0) {
603
+ throw new Error('Message content or attachments are required');
604
+ }
605
+
606
+ const reqId = requestId ?? uuid();
607
+ this.currentRequestId = reqId;
608
+ this.processing = true;
609
+ this.abortController = new AbortController();
610
+
611
+ const userMessage = createUserMessage(content, attachments.map((attachment) => ({
612
+ id: attachment.id,
613
+ filename: attachment.filename,
614
+ mimeType: attachment.mimeType,
615
+ data: attachment.data,
616
+ extractedText: attachment.extractedText,
617
+ })));
618
+ this.messages.push(userMessage);
619
+
620
+ try {
621
+ const persistedUserMessage = conversationStore.addMessage(
622
+ this.conversationId,
623
+ 'user',
624
+ JSON.stringify(userMessage.content),
625
+ );
626
+
627
+ if (!persistedUserMessage.id) {
628
+ throw new Error('Failed to persist user message');
629
+ }
630
+
631
+ return persistedUserMessage.id;
632
+ } catch (err) {
633
+ this.messages.pop();
634
+ this.processing = false;
635
+ this.abortController = null;
636
+ this.currentRequestId = undefined;
637
+ throw err;
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Run the agent loop after a user message has been persisted via
643
+ * `persistUserMessage`. Clears the `processing` flag when done.
644
+ *
645
+ * @param options.skipPreMessageRollback - When true, the pre-message hook
646
+ * blocked path will NOT delete the user message from in-memory history or
647
+ * the DB. Used by `regenerate()` where the user message is the original
648
+ * (not freshly persisted) and must be preserved.
649
+ */
650
+ async runAgentLoop(
651
+ content: string,
652
+ userMessageId: string,
653
+ onEvent: (msg: ServerMessage) => void,
654
+ options?: { skipPreMessageRollback?: boolean },
655
+ ): Promise<void> {
656
+ if (!this.abortController) {
657
+ throw new Error('runAgentLoop called without prior persistUserMessage');
658
+ }
659
+ const abortController = this.abortController;
660
+ const reqId = this.currentRequestId ?? uuid();
661
+ const rlog = log.child({ conversationId: this.conversationId, requestId: reqId });
662
+ let yieldedForHandoff = false;
663
+
664
+ // Reset attachment state so a failed exchange never retains stale data
665
+ // from a prior successful run.
666
+ this.lastAssistantAttachments = [];
667
+ this.lastAttachmentWarnings = [];
668
+
669
+ // Ensure the workspace git repo is initialized before any tools run.
670
+ // This must happen before the first turn so the initial commit captures
671
+ // the pre-turn workspace state; otherwise ensureInitialized() would be
672
+ // triggered lazily by getStatus() inside commitTurnChanges(), absorbing
673
+ // the first turn's file changes into the initial commit.
674
+ try {
675
+ const gitService = getWorkspaceGitService(this.workingDir);
676
+ await gitService.ensureInitialized();
677
+ } catch (err) {
678
+ rlog.warn({ err }, 'Failed to initialize workspace git repo (non-fatal)');
679
+ }
680
+
681
+ this.profiler.startRequest();
682
+
683
+ // Tracks whether the agent loop started — once true, we guarantee a
684
+ // turn-boundary commit even if post-processing throws.
685
+ let turnStarted = false;
686
+
687
+ try {
688
+ const preMessageResult = await getHookManager().trigger('pre-message', {
689
+ sessionId: this.conversationId,
690
+ messagePreview: content.slice(0, 200),
691
+ });
692
+
693
+ if (preMessageResult.blocked) {
694
+ if (!options?.skipPreMessageRollback) {
695
+ // Roll back the user message from both in-memory history and the DB.
696
+ // We use deleteMessageById (not deleteLastExchange) because it NULLs
697
+ // nullable FK references (message_runs, channel_inbound_events) before
698
+ // deleting the message row, so the run record survives.
699
+ this.messages.pop();
700
+ conversationStore.deleteMessageById(userMessageId);
701
+ }
702
+ onEvent({ type: 'error', message: `Message blocked by hook "${preMessageResult.blockedBy}"` });
703
+ return;
704
+ }
705
+
706
+ const isFirstMessage = this.messages.length === 1;
707
+
708
+ const compacted = await this.contextWindowManager.maybeCompact(
709
+ this.messages,
710
+ abortController.signal,
711
+ { lastCompactedAt: this.contextCompactedAt ?? undefined },
712
+ );
713
+ if (compacted.compacted) {
714
+ this.messages = compacted.messages;
715
+ this.contextCompactedMessageCount += compacted.compactedPersistedMessages;
716
+ this.contextCompactedAt = Date.now();
717
+ conversationStore.updateConversationContextWindow(
718
+ this.conversationId,
719
+ compacted.summaryText,
720
+ this.contextCompactedMessageCount,
721
+ );
722
+ onEvent({
723
+ type: 'context_compacted',
724
+ previousEstimatedInputTokens: compacted.previousEstimatedInputTokens,
725
+ estimatedInputTokens: compacted.estimatedInputTokens,
726
+ maxInputTokens: compacted.maxInputTokens,
727
+ thresholdTokens: compacted.thresholdTokens,
728
+ compactedMessages: compacted.compactedMessages,
729
+ summaryCalls: compacted.summaryCalls,
730
+ summaryInputTokens: compacted.summaryInputTokens,
731
+ summaryOutputTokens: compacted.summaryOutputTokens,
732
+ summaryModel: compacted.summaryModel,
733
+ });
734
+ this.recordUsage(
735
+ compacted.summaryInputTokens,
736
+ compacted.summaryOutputTokens,
737
+ compacted.summaryModel,
738
+ onEvent,
739
+ 'context_compactor',
740
+ reqId,
741
+ );
742
+ }
743
+
744
+ // Run agent loop
745
+ let firstAssistantText = '';
746
+ let exchangeInputTokens = 0;
747
+ let exchangeOutputTokens = 0;
748
+ let model = '';
749
+ let runMessages = this.messages;
750
+ const pendingToolResults = new Map<string, { content: string; isError: boolean; contentBlocks?: ContentBlock[] }>();
751
+ const persistedToolUseIds = new Set<string>();
752
+ const accumulatedDirectives: DirectiveRequest[] = [];
753
+ const accumulatedToolContentBlocks: ContentBlock[] = [];
754
+ const directiveWarnings: string[] = [];
755
+ let pendingDirectiveDisplayBuffer = '';
756
+ let lastAssistantMessageId: string | undefined;
757
+ let providerErrorUserMessage: string | null = null;
758
+ const memoryResult = await prepareMemoryContext(
759
+ {
760
+ conversationId: this.conversationId,
761
+ messages: this.messages,
762
+ systemPrompt: this.systemPrompt,
763
+ provider: this.provider,
764
+ conflictGate: this.conflictGate,
765
+ scopeId: this.memoryPolicy.scopeId,
766
+ includeDefaultFallback: this.memoryPolicy.includeDefaultFallback,
767
+ },
768
+ content,
769
+ userMessageId,
770
+ abortController.signal,
771
+ onEvent,
772
+ );
773
+
774
+ if (memoryResult.conflictClarification) {
775
+ const assistantMessage = createAssistantMessage(memoryResult.conflictClarification);
776
+ conversationStore.addMessage(
777
+ this.conversationId,
778
+ 'assistant',
779
+ JSON.stringify(assistantMessage.content),
780
+ );
781
+ this.messages.push(assistantMessage);
782
+ onEvent({
783
+ type: 'assistant_text_delta',
784
+ text: memoryResult.conflictClarification,
785
+ sessionId: this.conversationId,
786
+ });
787
+ this.traceEmitter.emit('message_complete', 'Conflict clarification requested (relevant)', {
788
+ requestId: reqId,
789
+ status: 'info',
790
+ attributes: { conflictGate: 'relevant' },
791
+ });
792
+ onEvent({ type: 'message_complete', sessionId: this.conversationId });
793
+ return;
794
+ }
795
+
796
+ const { recall, dynamicProfile, softConflictInstruction, recallInjectionStrategy } = memoryResult;
797
+ runMessages = memoryResult.runMessages;
798
+
799
+ // Inject soft-conflict instruction and active surface context
800
+ let activeSurface: ActiveSurfaceContext | null = null;
801
+ if (this.currentActiveSurfaceId) {
802
+ const stored = this.surfaceState.get(this.currentActiveSurfaceId);
803
+ if (stored && stored.surfaceType === 'dynamic_page') {
804
+ const data = stored.data as DynamicPageSurfaceData;
805
+ activeSurface = {
806
+ surfaceId: this.currentActiveSurfaceId,
807
+ html: data.html,
808
+ currentPage: this.currentPage,
809
+ };
810
+ // Enrich with app context when the surface is backed by a persisted app
811
+ if (data.appId) {
812
+ const app = getApp(data.appId);
813
+ if (app) {
814
+ activeSurface.appId = app.id;
815
+ activeSurface.appName = app.name;
816
+ activeSurface.appSchemaJson = app.schemaJson;
817
+ activeSurface.appFiles = listAppFiles(app.id);
818
+ if (app.pages && Object.keys(app.pages).length > 0) {
819
+ activeSurface.appPages = app.pages;
820
+ }
821
+ }
822
+ }
823
+ }
824
+ }
825
+ // Refresh workspace top-level context before injection
826
+ this.refreshWorkspaceTopLevelContextIfNeeded();
827
+
828
+ runMessages = applyRuntimeInjections(runMessages, {
829
+ softConflictInstruction,
830
+ activeSurface,
831
+ workspaceTopLevelContext: this.workspaceTopLevelContext,
832
+ channelCapabilities: this.channelCapabilities ?? null,
833
+ });
834
+
835
+ // Pre-run repair: fix any message ordering issues before sending to provider.
836
+ // Keep a reference to the original (un-repaired) messages so we can
837
+ // reconstruct this.messages after the agent loop without leaking synthetic
838
+ // tool_result blocks that repair may inject. Leaking those blocks would
839
+ // break undo semantics (isUndoableUserMessage skips user messages
840
+ // containing only tool_result blocks).
841
+ let preRepairMessages = runMessages;
842
+ const preRunRepair = repairHistory(runMessages);
843
+ if (preRunRepair.stats.assistantToolResultsMigrated > 0 || preRunRepair.stats.missingToolResultsInserted > 0 || preRunRepair.stats.orphanToolResultsDowngraded > 0 || preRunRepair.stats.consecutiveSameRoleMerged > 0) {
844
+ rlog.warn({ phase: 'pre_run', ...preRunRepair.stats }, 'Repaired runtime history before provider call');
845
+ runMessages = preRunRepair.messages;
846
+ }
847
+
848
+ let orderingErrorDetected = false;
849
+ let deferredOrderingError: string | null = null;
850
+ let contextTooLargeDetected = false;
851
+ let preRunHistoryLength = runMessages.length;
852
+
853
+ // Track whether llm_call_started has been emitted for the current provider turn.
854
+ // Reset on each usage event (which marks the end of a provider call).
855
+ let llmCallStartedEmitted = false;
856
+
857
+ // Map tool_use_id → toolName so tool_result processing can identify the originating tool.
858
+ const toolUseIdToName = new Map<string, string>();
859
+
860
+ // Track tool names used in the current agent turn for checkpoint decisions.
861
+ let currentTurnToolNames: string[] = [];
862
+
863
+ const buildEventHandler = () => (event: import('../agent/loop.js').AgentEvent) => {
864
+ // Emit llm_call_started once per provider call. Called on first streaming
865
+ // token (text or thinking) or, for tool-only turns, right before the
866
+ // usage event so every llm_call_finished has a matching start.
867
+ const emitLlmCallStartedIfNeeded = () => {
868
+ if (llmCallStartedEmitted) return;
869
+ llmCallStartedEmitted = true;
870
+ this.traceEmitter.emit('llm_call_started', `LLM call to ${this.provider.name}`, {
871
+ requestId: reqId,
872
+ status: 'info',
873
+ attributes: { provider: this.provider.name, model: model || 'unknown' },
874
+ });
875
+ };
876
+
877
+ switch (event.type) {
878
+ case 'text_delta': {
879
+ emitLlmCallStartedIfNeeded();
880
+ pendingDirectiveDisplayBuffer += event.text;
881
+ const drained = drainDirectiveDisplayBuffer(pendingDirectiveDisplayBuffer);
882
+ pendingDirectiveDisplayBuffer = drained.bufferedRemainder;
883
+ if (drained.emitText.length > 0) {
884
+ onEvent({ type: 'assistant_text_delta', text: drained.emitText, sessionId: this.conversationId });
885
+ if (isFirstMessage) firstAssistantText += drained.emitText;
886
+ }
887
+ break;
888
+ }
889
+ case 'thinking_delta':
890
+ // Thinking content itself is NOT included in traces to avoid leaking
891
+ // extended-thinking data.
892
+ emitLlmCallStartedIfNeeded();
893
+ onEvent({ type: 'assistant_thinking_delta', thinking: event.thinking });
894
+ break;
895
+ case 'tool_use':
896
+ toolUseIdToName.set(event.id, event.name);
897
+ currentTurnToolNames.push(event.name);
898
+ onEvent({ type: 'tool_use_start', toolName: event.name, input: event.input, sessionId: this.conversationId });
899
+ break;
900
+ case 'tool_output_chunk':
901
+ onEvent({ type: 'tool_output_chunk', chunk: event.chunk });
902
+ break;
903
+ case 'input_json_delta':
904
+ onEvent({ type: 'tool_input_delta', toolName: event.toolName, content: event.accumulatedJson, sessionId: this.conversationId });
905
+ break;
906
+ case 'tool_result': {
907
+ const imageBlock = event.contentBlocks?.find((b): b is ImageContent => b.type === 'image');
908
+ onEvent({ type: 'tool_result', toolName: '', result: event.content, isError: event.isError, diff: event.diff, status: event.status, sessionId: this.conversationId, imageData: imageBlock?.source.data });
909
+ pendingToolResults.set(event.toolUseId, { content: event.content, isError: event.isError, contentBlocks: event.contentBlocks });
910
+ // Mark workspace context dirty for mutation tools.
911
+ // file_write and bash are always dirty regardless of isError —
912
+ // file_write may physically write before a post-write error, and
913
+ // bash commands can modify the filesystem even when exiting
914
+ // non-zero (e.g. `mkdir foo && false`, `npm install` with audit
915
+ // warnings, compound commands where early parts succeed).
916
+ // file_edit is only dirty on success — a failed edit provably
917
+ // never touches the filesystem.
918
+ {
919
+ const toolName = toolUseIdToName.get(event.toolUseId);
920
+ if (toolName === 'file_write' || toolName === 'bash') {
921
+ this.markWorkspaceTopLevelDirty();
922
+ } else if (toolName === 'file_edit' && !event.isError) {
923
+ this.markWorkspaceTopLevelDirty();
924
+ }
925
+ }
926
+ // Collect image/file content blocks for assistant attachment conversion
927
+ if (event.contentBlocks) {
928
+ for (const cb of event.contentBlocks) {
929
+ if (cb.type === 'image' || cb.type === 'file') {
930
+ accumulatedToolContentBlocks.push(cb);
931
+ }
932
+ }
933
+ }
934
+ break;
935
+ }
936
+ case 'error':
937
+ if (isProviderOrderingError(event.error.message)) {
938
+ orderingErrorDetected = true;
939
+ // Defer the error event — only forward if retry also fails
940
+ deferredOrderingError = event.error.message;
941
+ } else if (isContextTooLarge(event.error.message)) {
942
+ contextTooLargeDetected = true;
943
+ // Defer — attempt compaction + retry before surfacing to user
944
+ } else {
945
+ const classified = classifySessionError(event.error, { phase: 'agent_loop' });
946
+ onEvent(buildSessionErrorMessage(this.conversationId, classified));
947
+ providerErrorUserMessage = classified.userMessage;
948
+ }
949
+ break;
950
+ case 'message_complete': {
951
+ if (pendingDirectiveDisplayBuffer.length > 0) {
952
+ onEvent({
953
+ type: 'assistant_text_delta',
954
+ text: pendingDirectiveDisplayBuffer,
955
+ sessionId: this.conversationId,
956
+ });
957
+ if (isFirstMessage) firstAssistantText += pendingDirectiveDisplayBuffer;
958
+ pendingDirectiveDisplayBuffer = '';
959
+ }
960
+ // Save pending tool results as a user message before the next assistant message.
961
+ // tool_result blocks belong in user messages per the Anthropic API spec.
962
+ if (pendingToolResults.size > 0) {
963
+ const toolResultBlocks = Array.from(pendingToolResults.entries()).map(
964
+ ([toolUseId, result]) => ({
965
+ type: 'tool_result',
966
+ tool_use_id: toolUseId,
967
+ content: result.content,
968
+ is_error: result.isError,
969
+ ...(result.contentBlocks ? { contentBlocks: result.contentBlocks } : {}),
970
+ }),
971
+ );
972
+ conversationStore.addMessage(
973
+ this.conversationId,
974
+ 'user',
975
+ JSON.stringify(toolResultBlocks),
976
+ );
977
+ for (const id of pendingToolResults.keys()) {
978
+ persistedToolUseIds.add(id);
979
+ }
980
+ pendingToolResults.clear();
981
+ }
982
+ // Parse and strip attachment directives from assistant text
983
+ const { cleanedContent, directives: msgDirectives, warnings: msgWarnings } =
984
+ cleanAssistantContent(event.message.content);
985
+ accumulatedDirectives.push(...msgDirectives);
986
+ directiveWarnings.push(...msgWarnings);
987
+ if (msgDirectives.length > 0) {
988
+ rlog.info(
989
+ { parsedDirectives: msgDirectives.map(d => ({ source: d.source, path: d.path, mimeType: d.mimeType })), totalAccumulated: accumulatedDirectives.length },
990
+ 'Parsed attachment directives from assistant message',
991
+ );
992
+ }
993
+
994
+ // Add surface blocks to content for persistence
995
+ const contentWithSurfaces: ContentBlock[] = [...cleanedContent as ContentBlock[]];
996
+ for (const surface of this.currentTurnSurfaces) {
997
+ contentWithSurfaces.push({
998
+ type: 'ui_surface',
999
+ surfaceId: surface.surfaceId,
1000
+ surfaceType: surface.surfaceType,
1001
+ title: surface.title,
1002
+ data: surface.data,
1003
+ actions: surface.actions,
1004
+ display: surface.display,
1005
+ } as unknown as ContentBlock);
1006
+ }
1007
+
1008
+ // Save assistant message with cleaned content (tags stripped) plus surfaces
1009
+ const assistantMsg = conversationStore.addMessage(
1010
+ this.conversationId,
1011
+ 'assistant',
1012
+ JSON.stringify(contentWithSurfaces),
1013
+ );
1014
+ lastAssistantMessageId = assistantMsg.id;
1015
+
1016
+ // Clear surfaces for next turn
1017
+ this.currentTurnSurfaces = [];
1018
+
1019
+ // Emit assistant_message trace with content metrics.
1020
+ // Char count only includes text blocks; thinking blocks are
1021
+ // explicitly excluded from traces.
1022
+ const charCount = cleanedContent
1023
+ .filter((b) => (b as Record<string, unknown>).type === 'text')
1024
+ .reduce((sum: number, b) => sum + ((b as { text?: string }).text?.length ?? 0), 0);
1025
+ const toolUseCount = event.message.content
1026
+ .filter((b) => b.type === 'tool_use')
1027
+ .length;
1028
+ this.traceEmitter.emit('assistant_message', 'Assistant message complete', {
1029
+ requestId: reqId,
1030
+ status: 'success',
1031
+ attributes: { charCount, toolUseCount },
1032
+ });
1033
+ break;
1034
+ }
1035
+ case 'usage':
1036
+ exchangeInputTokens += event.inputTokens;
1037
+ exchangeOutputTokens += event.outputTokens;
1038
+ model = event.model;
1039
+
1040
+ // Persist raw LLM request/response payloads for diagnostics export
1041
+ if (event.rawRequest && event.rawResponse) {
1042
+ try {
1043
+ recordRequestLog(
1044
+ this.conversationId,
1045
+ JSON.stringify(event.rawRequest),
1046
+ JSON.stringify(event.rawResponse),
1047
+ );
1048
+ } catch (err) {
1049
+ rlog.warn({ err }, 'Failed to persist LLM request log (non-fatal)');
1050
+ }
1051
+ }
1052
+
1053
+ // Ensure llm_call_started is emitted even for tool-only turns
1054
+ // (where no text_delta or thinking_delta events fire)
1055
+ emitLlmCallStartedIfNeeded();
1056
+
1057
+ // Emit llm_call_finished trace with token and latency metrics
1058
+ this.traceEmitter.emit('llm_call_finished', `LLM call to ${this.provider.name} finished`, {
1059
+ requestId: reqId,
1060
+ status: 'success',
1061
+ attributes: {
1062
+ provider: this.provider.name,
1063
+ model: event.model,
1064
+ inputTokens: event.inputTokens,
1065
+ outputTokens: event.outputTokens,
1066
+ latencyMs: event.providerDurationMs,
1067
+ },
1068
+ });
1069
+ // Reset flag so the next provider call in this agent loop run
1070
+ // gets its own llm_call_started trace
1071
+ llmCallStartedEmitted = false;
1072
+ break;
1073
+ }
1074
+ };
1075
+
1076
+ const onCheckpoint = (): CheckpointDecision => {
1077
+ // Capture and reset tool names for this turn
1078
+ const turnTools = currentTurnToolNames;
1079
+ currentTurnToolNames = [];
1080
+
1081
+ if (this.canHandoffAtCheckpoint()) {
1082
+ // Don't interrupt active browser interaction flows — the agent
1083
+ // needs multiple consecutive turns (snapshot → click → snapshot)
1084
+ // and yielding mid-flow leaves the task incomplete.
1085
+ const inBrowserFlow = turnTools.length > 0
1086
+ && turnTools.every(n => n.startsWith('browser_'));
1087
+ if (!inBrowserFlow) {
1088
+ yieldedForHandoff = true;
1089
+ return 'yield';
1090
+ }
1091
+ }
1092
+ return 'continue';
1093
+ };
1094
+
1095
+ // Mark that the agent loop is about to run — workspace files may be
1096
+ // modified from this point onward, so we must commit at the turn boundary
1097
+ // even if post-processing (e.g. resolveAssistantAttachments) throws.
1098
+ turnStarted = true;
1099
+
1100
+ let updatedHistory = await this.agentLoop.run(
1101
+ runMessages,
1102
+ buildEventHandler(),
1103
+ abortController.signal,
1104
+ reqId,
1105
+ onCheckpoint,
1106
+ );
1107
+
1108
+ // One-shot self-heal retry: if the provider returned a strict ordering
1109
+ // error and no messages were appended (error on first call), apply a
1110
+ // deep repair (handles additional edge cases like consecutive same-role
1111
+ // messages) and retry exactly once.
1112
+ if (orderingErrorDetected && updatedHistory.length === preRunHistoryLength) {
1113
+ rlog.warn({ phase: 'retry' }, 'Provider ordering error detected, attempting one-shot deep-repair retry');
1114
+ const retryRepair = deepRepairHistory(runMessages);
1115
+ runMessages = retryRepair.messages;
1116
+ // Update preRepairMessages so that structural fixes from deep repair
1117
+ // (e.g., stripping leading assistant messages, merging same-role runs)
1118
+ // persist in this.messages after the run. Without this, the original
1119
+ // malformed prefix would be restored and trigger the same error next turn.
1120
+ preRepairMessages = retryRepair.messages;
1121
+ preRunHistoryLength = runMessages.length;
1122
+ orderingErrorDetected = false;
1123
+ deferredOrderingError = null;
1124
+
1125
+ updatedHistory = await this.agentLoop.run(
1126
+ runMessages,
1127
+ buildEventHandler(),
1128
+ abortController.signal,
1129
+ reqId,
1130
+ onCheckpoint,
1131
+ );
1132
+
1133
+ if (orderingErrorDetected) {
1134
+ rlog.error({ phase: 'retry' }, 'Deep-repair retry also failed with ordering error. Consider starting a new conversation if this persists.');
1135
+ }
1136
+ }
1137
+
1138
+ // One-shot context-too-large recovery: force compaction and retry once.
1139
+ if (contextTooLargeDetected && updatedHistory.length === preRunHistoryLength) {
1140
+ rlog.warn({ phase: 'retry' }, 'Context too large — attempting forced compaction and retry');
1141
+ const emergencyCompact = await this.contextWindowManager.maybeCompact(
1142
+ this.messages,
1143
+ abortController.signal,
1144
+ { lastCompactedAt: this.contextCompactedAt ?? undefined, force: true },
1145
+ );
1146
+ if (emergencyCompact.compacted) {
1147
+ this.messages = emergencyCompact.messages;
1148
+ this.contextCompactedMessageCount += emergencyCompact.compactedPersistedMessages;
1149
+ this.contextCompactedAt = Date.now();
1150
+ conversationStore.updateConversationContextWindow(
1151
+ this.conversationId,
1152
+ emergencyCompact.summaryText,
1153
+ this.contextCompactedMessageCount,
1154
+ );
1155
+ onEvent({
1156
+ type: 'context_compacted',
1157
+ previousEstimatedInputTokens: emergencyCompact.previousEstimatedInputTokens,
1158
+ estimatedInputTokens: emergencyCompact.estimatedInputTokens,
1159
+ maxInputTokens: emergencyCompact.maxInputTokens,
1160
+ thresholdTokens: emergencyCompact.thresholdTokens,
1161
+ compactedMessages: emergencyCompact.compactedMessages,
1162
+ summaryCalls: emergencyCompact.summaryCalls,
1163
+ summaryInputTokens: emergencyCompact.summaryInputTokens,
1164
+ summaryOutputTokens: emergencyCompact.summaryOutputTokens,
1165
+ summaryModel: emergencyCompact.summaryModel,
1166
+ });
1167
+ this.recordUsage(
1168
+ emergencyCompact.summaryInputTokens,
1169
+ emergencyCompact.summaryOutputTokens,
1170
+ emergencyCompact.summaryModel,
1171
+ onEvent,
1172
+ 'context_compactor',
1173
+ reqId,
1174
+ );
1175
+
1176
+ // Retry with compacted context
1177
+ runMessages = applyRuntimeInjections(this.messages, {
1178
+ softConflictInstruction,
1179
+ activeSurface,
1180
+ workspaceTopLevelContext: this.workspaceTopLevelContext,
1181
+ });
1182
+ preRepairMessages = runMessages;
1183
+ preRunHistoryLength = runMessages.length;
1184
+ contextTooLargeDetected = false;
1185
+
1186
+ updatedHistory = await this.agentLoop.run(
1187
+ runMessages,
1188
+ buildEventHandler(),
1189
+ abortController.signal,
1190
+ reqId,
1191
+ onCheckpoint,
1192
+ );
1193
+ }
1194
+
1195
+ if (contextTooLargeDetected) {
1196
+ const mediaTrimmed = stripMediaPayloadsForRetry(this.messages);
1197
+ if (mediaTrimmed.modified) {
1198
+ rlog.warn(
1199
+ {
1200
+ phase: 'retry',
1201
+ replacedBlocks: mediaTrimmed.replacedBlocks,
1202
+ latestUserIndex: mediaTrimmed.latestUserIndex,
1203
+ },
1204
+ 'Context still too large — retrying with older media payloads trimmed',
1205
+ );
1206
+ this.messages = mediaTrimmed.messages;
1207
+ runMessages = applyRuntimeInjections(this.messages, {
1208
+ softConflictInstruction,
1209
+ activeSurface,
1210
+ workspaceTopLevelContext: this.workspaceTopLevelContext,
1211
+ });
1212
+ preRepairMessages = runMessages;
1213
+ preRunHistoryLength = runMessages.length;
1214
+ contextTooLargeDetected = false;
1215
+
1216
+ updatedHistory = await this.agentLoop.run(
1217
+ runMessages,
1218
+ buildEventHandler(),
1219
+ abortController.signal,
1220
+ reqId,
1221
+ onCheckpoint,
1222
+ );
1223
+ }
1224
+ }
1225
+
1226
+ // Surface the error if compaction didn't help or wasn't possible
1227
+ if (contextTooLargeDetected) {
1228
+ const classified = classifySessionError(
1229
+ new Error('context_length_exceeded'),
1230
+ { phase: 'agent_loop' },
1231
+ );
1232
+ onEvent(buildSessionErrorMessage(this.conversationId, classified));
1233
+ }
1234
+ }
1235
+
1236
+ // Forward the deferred ordering error to the client if retry failed or was not attempted
1237
+ if (deferredOrderingError) {
1238
+ const classified = classifySessionError(new Error(deferredOrderingError), { phase: 'agent_loop' });
1239
+ onEvent(buildSessionErrorMessage(this.conversationId, classified));
1240
+ }
1241
+
1242
+ // Reconcile synthesized cancellation tool_results from history tail.
1243
+ // When abort happens, the agent loop synthesizes "Cancelled by user"
1244
+ // results directly into the history without firing tool_result events,
1245
+ // so they're missing from pendingToolResults and would not be persisted.
1246
+ for (let i = preRunHistoryLength; i < updatedHistory.length; i++) {
1247
+ const msg = updatedHistory[i];
1248
+ if (msg.role === 'user') {
1249
+ for (const block of msg.content) {
1250
+ if (block.type === 'tool_result' && !pendingToolResults.has(block.tool_use_id) && !persistedToolUseIds.has(block.tool_use_id)) {
1251
+ pendingToolResults.set(block.tool_use_id, {
1252
+ content: block.content,
1253
+ isError: block.is_error ?? false,
1254
+ });
1255
+ }
1256
+ }
1257
+ }
1258
+ }
1259
+
1260
+ // Flush any remaining tool results as a user message
1261
+ if (pendingToolResults.size > 0) {
1262
+ const toolResultBlocks = Array.from(pendingToolResults.entries()).map(
1263
+ ([toolUseId, result]) => ({
1264
+ type: 'tool_result',
1265
+ tool_use_id: toolUseId,
1266
+ content: result.content,
1267
+ is_error: result.isError,
1268
+ ...(result.contentBlocks ? { contentBlocks: result.contentBlocks } : {}),
1269
+ }),
1270
+ );
1271
+ conversationStore.addMessage(
1272
+ this.conversationId,
1273
+ 'user',
1274
+ JSON.stringify(toolResultBlocks),
1275
+ );
1276
+ pendingToolResults.clear();
1277
+ }
1278
+
1279
+ // Reconstruct history: use the original (un-repaired) prefix so that
1280
+ // synthetic tool_result blocks from pre-run repair don't leak into
1281
+ // this.messages. Only the new messages appended by the agent loop
1282
+ // (beyond the repaired prefix) are carried forward.
1283
+ //
1284
+ // Strip directive tags from assistant messages so in-memory history
1285
+ // matches the cleaned content persisted to the DB. Without this,
1286
+ // subsequent turns would send raw <vellum-attachment /> tags to the
1287
+ // LLM, wasting tokens and encouraging hallucinated directives.
1288
+ const newMessages = updatedHistory.slice(preRunHistoryLength).map((msg) => {
1289
+ if (msg.role !== 'assistant') return msg;
1290
+ const { cleanedContent } = cleanAssistantContent(msg.content);
1291
+ return { ...msg, content: cleanedContent as ContentBlock[] };
1292
+ });
1293
+
1294
+ // If no assistant response was produced (e.g. provider 500 error),
1295
+ // synthesize an assistant message so the error is visible in the conversation.
1296
+ const hasAssistantResponse = newMessages.some((msg) => msg.role === 'assistant');
1297
+ if (!hasAssistantResponse && providerErrorUserMessage && !abortController.signal.aborted && !yieldedForHandoff) {
1298
+ const errorAssistantMessage = createAssistantMessage(providerErrorUserMessage);
1299
+ conversationStore.addMessage(
1300
+ this.conversationId,
1301
+ 'assistant',
1302
+ JSON.stringify(errorAssistantMessage.content),
1303
+ );
1304
+ newMessages.push(errorAssistantMessage);
1305
+ onEvent({
1306
+ type: 'assistant_text_delta',
1307
+ text: providerErrorUserMessage,
1308
+ sessionId: this.conversationId,
1309
+ });
1310
+ }
1311
+
1312
+ const restoredHistory = [...preRepairMessages, ...newMessages];
1313
+ const recallStripped = stripMemoryRecallMessages(restoredHistory, recall.injectedText, recallInjectionStrategy);
1314
+ this.messages = stripChannelCapabilityContext(
1315
+ stripWorkspaceTopLevelContext(
1316
+ stripActiveSurfaceContext(
1317
+ stripDynamicProfileMessages(recallStripped, dynamicProfile.text),
1318
+ ),
1319
+ ),
1320
+ );
1321
+
1322
+ this.recordUsage(exchangeInputTokens, exchangeOutputTokens, model, onEvent, 'main_agent', reqId);
1323
+
1324
+ void getHookManager().trigger('post-message', {
1325
+ sessionId: this.conversationId,
1326
+ });
1327
+
1328
+ // Resolve accumulated attachment directives and tool content blocks
1329
+ // BEFORE emitting the completion event so attachments are included.
1330
+ const attachmentResult = await resolveAssistantAttachments(
1331
+ accumulatedDirectives,
1332
+ accumulatedToolContentBlocks,
1333
+ directiveWarnings,
1334
+ this.workingDir,
1335
+ async (filePath) => this.approveHostAttachmentReadImpl(filePath),
1336
+ lastAssistantMessageId,
1337
+ this.assistantId ?? 'local-assistant',
1338
+ );
1339
+ const { assistantAttachments, emittedAttachments } = attachmentResult;
1340
+
1341
+ this.lastAssistantAttachments = assistantAttachments;
1342
+ this.lastAttachmentWarnings = attachmentResult.directiveWarnings;
1343
+
1344
+ const warningText = formatAttachmentWarnings(attachmentResult.directiveWarnings);
1345
+ if (warningText) {
1346
+ onEvent({ type: 'assistant_text_delta', text: warningText, sessionId: this.conversationId });
1347
+ }
1348
+
1349
+ // Emit the completion event here in the try block; the turn-boundary
1350
+ // commit runs in `finally` (after this), so the client's
1351
+ // thinking/streaming indicators clear immediately without waiting
1352
+ // for the git commit (which can take 0.5–2 s on large workspaces).
1353
+ if (yieldedForHandoff) {
1354
+ this.traceEmitter.emit('generation_handoff', 'Handing off to next queued message', {
1355
+ requestId: reqId,
1356
+ status: 'info',
1357
+ attributes: { queuedCount: this.getQueueDepth() },
1358
+ });
1359
+ onEvent({
1360
+ type: 'generation_handoff',
1361
+ sessionId: this.conversationId,
1362
+ requestId: reqId,
1363
+ queuedCount: this.getQueueDepth(),
1364
+ ...(emittedAttachments.length > 0 ? { attachments: emittedAttachments } : {}),
1365
+ });
1366
+ } else if (abortController.signal.aborted) {
1367
+ this.traceEmitter.emit('generation_cancelled', 'Generation cancelled by user', {
1368
+ requestId: reqId,
1369
+ status: 'warning',
1370
+ });
1371
+ onEvent({ type: 'generation_cancelled', sessionId: this.conversationId });
1372
+ } else {
1373
+ this.traceEmitter.emit('message_complete', 'Message processing complete', {
1374
+ requestId: reqId,
1375
+ status: 'success',
1376
+ });
1377
+ onEvent({
1378
+ type: 'message_complete',
1379
+ sessionId: this.conversationId,
1380
+ ...(emittedAttachments.length > 0 ? { attachments: emittedAttachments } : {}),
1381
+ });
1382
+ }
1383
+
1384
+ // Auto-generate conversation title after first exchange
1385
+ if (isFirstMessage) {
1386
+ this.generateTitle(content, firstAssistantText).catch((err) => {
1387
+ log.warn({ err, conversationId: this.conversationId }, 'Failed to generate conversation title (non-fatal, using default title)');
1388
+ });
1389
+ }
1390
+ } catch (err) {
1391
+ const errorCtx = { phase: 'agent_loop' as const, aborted: abortController.signal.aborted };
1392
+ // AbortError is expected when user cancels — don't treat as an error
1393
+ if (isUserCancellation(err, errorCtx)) {
1394
+ rlog.info('Generation cancelled by user');
1395
+ this.traceEmitter.emit('generation_cancelled', 'Generation cancelled by user', {
1396
+ requestId: reqId,
1397
+ status: 'warning',
1398
+ });
1399
+ onEvent({ type: 'generation_cancelled', sessionId: this.conversationId });
1400
+ } else {
1401
+ const message = err instanceof Error ? err.message : String(err);
1402
+ const errorClass = err instanceof Error ? err.constructor.name : 'Error';
1403
+ rlog.error({ err }, 'Session processing error');
1404
+ this.traceEmitter.emit('request_error', message.slice(0, 200), {
1405
+ requestId: reqId,
1406
+ status: 'error',
1407
+ attributes: { errorClass, message: message.slice(0, 500) },
1408
+ });
1409
+ onEvent({ type: 'error', message: `Failed to process message: ${message}` });
1410
+ const classified = classifySessionError(err, errorCtx);
1411
+ onEvent(buildSessionErrorMessage(this.conversationId, classified));
1412
+ void getHookManager().trigger('on-error', {
1413
+ error: err instanceof Error ? err.name : 'Error',
1414
+ message,
1415
+ stack: err instanceof Error ? err.stack : undefined,
1416
+ sessionId: this.conversationId,
1417
+ });
1418
+ }
1419
+ } finally {
1420
+ // Turn-boundary commit: runs after completion/error events (try or
1421
+ // catch) but before drainQueue. Guarantees a commit attempt whenever
1422
+ // the agent loop started, even if post-processing threw.
1423
+ if (turnStarted) {
1424
+ this.turnCount++;
1425
+ await commitTurnChanges(this.workingDir, this.conversationId, this.turnCount);
1426
+ }
1427
+
1428
+ this.profiler.emitSummary(this.traceEmitter, reqId);
1429
+
1430
+ this.abortController = null;
1431
+ this.processing = false;
1432
+ this.currentRequestId = undefined;
1433
+ this.currentActiveSurfaceId = undefined;
1434
+ this.allowedToolNames = undefined;
1435
+ this.preactivatedSkillIds = undefined;
1436
+
1437
+ // Consolidate consecutive assistant messages from this agent loop run
1438
+ if (userMessageId) {
1439
+ this.consolidateAssistantMessages(userMessageId);
1440
+ }
1441
+
1442
+ // Drain the next queued message, if any
1443
+ this.drainQueue(yieldedForHandoff ? 'checkpoint_handoff' : 'loop_complete');
1444
+ }
1445
+ }
1446
+
1447
+ private consolidateAssistantMessages(userMessageId: string): void {
1448
+ consolidateAssistantMessages(this.conversationId, userMessageId);
1449
+ }
1450
+
1451
+ private drainQueue(reason: QueueDrainReason = 'loop_complete'): void {
1452
+ drainQueueImpl(this as ProcessSessionContext, reason);
1453
+ }
1454
+
1455
+ async processMessage(
1456
+ content: string,
1457
+ attachments: UserMessageAttachment[],
1458
+ onEvent: (msg: ServerMessage) => void,
1459
+ requestId?: string,
1460
+ activeSurfaceId?: string,
1461
+ currentPage?: string,
1462
+ ): Promise<string> {
1463
+ return processMessageImpl(this as ProcessSessionContext, content, attachments, onEvent, requestId, activeSurfaceId, currentPage);
1464
+ }
1465
+
1466
+ handleSurfaceAction(surfaceId: string, actionId: string, data?: Record<string, unknown>): void {
1467
+ handleSurfaceActionImpl(this, surfaceId, actionId, data);
1468
+ }
1469
+
1470
+ getMessages(): Message[] {
1471
+ return this.messages;
1472
+ }
1473
+
1474
+ undo(): number {
1475
+ return undoImpl(this as HistorySessionContext);
1476
+ }
1477
+
1478
+ async regenerate(onEvent: (msg: ServerMessage) => void, requestId?: string): Promise<void> {
1479
+ return regenerateImpl(this as HistorySessionContext, onEvent, requestId);
1480
+ }
1481
+
1482
+ // ── Workspace Top-Level Context ──────────────────────────────────
1483
+
1484
+ refreshWorkspaceTopLevelContextIfNeeded(): void {
1485
+ refreshWorkspaceImpl(this);
1486
+ }
1487
+
1488
+ markWorkspaceTopLevelDirty(): void {
1489
+ this.workspaceTopLevelDirty = true;
1490
+ }
1491
+
1492
+ getWorkspaceTopLevelContext(): string | null {
1493
+ return this.workspaceTopLevelContext;
1494
+ }
1495
+
1496
+ isWorkspaceTopLevelDirty(): boolean {
1497
+ return this.workspaceTopLevelDirty;
1498
+ }
1499
+
1500
+ /**
1501
+ * After an app_update, refresh any active surface that displays the updated app.
1502
+ * This makes app_update a single call that both persists AND displays changes.
1503
+ */
1504
+ handleSurfaceUndo(surfaceId: string): void {
1505
+ handleSurfaceUndoImpl(this, surfaceId);
1506
+ }
1507
+
1508
+ private recordUsage(
1509
+ inputTokens: number,
1510
+ outputTokens: number,
1511
+ model: string,
1512
+ onEvent: (msg: ServerMessage) => void,
1513
+ actor: UsageActor,
1514
+ requestId: string | null = null,
1515
+ ): void {
1516
+ recordUsage(
1517
+ { conversationId: this.conversationId, providerName: this.provider.name, assistantId: this.assistantId, usageStats: this.usageStats },
1518
+ inputTokens, outputTokens, model, onEvent, actor, requestId,
1519
+ );
1520
+ }
1521
+
1522
+ private async generateTitle(userMessage: string, assistantResponse: string): Promise<void> {
1523
+ const prompt = `Generate a very short title for this conversation. Rules: at most 5 words, at most 40 characters, no quotes.\n\nUser: ${userMessage.slice(0, 200)}\nAssistant: ${assistantResponse.slice(0, 200)}`;
1524
+ const response = await this.provider.sendMessage(
1525
+ [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
1526
+ [], // no tools
1527
+ undefined, // no system prompt
1528
+ { config: { max_tokens: 30 } },
1529
+ );
1530
+
1531
+ const textBlock = response.content.find((b) => b.type === 'text');
1532
+ if (textBlock && textBlock.type === 'text') {
1533
+ let title = textBlock.text.trim().replace(/^["']|["']$/g, '');
1534
+ const words = title.split(/\s+/);
1535
+ if (words.length > 5) title = words.slice(0, 5).join(' ');
1536
+ if (title.length > 40) title = title.slice(0, 40).trimEnd();
1537
+ conversationStore.updateConversationTitle(this.conversationId, title);
1538
+ log.info({ conversationId: this.conversationId, title }, 'Auto-generated conversation title');
1539
+ }
1540
+ }
1541
+
1542
+ }
1543
+
1544
+ function stripMediaPayloadsForRetry(messages: Message[]): { messages: Message[]; modified: boolean; replacedBlocks: number; latestUserIndex: number | null } {
1545
+ let latestUserIndex: number | null = null;
1546
+ for (let i = messages.length - 1; i >= 0; i--) {
1547
+ const msg = messages[i];
1548
+ if (msg.role !== 'user') continue;
1549
+ if (getSummaryFromContextMessage(msg) !== null) continue;
1550
+ if (isToolResultOnlyMessage(msg)) continue;
1551
+ latestUserIndex = i;
1552
+ break;
1553
+ }
1554
+
1555
+ let modified = false;
1556
+ let replacedBlocks = 0;
1557
+ let keptLatestMediaBlocks = 0;
1558
+
1559
+ const nextMessages = messages.map((msg, msgIndex) => {
1560
+ const nextContent: ContentBlock[] = [];
1561
+ for (const block of msg.content) {
1562
+ if (block.type === 'image') {
1563
+ const keep = latestUserIndex === msgIndex && keptLatestMediaBlocks < RETRY_KEEP_LATEST_MEDIA_BLOCKS;
1564
+ if (keep) {
1565
+ keptLatestMediaBlocks += 1;
1566
+ nextContent.push(block);
1567
+ } else {
1568
+ replacedBlocks += 1;
1569
+ modified = true;
1570
+ nextContent.push(imageBlockToStub(block));
1571
+ }
1572
+ continue;
1573
+ }
1574
+
1575
+ if (block.type === 'file') {
1576
+ const keep = latestUserIndex === msgIndex && keptLatestMediaBlocks < RETRY_KEEP_LATEST_MEDIA_BLOCKS;
1577
+ if (keep) {
1578
+ keptLatestMediaBlocks += 1;
1579
+ nextContent.push(block);
1580
+ } else {
1581
+ replacedBlocks += 1;
1582
+ modified = true;
1583
+ nextContent.push(fileBlockToStub(block));
1584
+ }
1585
+ continue;
1586
+ }
1587
+
1588
+ if (block.type === 'tool_result' && block.contentBlocks && block.contentBlocks.length > 0) {
1589
+ let toolResultChanged = false;
1590
+ const nextToolContentBlocks: ContentBlock[] = block.contentBlocks.map((cb) => {
1591
+ if (cb.type === 'image') {
1592
+ replacedBlocks += 1;
1593
+ modified = true;
1594
+ toolResultChanged = true;
1595
+ return imageBlockToStub(cb);
1596
+ }
1597
+ if (cb.type === 'file') {
1598
+ replacedBlocks += 1;
1599
+ modified = true;
1600
+ toolResultChanged = true;
1601
+ return fileBlockToStub(cb);
1602
+ }
1603
+ return cb;
1604
+ });
1605
+ if (toolResultChanged) {
1606
+ nextContent.push({ ...block, contentBlocks: nextToolContentBlocks });
1607
+ } else {
1608
+ nextContent.push(block);
1609
+ }
1610
+ continue;
1611
+ }
1612
+
1613
+ nextContent.push(block);
1614
+ }
1615
+ return { ...msg, content: nextContent };
1616
+ });
1617
+
1618
+ return {
1619
+ messages: modified ? nextMessages : messages,
1620
+ modified,
1621
+ replacedBlocks,
1622
+ latestUserIndex,
1623
+ };
1624
+ }
1625
+
1626
+ function imageBlockToStub(block: Extract<ContentBlock, { type: 'image' }>): Extract<ContentBlock, { type: 'text' }> {
1627
+ const sizeBytes = Math.ceil(block.source.data.length / 4) * 3;
1628
+ return {
1629
+ type: 'text',
1630
+ text: `[Image omitted from retry context: ${block.source.media_type}, ${sizeBytes} bytes]`,
1631
+ };
1632
+ }
1633
+
1634
+ function fileBlockToStub(block: Extract<ContentBlock, { type: 'file' }>): Extract<ContentBlock, { type: 'text' }> {
1635
+ const sizeBytes = Math.ceil(block.source.data.length / 4) * 3;
1636
+ const extracted = (block.extracted_text ?? '').trim();
1637
+ const preview = extracted.length > MAX_MEDIA_STUB_TEXT
1638
+ ? `${extracted.slice(0, MAX_MEDIA_STUB_TEXT)}...`
1639
+ : extracted;
1640
+ return {
1641
+ type: 'text',
1642
+ text: preview.length > 0
1643
+ ? `[File omitted from retry context: ${block.source.filename} (${block.source.media_type}, ${sizeBytes} bytes)]\n${preview}`
1644
+ : `[File omitted from retry context: ${block.source.filename} (${block.source.media_type}, ${sizeBytes} bytes)]`,
1645
+ };
1646
+ }
1647
+
1648
+ function isToolResultOnlyMessage(message: Message): boolean {
1649
+ return message.content.length > 0
1650
+ && message.content.every((block) => block.type === 'tool_result');
1651
+ }