@vellumai/assistant 0.8.2 → 0.8.4

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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -240,6 +240,99 @@ export function deleteDocument(surfaceId: string): boolean {
240
240
  }
241
241
  }
242
242
 
243
+ // ---------------------------------------------------------------------------
244
+ // In-document search (grep)
245
+ // ---------------------------------------------------------------------------
246
+
247
+ export interface FindMatch {
248
+ lineNumber: number;
249
+ lineContent: string;
250
+ matchStart: number;
251
+ matchEnd: number;
252
+ matchText: string;
253
+ }
254
+
255
+ export interface FindResult {
256
+ surfaceId: string;
257
+ totalMatches: number;
258
+ matches: FindMatch[];
259
+ }
260
+
261
+ const MAX_FIND_MATCHES = 50;
262
+
263
+ /**
264
+ * Search for text or a regex pattern within a document's content.
265
+ * Returns matching lines with line numbers and match positions.
266
+ * Results are capped at {@link MAX_FIND_MATCHES} to avoid oversized responses.
267
+ */
268
+ export function findInDocument(
269
+ surfaceId: string,
270
+ query: string,
271
+ options: { regex?: boolean; caseSensitive?: boolean } = {},
272
+ ): FindResult | null {
273
+ try {
274
+ const row = rawGet<{ content: string }>(
275
+ /*sql*/ `SELECT content FROM documents WHERE surface_id = ?`,
276
+ surfaceId,
277
+ );
278
+ if (!row) return null;
279
+
280
+ const lines = row.content.split("\n");
281
+ const matches: FindMatch[] = [];
282
+ let uncappedTotal = 0;
283
+
284
+ if (options.regex) {
285
+ const flags = options.caseSensitive ? "g" : "gi";
286
+ const re = new RegExp(query, flags);
287
+ for (let i = 0; i < lines.length; i++) {
288
+ const line = lines[i];
289
+ re.lastIndex = 0;
290
+ let m: RegExpExecArray | null;
291
+ while ((m = re.exec(line)) !== null) {
292
+ uncappedTotal++;
293
+ if (matches.length < MAX_FIND_MATCHES) {
294
+ matches.push({
295
+ lineNumber: i + 1,
296
+ lineContent: line,
297
+ matchStart: m.index,
298
+ matchEnd: m.index + m[0].length,
299
+ matchText: m[0],
300
+ });
301
+ }
302
+ if (m[0].length === 0) re.lastIndex++;
303
+ }
304
+ }
305
+ } else {
306
+ const needle = options.caseSensitive ? query : query.toLowerCase();
307
+ for (let i = 0; i < lines.length; i++) {
308
+ const line = lines[i];
309
+ const haystack = options.caseSensitive ? line : line.toLowerCase();
310
+ let startPos = 0;
311
+ while (startPos <= haystack.length) {
312
+ const idx = haystack.indexOf(needle, startPos);
313
+ if (idx === -1) break;
314
+ uncappedTotal++;
315
+ if (matches.length < MAX_FIND_MATCHES) {
316
+ matches.push({
317
+ lineNumber: i + 1,
318
+ lineContent: line,
319
+ matchStart: idx,
320
+ matchEnd: idx + needle.length,
321
+ matchText: line.slice(idx, idx + needle.length),
322
+ });
323
+ }
324
+ startPos = idx + Math.max(needle.length, 1);
325
+ }
326
+ }
327
+ }
328
+
329
+ return { surfaceId, totalMatches: uncappedTotal, matches };
330
+ } catch (error) {
331
+ log.error({ err: error, surfaceId }, "Find-in-document error");
332
+ return null;
333
+ }
334
+ }
335
+
243
336
  // ---------------------------------------------------------------------------
244
337
  // Document persistence
245
338
  // ---------------------------------------------------------------------------
@@ -297,6 +390,115 @@ export function saveDocument(params: {
297
390
  }
298
391
  }
299
392
 
