@vellumai/assistant 0.4.48 → 0.4.50

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 (423) hide show
  1. package/ARCHITECTURE.md +26 -35
  2. package/README.md +5 -26
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/architecture/memory.md +180 -119
  6. package/docs/runbook-trusted-contacts.md +3 -8
  7. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  8. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  9. package/package.json +2 -2
  10. package/src/__tests__/actor-token-service.test.ts +0 -1
  11. package/src/__tests__/agent-loop.test.ts +3 -1
  12. package/src/__tests__/anthropic-provider.test.ts +249 -2
  13. package/src/__tests__/approval-cascade.test.ts +796 -0
  14. package/src/__tests__/approval-primitive.test.ts +0 -1
  15. package/src/__tests__/approval-routes-http.test.ts +4 -0
  16. package/src/__tests__/assistant-attachments.test.ts +12 -34
  17. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  18. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  19. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  22. package/src/__tests__/channel-guardian.test.ts +0 -2
  23. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  24. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  25. package/src/__tests__/checker.test.ts +13 -20
  26. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  27. package/src/__tests__/computer-use-tools.test.ts +2 -19
  28. package/src/__tests__/config-schema.test.ts +1 -68
  29. package/src/__tests__/config-watcher.test.ts +0 -1
  30. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  31. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  32. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  33. package/src/__tests__/context-token-estimator.test.ts +196 -13
  34. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  35. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  36. package/src/__tests__/conversation-routes-guardian-reply.test.ts +152 -0
  37. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
  38. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  39. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  40. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  41. package/src/__tests__/credential-vault-unit.test.ts +284 -49
  42. package/src/__tests__/credential-vault.test.ts +150 -16
  43. package/src/__tests__/credentials-cli.test.ts +71 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  45. package/src/__tests__/date-context.test.ts +93 -77
  46. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  47. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  48. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  49. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  50. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  51. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  52. package/src/__tests__/guardian-routing-invariants.test.ts +93 -1
  53. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  54. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  55. package/src/__tests__/heartbeat-service.test.ts +0 -1
  56. package/src/__tests__/history-repair.test.ts +245 -0
  57. package/src/__tests__/host-cu-proxy.test.ts +791 -0
  58. package/src/__tests__/host-shell-tool.test.ts +27 -15
  59. package/src/__tests__/http-user-message-parity.test.ts +2 -0
  60. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  61. package/src/__tests__/integration-status.test.ts +32 -51
  62. package/src/__tests__/intent-routing.test.ts +0 -1
  63. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  64. package/src/__tests__/invite-routes-http.test.ts +10 -9
  65. package/src/__tests__/keychain-broker-client.test.ts +14 -46
  66. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  68. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  69. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  70. package/src/__tests__/memory-regressions.test.ts +477 -2841
  71. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  72. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  73. package/src/__tests__/mime-builder.test.ts +28 -0
  74. package/src/__tests__/native-web-search.test.ts +1 -0
  75. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  76. package/src/__tests__/oauth-cli.test.ts +941 -15
  77. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  78. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  79. package/src/__tests__/oauth-store.test.ts +870 -0
  80. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  81. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  82. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  83. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  84. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  85. package/src/__tests__/recording-handler.test.ts +3 -4
  86. package/src/__tests__/registry.test.ts +2 -3
  87. package/src/__tests__/relay-server.test.ts +46 -1
  88. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  89. package/src/__tests__/schedule-store.test.ts +0 -1
  90. package/src/__tests__/schedule-tools.test.ts +32 -0
  91. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  92. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  93. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  94. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  95. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  96. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  97. package/src/__tests__/secure-keys.test.ts +7 -2
  98. package/src/__tests__/send-endpoint-busy.test.ts +24 -6
  99. package/src/__tests__/sequence-store.test.ts +0 -1
  100. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  101. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  102. package/src/__tests__/session-agent-loop.test.ts +19 -15
  103. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  104. package/src/__tests__/session-error.test.ts +124 -2
  105. package/src/__tests__/session-history-web-search.test.ts +918 -0
  106. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  107. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  108. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  109. package/src/__tests__/session-queue.test.ts +37 -27
  110. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  111. package/src/__tests__/session-slash-known.test.ts +1 -15
  112. package/src/__tests__/session-slash-queue.test.ts +1 -15
  113. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  114. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  115. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  116. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  117. package/src/__tests__/skill-include-graph.test.ts +66 -0
  118. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  119. package/src/__tests__/skill-load-tool.test.ts +149 -1
  120. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  121. package/src/__tests__/skills-install-extract.test.ts +93 -0
  122. package/src/__tests__/skills-uninstall.test.ts +1 -1
  123. package/src/__tests__/skills.test.ts +3 -3
  124. package/src/__tests__/skillssh-registry.test.ts +451 -0
  125. package/src/__tests__/slack-channel-config.test.ts +67 -3
  126. package/src/__tests__/slack-share-routes.test.ts +17 -19
  127. package/src/__tests__/system-prompt.test.ts +0 -1
  128. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  129. package/src/__tests__/terminal-tools.test.ts +4 -3
  130. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  131. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  132. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  133. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  134. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  135. package/src/__tests__/tool-executor.test.ts +0 -1
  136. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  137. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  138. package/src/__tests__/trust-store.test.ts +7 -13
  139. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  140. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  141. package/src/__tests__/twilio-routes.test.ts +0 -16
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  145. package/src/agent/ax-tree-compaction.test.ts +286 -0
  146. package/src/agent/loop.ts +104 -131
  147. package/src/approvals/AGENTS.md +1 -1
  148. package/src/approvals/guardian-request-resolvers.ts +14 -2
  149. package/src/bundler/compiler-tools.ts +66 -2
  150. package/src/calls/call-domain.ts +133 -6
  151. package/src/calls/call-store.ts +6 -0
  152. package/src/calls/relay-server.ts +52 -18
  153. package/src/calls/relay-setup-router.ts +17 -1
  154. package/src/calls/twilio-config.ts +3 -8
  155. package/src/calls/twilio-routes.ts +1 -2
  156. package/src/calls/types.ts +3 -1
  157. package/src/calls/voice-ingress-preflight.ts +1 -1
  158. package/src/cli/commands/browser-relay.ts +18 -12
  159. package/src/cli/commands/completions.ts +0 -3
  160. package/src/cli/commands/credentials.ts +101 -15
  161. package/src/cli/commands/doctor.ts +4 -3
  162. package/src/cli/commands/mcp.ts +46 -59
  163. package/src/cli/commands/memory.ts +16 -165
  164. package/src/cli/commands/oauth/apps.ts +284 -0
  165. package/src/cli/commands/oauth/connections.ts +633 -0
  166. package/src/cli/commands/oauth/index.ts +52 -0
  167. package/src/cli/commands/oauth/providers.ts +256 -0
  168. package/src/cli/commands/sessions.ts +5 -2
  169. package/src/cli/commands/skills.ts +177 -339
  170. package/src/cli/http-client.ts +0 -20
  171. package/src/cli/main-screen.tsx +2 -2
  172. package/src/cli/program.ts +6 -11
  173. package/src/cli/reference.ts +1 -3
  174. package/src/cli.ts +4 -10
  175. package/src/config/assistant-feature-flags.ts +0 -3
  176. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  177. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  178. package/src/config/bundled-skills/computer-use/TOOLS.json +23 -5
  179. package/src/config/bundled-skills/computer-use/tools/{computer-use-request-control.ts → computer-use-observe.ts} +1 -5
  180. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  181. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  182. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  183. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  184. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  185. package/src/config/bundled-tool-registry.ts +2 -5
  186. package/src/config/env-registry.ts +14 -83
  187. package/src/config/env.ts +11 -50
  188. package/src/config/feature-flag-registry.json +16 -16
  189. package/src/config/loader.ts +0 -6
  190. package/src/config/schema.ts +4 -13
  191. package/src/config/schemas/memory-lifecycle.ts +0 -9
  192. package/src/config/schemas/memory-processing.ts +0 -180
  193. package/src/config/schemas/memory-retrieval.ts +32 -104
  194. package/src/config/schemas/memory.ts +0 -10
  195. package/src/config/skills.ts +21 -2
  196. package/src/config/types.ts +0 -4
  197. package/src/context/image-dimensions.ts +229 -0
  198. package/src/context/token-estimator.ts +75 -12
  199. package/src/context/window-manager.ts +53 -11
  200. package/src/daemon/assistant-attachments.ts +1 -13
  201. package/src/daemon/config-watcher.ts +61 -3
  202. package/src/daemon/daemon-control.ts +1 -1
  203. package/src/daemon/date-context.ts +114 -31
  204. package/src/daemon/handlers/config-ingress.ts +8 -33
  205. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  206. package/src/daemon/handlers/config-telegram.ts +32 -16
  207. package/src/daemon/handlers/sessions.ts +27 -36
  208. package/src/daemon/handlers/shared.ts +0 -130
  209. package/src/daemon/handlers/skills.ts +20 -1
  210. package/src/daemon/history-repair.ts +72 -8
  211. package/src/daemon/host-cu-proxy.ts +430 -0
  212. package/src/daemon/lifecycle.ts +67 -71
  213. package/src/daemon/mcp-reload-service.ts +2 -2
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/computer-use.ts +1 -129
  216. package/src/daemon/message-types/host-cu.ts +19 -0
  217. package/src/daemon/message-types/memory.ts +4 -16
  218. package/src/daemon/message-types/messages.ts +4 -0
  219. package/src/daemon/message-types/sessions.ts +4 -0
  220. package/src/daemon/server.ts +25 -21
  221. package/src/daemon/session-agent-loop-handlers.ts +40 -0
  222. package/src/daemon/session-agent-loop.ts +334 -48
  223. package/src/daemon/session-attachments.ts +1 -2
  224. package/src/daemon/session-error.ts +89 -6
  225. package/src/daemon/session-history.ts +17 -7
  226. package/src/daemon/session-media-retry.ts +6 -2
  227. package/src/daemon/session-memory.ts +69 -149
  228. package/src/daemon/session-process.ts +10 -1
  229. package/src/daemon/session-runtime-assembly.ts +49 -19
  230. package/src/daemon/session-slash.ts +1 -1
  231. package/src/daemon/session-surfaces.ts +43 -28
  232. package/src/daemon/session-tool-setup.ts +9 -10
  233. package/src/daemon/session.ts +150 -17
  234. package/src/daemon/tool-side-effects.ts +2 -8
  235. package/src/daemon/watch-handler.ts +2 -2
  236. package/src/events/tool-metrics-listener.ts +2 -2
  237. package/src/hooks/manager.ts +1 -4
  238. package/src/inbound/public-ingress-urls.ts +7 -7
  239. package/src/instrument.ts +61 -1
  240. package/src/logfire.ts +16 -5
  241. package/src/memory/admin.ts +2 -191
  242. package/src/memory/canonical-guardian-store.ts +38 -2
  243. package/src/memory/conversation-crud.ts +0 -33
  244. package/src/memory/conversation-key-store.ts +21 -0
  245. package/src/memory/conversation-queries.ts +22 -3
  246. package/src/memory/db-init.ts +32 -0
  247. package/src/memory/embedding-backend.ts +84 -8
  248. package/src/memory/embedding-types.ts +9 -1
  249. package/src/memory/indexer.ts +7 -46
  250. package/src/memory/items-extractor.ts +274 -76
  251. package/src/memory/job-handlers/backfill.ts +2 -127
  252. package/src/memory/job-handlers/cleanup.ts +2 -16
  253. package/src/memory/job-handlers/extraction.ts +2 -138
  254. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  255. package/src/memory/job-handlers/summarization.ts +3 -148
  256. package/src/memory/job-utils.ts +21 -59
  257. package/src/memory/jobs-store.ts +1 -159
  258. package/src/memory/jobs-worker.ts +9 -52
  259. package/src/memory/migrations/104-core-indexes.ts +3 -3
  260. package/src/memory/migrations/149-oauth-tables.ts +62 -0
  261. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  262. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  263. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  264. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  265. package/src/memory/migrations/154-drop-fts.ts +20 -0
  266. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  267. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  268. package/src/memory/migrations/index.ts +8 -0
  269. package/src/memory/qdrant-client.ts +148 -51
  270. package/src/memory/raw-query.ts +1 -1
  271. package/src/memory/retriever.test.ts +294 -273
  272. package/src/memory/retriever.ts +421 -645
  273. package/src/memory/schema/calls.ts +2 -0
  274. package/src/memory/schema/index.ts +1 -0
  275. package/src/memory/schema/memory-core.ts +3 -48
  276. package/src/memory/schema/oauth.ts +67 -0
  277. package/src/memory/search/formatting.ts +263 -176
  278. package/src/memory/search/lexical.ts +1 -254
  279. package/src/memory/search/ranking.ts +0 -455
  280. package/src/memory/search/semantic.ts +100 -14
  281. package/src/memory/search/staleness.ts +47 -0
  282. package/src/memory/search/tier-classifier.ts +21 -0
  283. package/src/memory/search/types.ts +15 -77
  284. package/src/memory/task-memory-cleanup.ts +4 -6
  285. package/src/messaging/provider.ts +4 -4
  286. package/src/messaging/providers/gmail/client.ts +82 -2
  287. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  288. package/src/messaging/providers/gmail/people-client.ts +10 -10
  289. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  290. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  291. package/src/messaging/registry.ts +2 -32
  292. package/src/notifications/copy-composer.ts +0 -5
  293. package/src/notifications/signal.ts +4 -5
  294. package/src/oauth/byo-connection.test.ts +133 -25
  295. package/src/oauth/byo-connection.ts +22 -6
  296. package/src/oauth/connect-orchestrator.ts +113 -57
  297. package/src/oauth/connect-types.ts +17 -23
  298. package/src/oauth/connection-resolver.ts +35 -11
  299. package/src/oauth/connection.ts +1 -1
  300. package/src/oauth/manual-token-connection.ts +104 -0
  301. package/src/oauth/oauth-store.ts +582 -0
  302. package/src/oauth/platform-connection.test.ts +29 -0
  303. package/src/oauth/platform-connection.ts +6 -5
  304. package/src/oauth/provider-behaviors.ts +124 -0
  305. package/src/oauth/scope-policy.ts +9 -2
  306. package/src/oauth/seed-providers.ts +167 -0
  307. package/src/oauth/token-persistence.ts +81 -77
  308. package/src/permissions/checker.ts +3 -3
  309. package/src/permissions/defaults.ts +1 -1
  310. package/src/permissions/prompter.ts +10 -1
  311. package/src/permissions/trust-store.ts +36 -1
  312. package/src/playbooks/playbook-compiler.ts +1 -1
  313. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  314. package/src/prompts/system-prompt.ts +46 -42
  315. package/src/providers/anthropic/client.ts +59 -20
  316. package/src/providers/retry.ts +1 -27
  317. package/src/providers/types.ts +7 -1
  318. package/src/runtime/AGENTS.md +9 -0
  319. package/src/runtime/auth/route-policy.ts +6 -6
  320. package/src/runtime/channel-reply-delivery.ts +0 -40
  321. package/src/runtime/gateway-client.ts +0 -7
  322. package/src/runtime/guardian-reply-router.ts +24 -22
  323. package/src/runtime/http-server.ts +10 -8
  324. package/src/runtime/http-types.ts +2 -2
  325. package/src/runtime/invite-redemption-service.ts +19 -1
  326. package/src/runtime/invite-service.ts +25 -0
  327. package/src/runtime/middleware/twilio-validation.ts +1 -11
  328. package/src/runtime/pending-interactions.ts +14 -12
  329. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  330. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  331. package/src/runtime/routes/conversation-routes.ts +81 -19
  332. package/src/runtime/routes/events-routes.ts +21 -11
  333. package/src/runtime/routes/host-cu-routes.ts +97 -0
  334. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  335. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  336. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  337. package/src/runtime/routes/log-export-routes.ts +126 -8
  338. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  339. package/src/runtime/routes/memory-item-routes.ts +503 -0
  340. package/src/runtime/routes/session-management-routes.ts +3 -3
  341. package/src/runtime/routes/settings-routes.ts +55 -48
  342. package/src/runtime/routes/surface-action-routes.ts +1 -1
  343. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  344. package/src/runtime/routes/watch-routes.ts +128 -0
  345. package/src/runtime/routes/workspace-routes.ts +2 -1
  346. package/src/schedule/integration-status.ts +10 -9
  347. package/src/security/credential-key.ts +0 -156
  348. package/src/security/keychain-broker-client.ts +22 -10
  349. package/src/security/oauth2.ts +1 -1
  350. package/src/security/secure-keys.ts +25 -3
  351. package/src/security/token-manager.ts +137 -64
  352. package/src/skills/catalog-install.ts +414 -0
  353. package/src/skills/include-graph.ts +32 -0
  354. package/src/skills/skillssh-registry.ts +503 -0
  355. package/src/telegram/bot-username.ts +2 -3
  356. package/src/tools/assets/search.ts +5 -1
  357. package/src/tools/browser/network-recorder.ts +1 -1
  358. package/src/tools/browser/network-recording-types.ts +1 -1
  359. package/src/tools/computer-use/definitions.ts +36 -11
  360. package/src/tools/computer-use/registry.ts +5 -6
  361. package/src/tools/credentials/broker.ts +1 -2
  362. package/src/tools/credentials/metadata-store.ts +17 -121
  363. package/src/tools/credentials/vault.ts +92 -167
  364. package/src/tools/memory/definitions.ts +4 -13
  365. package/src/tools/memory/handlers.test.ts +83 -103
  366. package/src/tools/memory/handlers.ts +50 -85
  367. package/src/tools/registry.ts +2 -7
  368. package/src/tools/schedule/create.ts +8 -1
  369. package/src/tools/schedule/update.ts +8 -1
  370. package/src/tools/skills/load.ts +85 -3
  371. package/src/tools/watch/watch-state.ts +0 -12
  372. package/src/util/logger.ts +7 -41
  373. package/src/util/platform.ts +9 -28
  374. package/src/watcher/providers/google-calendar.ts +2 -1
  375. package/src/__tests__/clarification-resolver.test.ts +0 -193
  376. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  377. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  378. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  379. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  380. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  381. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  382. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  383. package/src/__tests__/conflict-policy.test.ts +0 -269
  384. package/src/__tests__/conflict-store.test.ts +0 -372
  385. package/src/__tests__/contradiction-checker.test.ts +0 -361
  386. package/src/__tests__/entity-extractor.test.ts +0 -211
  387. package/src/__tests__/entity-search.test.ts +0 -1117
  388. package/src/__tests__/profile-compiler.test.ts +0 -392
  389. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  390. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  391. package/src/__tests__/session-profile-injection.test.ts +0 -557
  392. package/src/cli/commands/dev.ts +0 -129
  393. package/src/cli/commands/map.ts +0 -391
  394. package/src/cli/commands/oauth.ts +0 -77
  395. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  396. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  397. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  398. package/src/daemon/computer-use-session.ts +0 -1026
  399. package/src/daemon/ride-shotgun-handler.ts +0 -569
  400. package/src/daemon/session-conflict-gate.ts +0 -167
  401. package/src/daemon/session-dynamic-profile.ts +0 -77
  402. package/src/memory/clarification-resolver.ts +0 -417
  403. package/src/memory/conflict-intent.ts +0 -205
  404. package/src/memory/conflict-policy.ts +0 -127
  405. package/src/memory/conflict-store.ts +0 -410
  406. package/src/memory/contradiction-checker.ts +0 -508
  407. package/src/memory/entity-extractor.ts +0 -535
  408. package/src/memory/format-recall.ts +0 -47
  409. package/src/memory/fts-reconciler.ts +0 -165
  410. package/src/memory/job-handlers/conflict.ts +0 -200
  411. package/src/memory/profile-compiler.ts +0 -195
  412. package/src/memory/recall-cache.ts +0 -117
  413. package/src/memory/search/entity.ts +0 -535
  414. package/src/memory/search/query-expansion.test.ts +0 -70
  415. package/src/memory/search/query-expansion.ts +0 -118
  416. package/src/oauth/provider-base-urls.ts +0 -21
  417. package/src/oauth/provider-profiles.ts +0 -192
  418. package/src/prompts/computer-use-prompt.ts +0 -98
  419. package/src/runtime/routes/computer-use-routes.ts +0 -641
  420. package/src/runtime/routes/mcp-routes.ts +0 -20
  421. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  422. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  423. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Parses image dimensions from base64-encoded image data by reading binary headers.