393
+ // ---------------------------------------------------------------------------
394
+ // Find-and-replace
395
+ // ---------------------------------------------------------------------------
396
+
397
+ export interface ReplaceInDocumentOptions {
398
+ regex?: boolean;
399
+ caseSensitive?: boolean;
400
+ maxReplacements?: number;
401
+ }
402
+
403
+ export type ReplaceInDocumentResult =
404
+ | { success: true; replacements_made: number; content_changed: boolean }
405
+ | { success: false; error: string };
406
+
407
+ function escapeRegExpChars(str: string): string {
408
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
409
+ }
410
+
411
+ /**
412
+ * Find and replace text within a document — like sed.
413
+ * Supports literal text and regex patterns with optional backreferences.
414
+ */
415
+ export function replaceInDocument(
416
+ surfaceId: string,
417
+ find: string,
418
+ replace: string,
419
+ options: ReplaceInDocumentOptions = {},
420
+ ): ReplaceInDocumentResult {
421
+ try {
422
+ const row = rawGet<{ content: string }>(
423
+ /*sql*/ `SELECT content FROM documents WHERE surface_id = ?`,
424
+ surfaceId,
425
+ );
426
+ if (!row) {
427
+ return { success: false, error: "Document not found" };
428
+ }
429
+
430
+ const flags = "g" + (options.caseSensitive === true ? "" : "i");
431
+ const pattern = options.regex
432
+ ? new RegExp(find, flags)
433
+ : new RegExp(escapeRegExpChars(find), flags);
434
+
435
+ const totalMatches = [...row.content.matchAll(pattern)].length;
436
+ if (
437
+ totalMatches === 0 ||
438
+ (options.maxReplacements != null && options.maxReplacements <= 0)
439
+ ) {
440
+ return { success: true, replacements_made: 0, content_changed: false };
441
+ }
442
+
443
+ let newContent: string;
444
+ let replacementsMade: number;
445
+
446
+ if (
447
+ options.maxReplacements != null &&
448
+ options.maxReplacements < totalMatches
449
+ ) {
450
+ // Iterative replacement up to maxReplacements using manual exec loop
451
+ // so backreferences in the replacement string work correctly.
452
+ const limit = options.maxReplacements;
453
+ let count = 0;
454
+ let result = "";
455
+ let lastIndex = 0;
456
+ let m: RegExpExecArray | null;
457
+ while ((m = pattern.exec(row.content)) !== null) {
458
+ if (count >= limit) break;
459
+ result += row.content.slice(lastIndex, m.index);
460
+ const singleMatchPattern = new RegExp(
461
+ pattern.source,
462
+ pattern.flags.replace("g", ""),
463
+ );
464
+ result += m[0].replace(singleMatchPattern, replace);
465
+ lastIndex = m.index + m[0].length;
466
+ count++;
467
+ if (m[0].length === 0) pattern.lastIndex++;
468
+ }
469
+ result += row.content.slice(lastIndex);
470
+ newContent = result;
471
+ replacementsMade = count;
472
+ } else {
473
+ newContent = row.content.replace(pattern, replace);
474
+ replacementsMade = totalMatches;
475
+ }
476
+
477
+ const wordCount = newContent
478
+ .split(/\s+/)
479
+ .filter((w) => w.length > 0).length;
480
+ rawRun(
481
+ /*sql*/ `UPDATE documents SET content = ?, word_count = ?, updated_at = ? WHERE surface_id = ?`,
482
+ newContent,
483
+ wordCount,
484
+ Date.now(),
485
+ surfaceId,
486
+ );
487
+ log.info({ surfaceId, replacementsMade }, "Replaced text in document");
488
+ return {
489
+ success: true,
490
+ replacements_made: replacementsMade,
491
+ content_changed: true,
492
+ };
493
+ } catch (error) {
494
+ log.error({ err: error, surfaceId }, "Replace-in-document error");
495
+ return {
496
+ success: false,
497
+ error: error instanceof Error ? error.message : "Unknown error",
498
+ };
499
+ }
500
+ }
501
+
300
502
  /** Update persisted document content (append or replace). */
301
503
  export function updateDocumentContent(
302
504
  surfaceId: string,
@@ -0,0 +1,121 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ import { z } from "zod";
4
+
5
+ type TestMessage = {
6
+ id: string;
7
+ conversationId: string;
8
+ role: string;
9
+ content: string;
10
+ createdAt: number;
11
+ metadata: string | null;
12
+ };
13
+
14
+ const parentMessages: TestMessage[] = [
15
+ {
16
+ id: "msg-parent-1",
17
+ conversationId: "parent-conv",
18
+ role: "user",
19
+ content: JSON.stringify([{ type: "text", text: "go research foo" }]),
20
+ createdAt: 1_700_000_000_000,
21
+ metadata: null,
22
+ },
23
+ {
24
+ id: "msg-parent-2",
25
+ conversationId: "parent-conv",
26
+ role: "assistant",
27
+ content: JSON.stringify([{ type: "text", text: "spawning subagent" }]),
28
+ createdAt: 1_700_000_001_000,
29
+ metadata: JSON.stringify({
30
+ subagentNotification: {
31
+ subagentId: "sa-1",
32
+ label: "research-foo",
33
+ status: "completed",
34
+ conversationId: "child-conv-1",
35
+ },
36
+ }),
37
+ },
38
+ ];
39
+
40
+ const childMessages: TestMessage[] = [
41
+ {
42
+ id: "msg-child-1",
43
+ conversationId: "child-conv-1",
44
+ role: "user",
45
+ content: JSON.stringify([
46
+ { type: "text", text: "Objective: research foo and report back." },
47
+ ]),
48
+ createdAt: 1_700_000_002_000,
49
+ metadata: null,
50
+ },
51
+ {
52
+ id: "msg-child-2",
53
+ conversationId: "child-conv-1",
54
+ role: "assistant",
55
+ content: JSON.stringify([
56
+ { type: "text", text: "I found that foo is a bar." },
57
+ ]),
58
+ createdAt: 1_700_000_003_000,
59
+ metadata: null,
60
+ },
61
+ ];
62
+
63
+ const messageMetadataSchema = z.object({
64
+ subagentNotification: z
65
+ .object({
66
+ subagentId: z.string(),
67
+ label: z.string(),
68
+ status: z.enum(["running", "completed", "failed", "aborted"]),
69
+ conversationId: z.string().optional(),
70
+ })
71
+ .optional(),
72
+ });
73
+
74
+ mock.module("../../memory/conversation-crud.js", () => ({
75
+ getConversation: (_id: string) => null,
76
+ getMessages: (id: string) =>
77
+ id === "child-conv-1" ? childMessages : parentMessages,
78
+ messageMetadataSchema,
79
+ }));
80
+
81
+ mock.module("../../util/truncate.js", () => ({
82
+ truncate: (s: string) => s,
83
+ }));
84
+
85
+ mock.module("../../daemon/date-context.js", () => ({
86
+ formatLocalTimestamp: (_ts: number, _tz?: string) => "TIME",
87
+ }));
88
+
89
+ const { formatMessageSliceForTranscript } =
90
+ await import("../transcript-formatter.js");
91
+
92
+ describe("formatMessageSliceForTranscript subagent labels", () => {
93
+ test("embedded subagent transcripts render with generic role labels even when parent display names are provided", () => {
94
+ const out = formatMessageSliceForTranscript(parentMessages, {
95
+ assistantName: "Bob",
96
+ userName: "Alice",
97
+ });
98
+
99
+ // Parent messages use the provided display names.
100
+ expect(out).toContain("## Alice (TIME)");
101
+ expect(out).toContain("## Bob (TIME)");
102
+
103
+ // Subagent block headers must use generic labels — the child "user" message
104
+ // is actually the parent assistant's objective, so labeling it "Alice"
105
+ // would misattribute the assistant's tasking text to the human user.
106
+ expect(out).toContain("### Subagent: research-foo (completed)");
107
+ expect(out).toContain("> **User** (TIME)");
108
+ expect(out).toContain("> **Assistant** (TIME)");
109
+ expect(out).not.toContain("> **Alice**");
110
+ expect(out).not.toContain("> **Bob**");
111
+ });
112
+
113
+ test("without display-name options, parent and subagent both use generic labels", () => {
114
+ const out = formatMessageSliceForTranscript(parentMessages);
115
+
116
+ expect(out).toContain("## User (TIME)");
117
+ expect(out).toContain("## Assistant (TIME)");
118
+ expect(out).toContain("> **User** (TIME)");
119
+ expect(out).toContain("> **Assistant** (TIME)");
120
+ });
121
+ });
@@ -5,6 +5,7 @@
5
5
  * subagent conversation sections when present in message metadata.
6
6
  */
7
7
 
8
+ import { formatLocalTimestamp } from "../daemon/date-context.js";
8
9
  import {
9
10
  getConversation,
10
11
  getMessages,
@@ -23,10 +24,6 @@ interface ContentBlock {
23
24
  source?: { media_type?: string; filename?: string };
24
25
  }
25
26
 
26
- function formatTimestamp(ms: number): string {
27
- return new Date(ms).toISOString().replace("T", " ").slice(0, 19);
28
- }
29
-
30
27
  function extractAnalysisText(blocks: ContentBlock[]): string {
31
28
  const parts: string[] = [];
32
29
  for (const block of blocks) {
@@ -67,15 +64,36 @@ function extractAnalysisText(blocks: ContentBlock[]): string {
67
64
  return parts.join("\n");
68
65
  }
69
66
 
70
- function formatRole(role: string): string {
71
- return role === "user" ? "User" : "Assistant";
67
+ export interface TranscriptFormatOptions {
68
+ timeZone?: string;
69
+ assistantName?: string | null;
70
+ userName?: string | null;
71
+ }
72
+
73
+ function resolveName(
74
+ name: string | null | undefined,
75
+ fallback: string,
76
+ ): string {
77
+ return name && name.length > 0 ? name : fallback;
78
+ }
79
+
80
+ function formatRole(
81
+ role: string,
82
+ options: TranscriptFormatOptions = {},
83
+ ): string {
84
+ return role === "user"
85
+ ? resolveName(options.userName, "User")
86
+ : resolveName(options.assistantName, "Assistant");
72
87
  }
73
88
 
74
- function formatSubagentMessages(msgs: ReturnType<typeof getMessages>): string {
89
+ function formatSubagentMessages(
90
+ msgs: ReturnType<typeof getMessages>,
91
+ options: TranscriptFormatOptions = {},
92
+ ): string {
75
93
  const lines: string[] = [];
76
94
  for (const msg of msgs) {
77
- const role = formatRole(msg.role);
78
- const time = formatTimestamp(msg.createdAt);
95
+ const role = formatRole(msg.role, options);
96
+ const time = formatLocalTimestamp(msg.createdAt, options.timeZone);
79
97
  const content = parseContent(msg.content);
80
98
  const text = extractAnalysisText(content);
81
99
  if (text) {
@@ -103,24 +121,33 @@ type TranscriptMessage = ReturnType<typeof getMessages>[number];
103
121
  * Format a slice of messages as a transcript body (no top-of-conversation
104
122
  * header). Used by background jobs that process incremental slices — the
105
123
  * memory-retrospective job re-renders only the messages added since its
106
- * last successful run rather than the whole conversation. The format
107
- * matches `buildAnalysisTranscript` per message so downstream agents see a
108
- * consistent shape regardless of whether the input is a full transcript or
109
- * a slice.
124
+ * last successful run rather than the whole conversation. The per-message
125
+ * structural shape matches `buildAnalysisTranscript` (header line, body,
126
+ * optional subagent block) so downstream agents see consistent framing.
127
+ * The participant *labels*, however, intentionally diverge: this function
128
+ * honors `TranscriptFormatOptions` so the memory-retrospective prompt can
129
+ * render the conversation under the assistant and user display names,
130
+ * while `buildAnalysisTranscript` always uses generic "User"/"Assistant"
131
+ * labels for the analyze-conversation flow.
110
132
  */
111
133
  export function formatMessageSliceForTranscript(
112
134
  messages: TranscriptMessage[],
135
+ options: TranscriptFormatOptions = {},
113
136
  ): string {
114
137
  const lines: string[] = [];
115
138
  for (const msg of messages) {
116
- appendMessageBlock(lines, msg);
139
+ appendMessageBlock(lines, msg, options);
117
140
  }
118
141
  return lines.join("\n");
119
142
  }
120
143
 
121
- function appendMessageBlock(lines: string[], msg: TranscriptMessage): void {
122
- const role = formatRole(msg.role);
123
- const time = formatTimestamp(msg.createdAt);
144
+ function appendMessageBlock(
145
+ lines: string[],
146
+ msg: TranscriptMessage,
147
+ options: TranscriptFormatOptions = {},
148
+ ): void {
149
+ const role = formatRole(msg.role, options);
150
+ const time = formatLocalTimestamp(msg.createdAt, options.timeZone);
124
151
  const content = parseContent(msg.content);
125
152
  const text = extractAnalysisText(content);
126
153
 
@@ -142,7 +169,14 @@ function appendMessageBlock(lines: string[], msg: TranscriptMessage): void {
142
169
  const subMessages = getMessages(notif.conversationId);
143
170
  lines.push(`### Subagent: ${notif.label} (${notif.status})`);
144
171
  lines.push("");
145
- lines.push(formatSubagentMessages(subMessages));
172
+ // Subagent conversations persist the parent assistant's objective
173
+ // as a `user` message (see subagent/manager.ts), so reusing the
174
+ // parent's display-name options would render the assistant's
175
+ // tasking text under the human user's name. Keep child transcripts
176
+ // on generic role labels — and only pass through the time zone.
177
+ lines.push(
178
+ formatSubagentMessages(subMessages, { timeZone: options.timeZone }),
179
+ );
146
180
  lines.push("");
147
181
  }
148
182
  }
@@ -163,12 +197,12 @@ export function buildAnalysisTranscript(conversationId: string): string {
163
197
  const lines: string[] = [];
164
198
 
165
199
  lines.push(`# Conversation: ${title}`);
166
- lines.push(`Created: ${formatTimestamp(conversation.createdAt)}`);
200
+ lines.push(`Created: ${formatLocalTimestamp(conversation.createdAt)}`);
167
201
  lines.push("");
168
202
 
169
203
  for (const msg of allMessages) {
170
204
  const role = formatRole(msg.role);
171
- const time = formatTimestamp(msg.createdAt);
205
+ const time = formatLocalTimestamp(msg.createdAt);
172
206
  const content = parseContent(msg.content);
173
207
  const text = extractAnalysisText(content);
174
208
 
@@ -35,7 +35,6 @@ mock.module("../../util/platform.js", () => ({
35
35
  getEmbeddingModelsDir: () => join(workspaceDir ?? fallbackDir, "models"),
36
36
  getSandboxRootDir: () => join(workspaceDir ?? fallbackDir, "sandbox"),
37
37
  getSandboxWorkingDir: () => join(workspaceDir ?? fallbackDir, "sandbox/work"),
38
- getInterfacesDir: () => join(workspaceDir ?? fallbackDir, "interfaces"),
39
38
  getSoundsDir: () => join(workspaceDir ?? fallbackDir, "sounds"),
40
39
  getAvatarDir: () => join(workspaceDir ?? fallbackDir, "avatar"),
41
40
  AVATAR_IMAGE_FILENAME: "avatar-image.png",
@@ -55,6 +54,7 @@ const stubConfig: {
55
54
  activeHoursStart: number | null;
56
55
  activeHoursEnd: number | null;
57
56
  maxConsecutiveRuns: number | null;
57
+ disposition: string;
58
58
  };
59
59
  } = {
60
60
  heartbeat: {
@@ -63,6 +63,7 @@ const stubConfig: {
63
63
  activeHoursStart: null,
64
64
  activeHoursEnd: null,
65
65
  maxConsecutiveRuns: null,
66
+ disposition: "Default disposition text.",
66
67
  },
67
68
  };
68
69
  mock.module("../../config/loader.js", () => ({
@@ -395,6 +396,48 @@ describe("HeartbeatService", () => {
395
396
  }
396
397
  });
397
398
 
399
+ test("resetTimer() during an in-flight run is not undone by that run's increment", async () => {
400
+ // Regression: if a guardian message arrives mid-run, `resetTimer()`
401
+ // zeroes the counter but the in-flight run's `finally` block used to
402
+ // unconditionally `_consecutiveRuns++`, leaving the counter at 1 and
403
+ // tripping the cap-at-1 path one auto run too early.
404
+ stubConfig.heartbeat.maxConsecutiveRuns = 1;
405
+
406
+ let releaseInflight: () => void = () => {};
407
+ const inflight = new Promise<void>((resolve) => {
408
+ releaseInflight = resolve;
409
+ });
410
+ runBackgroundJobImpl = async () => {
411
+ await inflight;
412
+ return { conversationId: STUB_CONVERSATION_ID, ok: true };
413
+ };
414
+
415
+ const service = new HeartbeatService({ alerter: () => {} });
416
+ service.start();
417
+ try {
418
+ const runPromise = service.runOnce({ force: false });
419
+ // Guardian message arrives while the run is still executing.
420
+ service.resetTimer();
421
+ releaseInflight();
422
+ expect(await runPromise).toBe(true);
423
+
424
+ // The reset during the in-flight run must survive: the next auto run
425
+ // should proceed because the counter is still 0, not 1.
426
+ runBackgroundJobImpl = async () => ({
427
+ conversationId: STUB_CONVERSATION_ID,
428
+ ok: true,
429
+ });
430
+ expect(await service.runOnce({ force: false })).toBe(true);
431
+ expect(
432
+ skipHeartbeatRunCalls.some(
433
+ (c) => c.reason === "max_consecutive_runs",
434
+ ),
435
+ ).toBe(false);
436
+ } finally {
437
+ await service.stop();
438
+ }
439
+ });
440
+
398
441
  test("null disables the cap entirely", async () => {
399
442
  stubConfig.heartbeat.maxConsecutiveRuns = null;
400
443
  const service = new HeartbeatService({ alerter: () => {} });