3
+ * Supports PNG, JPEG, GIF, and WebP formats.
4
+ * Returns null if parsing fails for any reason (corrupt, truncated, unrecognized).
5
+ */
6
+ export function parseImageDimensions(
7
+ base64Data: string,
8
+ mediaType: string,
9
+ ): { width: number; height: number } | null {
10
+ try {
11
+ switch (mediaType) {
12
+ case "image/png":
13
+ return parsePng(base64Data);
14
+ case "image/jpeg":
15
+ return parseJpeg(base64Data);
16
+ case "image/gif":
17
+ return parseGif(base64Data);
18
+ case "image/webp":
19
+ return parseWebp(base64Data);
20
+ default:
21
+ return null;
22
+ }
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function decodeBase64Bytes(
29
+ base64Data: string,
30
+ maxBytes: number,
31
+ ): Buffer | null {
32
+ // Estimate how much base64 we need: every 4 base64 chars = 3 bytes
33
+ const charsNeeded = Math.ceil((maxBytes * 4) / 3);
34
+ const slice = base64Data.slice(0, charsNeeded + 4); // a little extra for padding
35
+ try {
36
+ const buf = Buffer.from(slice, "base64");
37
+ return buf.length > 0 ? buf : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function readUint32BE(buf: Buffer, offset: number): number {
44
+ if (offset + 4 > buf.length) return -1;
45
+ return buf.readUInt32BE(offset);
46
+ }
47
+
48
+ function readUint16BE(buf: Buffer, offset: number): number {
49
+ if (offset + 2 > buf.length) return -1;
50
+ return buf.readUInt16BE(offset);
51
+ }
52
+
53
+ function readUint16LE(buf: Buffer, offset: number): number {
54
+ if (offset + 2 > buf.length) return -1;
55
+ return buf.readUInt16LE(offset);
56
+ }
57
+
58
+ function readUint32LE(buf: Buffer, offset: number): number {
59
+ if (offset + 4 > buf.length) return -1;
60
+ return buf.readUInt32LE(offset);
61
+ }
62
+
63
+ function readUint24LE(buf: Buffer, offset: number): number {
64
+ if (offset + 3 > buf.length) return -1;
65
+ return buf[offset]! | (buf[offset + 1]! << 8) | (buf[offset + 2]! << 16);
66
+ }
67
+
68
+ function parsePng(
69
+ base64Data: string,
70
+ ): { width: number; height: number } | null {
71
+ const buf = decodeBase64Bytes(base64Data, 32);
72
+ if (!buf || buf.length < 24) return null;
73
+
74
+ // Validate PNG signature: 89 50 4E 47
75
+ if (
76
+ buf[0] !== 0x89 ||
77
+ buf[1] !== 0x50 ||
78
+ buf[2] !== 0x4e ||
79
+ buf[3] !== 0x47
80
+ ) {
81
+ return null;
82
+ }
83
+
84
+ const width = readUint32BE(buf, 16);
85
+ const height = readUint32BE(buf, 20);
86
+ if (width <= 0 || height <= 0) return null;
87
+
88
+ return { width, height };
89
+ }
90
+
91
+ function parseJpeg(
92
+ base64Data: string,
93
+ ): { width: number; height: number } | null {
94
+ // Scan up to 1 MiB to handle JPEGs with large EXIF/ICC metadata before the SOF marker
95
+ const buf = decodeBase64Bytes(base64Data, 1_048_576);
96
+ if (!buf || buf.length < 2) return null;
97
+
98
+ // Validate JPEG SOI marker
99
+ if (buf[0] !== 0xff || buf[1] !== 0xd8) return null;
100
+
101
+ let offset = 2;
102
+ while (offset < buf.length - 1) {
103
+ // Find next marker
104
+ if (buf[offset] !== 0xff) {
105
+ offset++;
106
+ continue;
107
+ }
108
+
109
+ // Skip padding 0xFF bytes
110
+ while (offset < buf.length && buf[offset] === 0xff) {
111
+ offset++;
112
+ }
113
+ if (offset >= buf.length) return null;
114
+
115
+ const marker = buf[offset]!;
116
+ offset++;
117
+
118
+ // Check for SOF markers: C0-CF excluding C4 (DHT) and CC (DAC)
119
+ if (
120
+ marker >= 0xc0 &&
121
+ marker <= 0xcf &&
122
+ marker !== 0xc4 &&
123
+ marker !== 0xcc
124
+ ) {
125
+ // SOF marker found: skip 2-byte length + 1-byte precision
126
+ if (offset + 7 > buf.length) return null;
127
+ const height = readUint16BE(buf, offset + 3);
128
+ const width = readUint16BE(buf, offset + 5);
129
+ if (width <= 0 || height <= 0) return null;
130
+ return { width, height };
131
+ }
132
+
133
+ // Skip this marker's payload
134
+ if (offset + 1 >= buf.length) return null;
135
+ const segmentLength = readUint16BE(buf, offset);
136
+ if (segmentLength < 2) return null;
137
+ offset += segmentLength;
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ function parseGif(
144
+ base64Data: string,
145
+ ): { width: number; height: number } | null {
146
+ const buf = decodeBase64Bytes(base64Data, 12);
147
+ if (!buf || buf.length < 10) return null;
148
+
149
+ // Validate GIF signature: 47 49 46 38 (GIF8)
150
+ if (
151
+ buf[0] !== 0x47 ||
152
+ buf[1] !== 0x49 ||
153
+ buf[2] !== 0x46 ||
154
+ buf[3] !== 0x38
155
+ ) {
156
+ return null;
157
+ }
158
+
159
+ const width = readUint16LE(buf, 6);
160
+ const height = readUint16LE(buf, 8);
161
+ if (width <= 0 || height <= 0) return null;
162
+
163
+ return { width, height };
164
+ }
165
+
166
+ function parseWebp(
167
+ base64Data: string,
168
+ ): { width: number; height: number } | null {
169
+ const buf = decodeBase64Bytes(base64Data, 32);
170
+ if (!buf || buf.length < 16) return null;
171
+
172
+ // Validate RIFF signature
173
+ if (
174
+ buf[0] !== 0x52 ||
175
+ buf[1] !== 0x49 ||
176
+ buf[2] !== 0x46 ||
177
+ buf[3] !== 0x46
178
+ ) {
179
+ return null;
180
+ }
181
+ // Validate WEBP signature at offset 8
182
+ if (
183
+ buf[8] !== 0x57 ||
184
+ buf[9] !== 0x45 ||
185
+ buf[10] !== 0x42 ||
186
+ buf[11] !== 0x50
187
+ ) {
188
+ return null;
189
+ }
190
+
191
+ // Identify sub-format at offset 12
192
+ const subFormat =
193
+ String.fromCharCode(buf[12]!) +
194
+ String.fromCharCode(buf[13]!) +
195
+ String.fromCharCode(buf[14]!) +
196
+ String.fromCharCode(buf[15]!);
197
+
198
+ if (subFormat === "VP8 ") {
199
+ // VP8 lossy
200
+ if (buf.length < 30) return null;
201
+ const width = readUint16LE(buf, 26) & 0x3fff;
202
+ const height = readUint16LE(buf, 28) & 0x3fff;
203
+ if (width <= 0 || height <= 0) return null;
204
+ return { width, height };
205
+ }
206
+
207
+ if (subFormat === "VP8L") {
208
+ // VP8L lossless — validate signature byte 0x2f at offset 20
209
+ if (buf.length < 25) return null;
210
+ if (buf[20] !== 0x2f) return null;
211
+ const bits = readUint32LE(buf, 21);
212
+ if (bits < 0) return null;
213
+ const width = (bits & 0x3fff) + 1;
214
+ const height = ((bits >> 14) & 0x3fff) + 1;
215
+ if (width <= 0 || height <= 0) return null;
216
+ return { width, height };
217
+ }
218
+
219
+ if (subFormat === "VP8X") {
220
+ // VP8X extended
221
+ if (buf.length < 30) return null;
222
+ const width = readUint24LE(buf, 24) + 1;
223
+ const height = readUint24LE(buf, 27) + 1;
224
+ if (width <= 0 || height <= 0) return null;
225
+ return { width, height };
226
+ }
227
+
228
+ return null;
229
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ContentBlock, Message } from "../providers/types.js";
2
+ import { parseImageDimensions } from "./image-dimensions.js";
2
3
 
3
4
  const CHARS_PER_TOKEN = 4;
4
5
  const MESSAGE_OVERHEAD_TOKENS = 4;
@@ -12,6 +13,22 @@ const OTHER_BLOCK_TOKENS = 16;
12
13
  const SYSTEM_PROMPT_OVERHEAD_TOKENS = 8;
13
14
  const GEMINI_INLINE_FILE_MIME_TYPES = new Set(["application/pdf"]);
14
15
 
16
+ // Anthropic scales images to fit within 1568x1568 maintaining aspect ratio,
17
+ // then charges ~(width * height) / 750 tokens.
18
+ const ANTHROPIC_IMAGE_MAX_DIMENSION = 1568;
19
+ const ANTHROPIC_IMAGE_TOKENS_PER_PIXEL = 1 / 750;
20
+ const ANTHROPIC_IMAGE_MAX_TOKENS = Math.ceil(
21
+ ANTHROPIC_IMAGE_MAX_DIMENSION *
22
+ ANTHROPIC_IMAGE_MAX_DIMENSION *
23
+ ANTHROPIC_IMAGE_TOKENS_PER_PIXEL,
24
+ ); // ~3,277 tokens
25
+
26
+ // Anthropic renders each PDF page as an image (~1,568 tokens at standard
27
+ // resolution) plus any extracted text. Typical PDF pages are 50-150 KB.
28
+ // Using ~100 KB/page and ~1,600 tokens/page gives ~0.016 tokens/byte.
29
+ const ANTHROPIC_PDF_TOKENS_PER_BYTE = 0.016;
30
+ const ANTHROPIC_PDF_MIN_TOKENS = 1600; // At least one page
31
+
15
32
  export interface TokenEstimatorOptions {
16
33
  providerName?: string;
17
34
  }
@@ -21,21 +38,69 @@ export function estimateTextTokens(text: string): number {
21
38
  return Math.ceil(text.length / CHARS_PER_TOKEN);
22
39
  }
23
40
 
24
- function shouldCountFileSourceData(
41
+ function estimateAnthropicPdfTokens(base64Data: string): number {
42
+ const rawBytes = Math.ceil((base64Data.length * 3) / 4);
43
+ return Math.max(
44
+ ANTHROPIC_PDF_MIN_TOKENS,
45
+ Math.ceil(rawBytes * ANTHROPIC_PDF_TOKENS_PER_BYTE),
46
+ );
47
+ }
48
+
49
+ function estimateFileDataTokens(
25
50
  block: Extract<ContentBlock, { type: "file" }>,
26
51
  options?: TokenEstimatorOptions,
27
- ): boolean {
28
- if (options?.providerName !== "gemini") {
29
- return false;
52
+ ): number {
53
+ const providerName = options?.providerName;
54
+
55
+ // Anthropic sends PDFs as native document blocks and renders each page as an image
56
+ if (
57
+ providerName === "anthropic" &&
58
+ block.source.media_type === "application/pdf"
59
+ ) {
60
+ return estimateAnthropicPdfTokens(block.source.data);
61
+ }
62
+
63
+ // Gemini sends certain file types inline as base64
64
+ if (
65
+ providerName === "gemini" &&
66
+ GEMINI_INLINE_FILE_MIME_TYPES.has(block.source.media_type)
67
+ ) {
68
+ return estimateTextTokens(block.source.data);
30
69
  }
31
- return GEMINI_INLINE_FILE_MIME_TYPES.has(block.source.media_type);
70
+
71
+ return 0;
72
+ }
73
+
74
+ function estimateAnthropicImageTokens(width: number, height: number): number {
75
+ // Scale down to fit within 1568x1568 bounding box, maintaining aspect ratio
76
+ const scale = Math.min(
77
+ 1,
78
+ ANTHROPIC_IMAGE_MAX_DIMENSION / Math.max(width, height),
79
+ );
80
+ const scaledWidth = Math.round(width * scale);
81
+ const scaledHeight = Math.round(height * scale);
82
+ return Math.max(
83
+ IMAGE_BLOCK_TOKENS, // minimum 1024
84
+ Math.ceil(scaledWidth * scaledHeight * ANTHROPIC_IMAGE_TOKENS_PER_PIXEL),
85
+ );
32
86
  }
33
87
 
34
- function estimateImageSourceDataTokens(
88
+ function estimateImageTokens(
35
89
  block: Extract<ContentBlock, { type: "image" }>,
90
+ options?: TokenEstimatorOptions,
36
91
  ): number {
37
- // Image payloads are carried inline as base64 for all currently supported
38
- // providers, so estimator must scale with payload size (not fixed per image).
92
+ if (options?.providerName === "anthropic") {
93
+ const dims = parseImageDimensions(
94
+ block.source.data,
95
+ block.source.media_type,
96
+ );
97
+ if (dims) {
98
+ return estimateAnthropicImageTokens(dims.width, dims.height);
99
+ }
100
+ // Fallback: if dimensions can't be parsed, use Anthropic's max
101
+ return ANTHROPIC_IMAGE_MAX_TOKENS;
102
+ }
103
+ // Non-Anthropic: keep existing base64-size heuristic
39
104
  return estimateTextTokens(block.source.data);
40
105
  }
41
106
 
@@ -69,16 +134,14 @@ export function estimateContentBlockTokens(
69
134
  IMAGE_BLOCK_TOKENS,
70
135
  IMAGE_BLOCK_OVERHEAD_TOKENS +
71
136
  estimateTextTokens(block.source.media_type) +
72
- estimateImageSourceDataTokens(block),
137
+ estimateImageTokens(block, options),
73
138
  );
74
139
  case "file":
75
140
  return (
76
141
  FILE_BLOCK_OVERHEAD_TOKENS +
77
142
  estimateTextTokens(block.source.filename) +
78
143
  estimateTextTokens(block.source.media_type) +
79
- (shouldCountFileSourceData(block, options)
80
- ? estimateTextTokens(block.source.data)
81
- : 0) +
144
+ estimateFileDataTokens(block, options) +
82
145
  estimateTextTokens(block.extracted_text ?? "")
83
146
  );
84
147
  case "thinking":
@@ -83,21 +83,44 @@ export interface ContextWindowCompactOptions {
83
83
 
84
84
  export interface ContextWindowManagerOptions {
85
85
  provider: Provider;
86
- systemPrompt: string;
86
+ systemPrompt: string | (() => string);
87
87
  config: ContextWindowConfig;
88
88
  }
89
89
 
90
90
  export class ContextWindowManager {
91
91
  private readonly provider: Provider;
92
- private readonly systemPrompt: string;
92
+ private readonly _systemPrompt: string | (() => string);
93
93
  private readonly config: ContextWindowConfig;
94
+ /**
95
+ * Cached resolved system prompt. Lazily populated on first access via the
96
+ * `systemPrompt` getter and cleared after each compaction pass so the next
97
+ * pass picks up any prompt changes.
98
+ */
99
+ private _resolvedSystemPrompt: string | undefined;
94
100
 
95
101
  constructor(options: ContextWindowManagerOptions) {
96
102
  this.provider = options.provider;
97
- this.systemPrompt = options.systemPrompt;
103
+ this._systemPrompt = options.systemPrompt;
98
104
  this.config = options.config;
99
105
  }
100
106
 
107
+ /** Lazily resolve and cache the system prompt for the duration of a compaction pass. */
108
+ private get systemPrompt(): string {
109
+ if (this._resolvedSystemPrompt !== undefined) {
110
+ return this._resolvedSystemPrompt;
111
+ }
112
+ const resolved =
113
+ typeof this._systemPrompt === "function"
114
+ ? this._systemPrompt()
115
+ : this._systemPrompt;
116
+ this._resolvedSystemPrompt = resolved;
117
+ return resolved;
118
+ }
119
+
120
+ private clearSystemPromptCache(): void {
121
+ this._resolvedSystemPrompt = undefined;
122
+ }
123
+
101
124
  /**
102
125
  * Cheap pre-check: returns whether the estimated token count exceeds
103
126
  * the compaction threshold, along with the estimated token count so
@@ -106,19 +129,35 @@ export class ContextWindowManager {
106
129
  */
107
130
  shouldCompact(messages: Message[]): ShouldCompactResult {
108
131
  if (!this.config.enabled) return { needed: false, estimatedTokens: 0 };
109
- const estimated = estimatePromptTokens(messages, this.systemPrompt, {
110
- providerName: this.provider.name,
111
- });
112
- const threshold = Math.floor(
113
- this.config.maxInputTokens * this.config.compactThreshold,
114
- );
115
- return { needed: estimated >= threshold, estimatedTokens: estimated };
132
+ try {
133
+ const estimated = estimatePromptTokens(messages, this.systemPrompt, {
134
+ providerName: this.provider.name,
135
+ });
136
+ const threshold = Math.floor(
137
+ this.config.maxInputTokens * this.config.compactThreshold,
138
+ );
139
+ return { needed: estimated >= threshold, estimatedTokens: estimated };
140
+ } finally {
141
+ this.clearSystemPromptCache();
142
+ }
116
143
  }
117
144
 
118
145
  async maybeCompact(
119
146
  messages: Message[],
120
147
  signal?: AbortSignal,
121
148
  options?: ContextWindowCompactOptions,
149
+ ): Promise<ContextWindowResult> {
150
+ try {
151
+ return await this._maybeCompact(messages, signal, options);
152
+ } finally {
153
+ this.clearSystemPromptCache();
154
+ }
155
+ }
156
+
157
+ private async _maybeCompact(
158
+ messages: Message[],
159
+ signal?: AbortSignal,
160
+ options?: ContextWindowCompactOptions,
122
161
  ): Promise<ContextWindowResult> {
123
162
  const previousEstimatedInputTokens =
124
163
  options?.precomputedEstimate ??
@@ -632,7 +671,10 @@ function countPersistedMessages(messages: Message[]): number {
632
671
  function isToolResultOnly(message: Message): boolean {
633
672
  return (
634
673
  message.content.length > 0 &&
635
- message.content.every((block) => block.type === "tool_result")
674
+ message.content.every(
675
+ (block) =>
676
+ block.type === "tool_result" || block.type === "web_search_tool_result",
677
+ )
636
678
  );
637
679
  }
638
680
 
@@ -17,9 +17,6 @@ import {
17
17
  // Constants
18
18
  // ---------------------------------------------------------------------------
19
19
 
20
- /** Maximum number of attachments the assistant may emit per turn. */
21
- export const MAX_ASSISTANT_ATTACHMENTS = 5;
22
-
23
20
  /** Maximum size in bytes for a single assistant attachment (20 MB). */
24
21
  export const MAX_ASSISTANT_ATTACHMENT_BYTES = 20 * 1024 * 1024;
25
22
 
@@ -122,10 +119,9 @@ export interface ValidatedDrafts {
122
119
  }
123
120
 
124
121
  /**
125
- * Enforce per-turn attachment caps.
122
+ * Enforce per-attachment size cap.
126
123
  *
127
124
  * - Rejects individual drafts that exceed `MAX_ASSISTANT_ATTACHMENT_BYTES`.
128
- * - Truncates the list at `MAX_ASSISTANT_ATTACHMENTS`.
129
125
  */
130
126
  export function validateDrafts(
131
127
  drafts: AssistantAttachmentDraft[],
@@ -144,14 +140,6 @@ export function validateDrafts(
144
140
  continue;
145
141
  }
146
142
 
147
- if (accepted.length >= MAX_ASSISTANT_ATTACHMENTS) {
148
- warnings.push(
149
- `Skipped attachment "${draft.filename}": ` +
150
- `exceeded maximum of ${MAX_ASSISTANT_ATTACHMENTS} attachments per turn.`,
151
- );
152
- continue;
153
- }
154
-
155
143
  accepted.push(draft);
156
144
  }
157
145
 
@@ -3,7 +3,13 @@
3
3
  * Watches workspace files (config, prompts), protected directory
4
4
  * (trust rules, secret allowlist), and skills directories for changes.
5
5
  */
6
- import { existsSync, type FSWatcher, readdirSync, watch } from "node:fs";
6
+ import {
7
+ existsSync,
8
+ type FSWatcher,
9
+ mkdirSync,
10
+ readdirSync,
11
+ watch,
12
+ } from "node:fs";
7
13
  import { join } from "node:path";
8
14
 
9
15
  import { getConfig, invalidateConfigCache } from "../config/loader.js";
@@ -98,8 +104,14 @@ export class ConfigWatcher {
98
104
  * Start all file watchers. `onSessionEvict` is called when watched
99
105
  * files change and sessions need to be evicted for reload.
100
106
  * `onIdentityChanged` is called when IDENTITY.md changes on disk.
107
+ * `onMcpReload` is called when the MCP section of config.json changes
108
+ * or when a signal file appears in the workspace `signals/` directory.
101
109
  */
102
- start(onSessionEvict: () => void, onIdentityChanged?: () => void): void {
110
+ start(
111
+ onSessionEvict: () => void,
112
+ onIdentityChanged?: () => void,
113
+ onMcpReload?: () => void,
114
+ ): void {
103
115
  const workspaceDir = getWorkspaceDir();
104
116
  const protectedDir = join(getRootDir(), "protected");
105
117
 
@@ -107,8 +119,17 @@ export class ConfigWatcher {
107
119
  "config.json": () => {
108
120
  if (this.suppressReload) return;
109
121
  try {
122
+ const prevConfig = getConfig();
123
+ const prevMcpFingerprint = JSON.stringify(prevConfig.mcp ?? {});
110
124
  const changed = this.refreshConfigFromSources();
111
- if (changed) onSessionEvict();
125
+ if (changed) {
126
+ onSessionEvict();
127
+ const newConfig = getConfig();
128
+ const newMcpFingerprint = JSON.stringify(newConfig.mcp ?? {});
129
+ if (newMcpFingerprint !== prevMcpFingerprint) {
130
+ onMcpReload?.();
131
+ }
132
+ }
112
133
  } catch (err) {
113
134
  log.error(
114
135
  { err, configPath: join(workspaceDir, "config.json") },
@@ -185,6 +206,7 @@ export class ConfigWatcher {
185
206
  );
186
207
  }
187
208
 
209
+ this.startSignalsWatcher(onMcpReload);
188
210
  this.startSkillsWatchers(onSessionEvict);
189
211
  }
190
212
 
@@ -196,6 +218,42 @@ export class ConfigWatcher {
196
218
  this.watchers = [];
197
219
  }
198
220
 
221
+ private startSignalsWatcher(onMcpReload?: () => void): void {
222
+ const signalsDir = join(getWorkspaceDir(), "signals");
223
+ try {
224
+ if (!existsSync(signalsDir)) {
225
+ mkdirSync(signalsDir, { recursive: true });
226
+ }
227
+ } catch {
228
+ // If we can't create it, watching will also fail — handled below.
229
+ }
230
+
231
+ const signalHandlers: Record<string, () => void> = {
232
+ "mcp-reload": () => {
233
+ onMcpReload?.();
234
+ },
235
+ };
236
+
237
+ try {
238
+ const watcher = watch(signalsDir, (_eventType, filename) => {
239
+ if (!filename) return;
240
+ const file = String(filename);
241
+ if (!signalHandlers[file]) return;
242
+ this.debounceTimers.schedule(`signal:${file}`, () => {
243
+ log.info({ file }, "Signal file detected");
244
+ signalHandlers[file]();
245
+ });
246
+ });
247
+ this.watchers.push(watcher);
248
+ log.info({ dir: signalsDir }, "Watching signals directory");
249
+ } catch (err) {
250
+ log.warn(
251
+ { err, dir: signalsDir },
252
+ "Failed to watch signals directory. Signal-based reload will be unavailable.",
253
+ );
254
+ }
255
+ }
256
+
199
257
  private startSkillsWatchers(onSessionEvict: () => void): void {
200
258
  const skillsDir = getWorkspaceSkillsDir();
201
259
  if (!existsSync(skillsDir)) return;
@@ -143,7 +143,7 @@ function healthCheckHost(host: string): string {
143
143
  /** Hit the daemon's HTTP /healthz endpoint. Returns true if it responds
144
144
  * with HTTP 200 within the timeout — false on connection refused, timeout,
145
145
  * or any other error. */
146
- async function isHttpHealthy(): Promise<boolean> {
146
+ export async function isHttpHealthy(): Promise<boolean> {
147
147
  const host = healthCheckHost(getRuntimeHttpHost());
148
148
  const port = getRuntimeHttpPort();
149
149
  try {