@ouro.bot/cli 0.1.0-alpha.40 → 0.1.0-alpha.400

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 (333) hide show
  1. package/README.md +109 -14
  2. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
  3. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
  4. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  5. package/changelog.json +2455 -6
  6. package/dist/arc/attention-types.js +8 -0
  7. package/dist/arc/cares.js +140 -0
  8. package/dist/arc/episodes.js +117 -0
  9. package/dist/arc/intentions.js +133 -0
  10. package/dist/arc/json-store.js +117 -0
  11. package/dist/arc/obligations.js +237 -0
  12. package/dist/arc/packets.js +193 -0
  13. package/dist/arc/presence.js +185 -0
  14. package/dist/arc/task-lifecycle.js +65 -0
  15. package/dist/heart/active-work.js +832 -0
  16. package/dist/heart/agent-entry.js +58 -3
  17. package/dist/heart/attachments/image-normalize.js +194 -0
  18. package/dist/heart/attachments/materialize.js +97 -0
  19. package/dist/heart/attachments/originals.js +88 -0
  20. package/dist/heart/attachments/render.js +29 -0
  21. package/dist/heart/attachments/sources/adapter.js +2 -0
  22. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  23. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  24. package/dist/heart/attachments/sources/index.js +16 -0
  25. package/dist/heart/attachments/store.js +103 -0
  26. package/dist/heart/attachments/types.js +93 -0
  27. package/dist/heart/auth/auth-flow.js +417 -0
  28. package/dist/heart/bridges/manager.js +358 -0
  29. package/dist/heart/bridges/state-machine.js +135 -0
  30. package/dist/heart/bridges/store.js +123 -0
  31. package/dist/heart/bundle-state.js +168 -0
  32. package/dist/heart/commitments.js +111 -0
  33. package/dist/heart/config-registry.js +304 -0
  34. package/dist/heart/config.js +101 -128
  35. package/dist/heart/core.js +801 -217
  36. package/dist/heart/cross-chat-delivery.js +131 -0
  37. package/dist/heart/daemon/agent-config-check.js +397 -0
  38. package/dist/heart/daemon/agent-discovery.js +79 -3
  39. package/dist/heart/daemon/agent-service.js +360 -0
  40. package/dist/heart/daemon/agentic-repair.js +213 -0
  41. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  42. package/dist/heart/daemon/cadence.js +70 -0
  43. package/dist/heart/daemon/cli-defaults.js +599 -0
  44. package/dist/heart/daemon/cli-exec.js +3616 -0
  45. package/dist/heart/daemon/cli-help.js +396 -0
  46. package/dist/heart/daemon/cli-parse.js +1118 -0
  47. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  48. package/dist/heart/daemon/cli-render.js +560 -0
  49. package/dist/heart/daemon/cli-types.js +8 -0
  50. package/dist/heart/daemon/daemon-cli.js +28 -1582
  51. package/dist/heart/daemon/daemon-entry.js +356 -3
  52. package/dist/heart/daemon/daemon-health.js +141 -0
  53. package/dist/heart/daemon/daemon-runtime-sync.js +157 -12
  54. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  55. package/dist/heart/daemon/daemon.js +684 -58
  56. package/dist/heart/daemon/doctor-types.js +8 -0
  57. package/dist/heart/daemon/doctor.js +419 -0
  58. package/dist/heart/daemon/health-monitor.js +79 -1
  59. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  60. package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
  61. package/dist/heart/daemon/http-health-probe.js +80 -0
  62. package/dist/heart/daemon/inner-status.js +89 -0
  63. package/dist/heart/daemon/interactive-repair.js +209 -0
  64. package/dist/heart/daemon/launchd.js +46 -9
  65. package/dist/heart/daemon/log-tailer.js +82 -12
  66. package/dist/heart/daemon/logs-prune.js +105 -0
  67. package/dist/heart/daemon/message-router.js +17 -8
  68. package/dist/heart/daemon/os-cron-deps.js +134 -0
  69. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  70. package/dist/heart/daemon/ouro-entry.js +3 -1
  71. package/dist/heart/daemon/process-manager.js +201 -0
  72. package/dist/heart/daemon/provider-discovery.js +137 -0
  73. package/dist/heart/daemon/pulse.js +475 -0
  74. package/dist/heart/daemon/readiness-repair.js +216 -0
  75. package/dist/heart/daemon/run-hooks.js +2 -0
  76. package/dist/heart/daemon/runtime-logging.js +67 -16
  77. package/dist/heart/daemon/runtime-metadata.js +73 -0
  78. package/dist/heart/daemon/runtime-mode.js +67 -0
  79. package/dist/heart/daemon/safe-mode.js +161 -0
  80. package/dist/heart/daemon/sense-manager.js +119 -30
  81. package/dist/heart/daemon/session-id-resolver.js +131 -0
  82. package/dist/heart/daemon/skill-management-installer.js +94 -0
  83. package/dist/heart/daemon/socket-client.js +307 -0
  84. package/dist/heart/daemon/stale-bundle-prune.js +96 -0
  85. package/dist/heart/daemon/startup-tui.js +237 -0
  86. package/dist/heart/daemon/task-scheduler.js +3 -25
  87. package/dist/heart/daemon/thoughts.js +510 -0
  88. package/dist/heart/daemon/up-progress.js +135 -0
  89. package/dist/heart/delegation.js +62 -0
  90. package/dist/heart/habits/habit-migration.js +181 -0
  91. package/dist/heart/habits/habit-parser.js +140 -0
  92. package/dist/heart/habits/habit-scheduler.js +371 -0
  93. package/dist/heart/{daemon → hatch}/hatch-flow.js +53 -117
  94. package/dist/heart/{daemon → hatch}/hatch-specialist.js +3 -3
  95. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  96. package/dist/heart/{daemon → hatch}/specialist-tools.js +33 -12
  97. package/dist/heart/identity.js +161 -65
  98. package/dist/heart/kept-notes.js +357 -0
  99. package/dist/heart/kicks.js +1 -1
  100. package/dist/heart/machine-identity.js +161 -0
  101. package/dist/heart/mcp/mcp-server.js +653 -0
  102. package/dist/heart/migrate-config.js +100 -0
  103. package/dist/heart/model-capabilities.js +59 -0
  104. package/dist/heart/outlook/outlook-http-hooks.js +64 -0
  105. package/dist/heart/outlook/outlook-http-response.js +7 -0
  106. package/dist/heart/outlook/outlook-http-routes.js +232 -0
  107. package/dist/heart/outlook/outlook-http-static.js +99 -0
  108. package/dist/heart/outlook/outlook-http-transport.js +116 -0
  109. package/dist/heart/outlook/outlook-http.js +99 -0
  110. package/dist/heart/outlook/outlook-read.js +28 -0
  111. package/dist/heart/outlook/outlook-types.js +27 -0
  112. package/dist/heart/outlook/outlook-view.js +195 -0
  113. package/dist/heart/outlook/readers/agent-machine.js +359 -0
  114. package/dist/heart/outlook/readers/continuity-readers.js +332 -0
  115. package/dist/heart/outlook/readers/runtime-readers.js +660 -0
  116. package/dist/heart/outlook/readers/sessions.js +232 -0
  117. package/dist/heart/outlook/readers/shared.js +111 -0
  118. package/dist/heart/platform.js +81 -0
  119. package/dist/heart/progress-story.js +42 -0
  120. package/dist/heart/provider-attempt.js +133 -0
  121. package/dist/heart/provider-binding-resolver.js +239 -0
  122. package/dist/heart/provider-credentials.js +383 -0
  123. package/dist/heart/provider-failover.js +266 -0
  124. package/dist/heart/provider-models.js +81 -0
  125. package/dist/heart/provider-ping.js +237 -0
  126. package/dist/heart/provider-state.js +216 -0
  127. package/dist/heart/provider-visibility.js +186 -0
  128. package/dist/heart/providers/anthropic-token.js +131 -0
  129. package/dist/heart/providers/anthropic.js +193 -55
  130. package/dist/heart/providers/azure.js +103 -12
  131. package/dist/heart/providers/error-classification.js +63 -0
  132. package/dist/heart/providers/github-copilot.js +145 -0
  133. package/dist/heart/providers/minimax-vlm.js +189 -0
  134. package/dist/heart/providers/minimax.js +29 -7
  135. package/dist/heart/providers/openai-codex.js +39 -29
  136. package/dist/heart/runtime-credentials.js +181 -0
  137. package/dist/heart/session-activity.js +190 -0
  138. package/dist/heart/session-events.js +855 -0
  139. package/dist/heart/session-transcript.js +167 -0
  140. package/dist/heart/start-of-turn-packet.js +345 -0
  141. package/dist/heart/streaming.js +36 -27
  142. package/dist/heart/sync.js +332 -0
  143. package/dist/heart/target-resolution.js +127 -0
  144. package/dist/heart/tempo.js +93 -0
  145. package/dist/heart/temporal-view.js +41 -0
  146. package/dist/heart/tool-activity-callbacks.js +36 -0
  147. package/dist/heart/tool-description.js +135 -0
  148. package/dist/heart/tool-friction.js +55 -0
  149. package/dist/heart/tool-loop.js +200 -0
  150. package/dist/heart/turn-context.js +351 -0
  151. package/dist/heart/turn-coordinator.js +28 -0
  152. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +1 -1
  153. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  154. package/dist/heart/versioning/ouro-path-installer.js +301 -0
  155. package/dist/heart/versioning/ouro-version-manager.js +295 -0
  156. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  157. package/dist/heart/{daemon → versioning}/update-checker.js +12 -2
  158. package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
  159. package/dist/mind/bundle-manifest.js +7 -1
  160. package/dist/mind/context.js +134 -87
  161. package/dist/mind/diary-integrity.js +60 -0
  162. package/dist/mind/{memory.js → diary.js} +84 -96
  163. package/dist/mind/embedding-provider.js +60 -0
  164. package/dist/mind/file-state.js +179 -0
  165. package/dist/mind/first-impressions.js +14 -1
  166. package/dist/mind/friends/channel.js +21 -0
  167. package/dist/mind/friends/group-context.js +144 -0
  168. package/dist/mind/friends/resolver.js +38 -1
  169. package/dist/mind/friends/store-file.js +39 -3
  170. package/dist/mind/friends/trust-explanation.js +74 -0
  171. package/dist/mind/friends/types.js +1 -1
  172. package/dist/mind/journal-index.js +161 -0
  173. package/dist/mind/note-search.js +268 -0
  174. package/dist/mind/obligation-steering.js +221 -0
  175. package/dist/mind/pending.js +66 -7
  176. package/dist/mind/prompt-refresh.js +3 -2
  177. package/dist/mind/prompt.js +946 -167
  178. package/dist/mind/provenance-trust.js +26 -0
  179. package/dist/mind/scrutiny.js +173 -0
  180. package/dist/nerves/cli-logging.js +7 -1
  181. package/dist/nerves/coverage/audit-rules.js +15 -6
  182. package/dist/nerves/coverage/audit.js +28 -2
  183. package/dist/nerves/coverage/cli.js +1 -1
  184. package/dist/nerves/coverage/contract.js +5 -5
  185. package/dist/nerves/coverage/file-completeness.js +83 -5
  186. package/dist/nerves/coverage/run-artifacts.js +1 -1
  187. package/dist/nerves/event-buffer.js +111 -0
  188. package/dist/nerves/index.js +224 -4
  189. package/dist/nerves/observation.js +20 -0
  190. package/dist/nerves/redact.js +79 -0
  191. package/dist/nerves/runtime.js +5 -1
  192. package/dist/outlook-ui/assets/index-BAcU08c-.css +1 -0
  193. package/dist/outlook-ui/assets/index-D7l3l4vY.js +61 -0
  194. package/dist/outlook-ui/index.html +15 -0
  195. package/dist/repertoire/ado-client.js +15 -56
  196. package/dist/repertoire/ado-semantic.js +11 -10
  197. package/dist/repertoire/api-client.js +97 -0
  198. package/dist/repertoire/bitwarden-store.js +461 -0
  199. package/dist/repertoire/bundle-templates.js +72 -0
  200. package/dist/repertoire/bw-installer.js +79 -0
  201. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  202. package/dist/repertoire/coding/context-pack.js +330 -0
  203. package/dist/repertoire/coding/feedback.js +197 -30
  204. package/dist/repertoire/coding/manager.js +158 -9
  205. package/dist/repertoire/coding/spawner.js +55 -9
  206. package/dist/repertoire/coding/tools.js +170 -7
  207. package/dist/repertoire/commerce-errors.js +109 -0
  208. package/dist/repertoire/commerce-self-test.js +156 -0
  209. package/dist/repertoire/credential-access.js +107 -0
  210. package/dist/repertoire/duffel-client.js +185 -0
  211. package/dist/repertoire/github-client.js +14 -55
  212. package/dist/repertoire/graph-client.js +11 -52
  213. package/dist/repertoire/guardrails.js +371 -0
  214. package/dist/repertoire/mcp-client.js +255 -0
  215. package/dist/repertoire/mcp-manager.js +305 -0
  216. package/dist/repertoire/mcp-tools.js +63 -0
  217. package/dist/repertoire/shell-sessions.js +133 -0
  218. package/dist/repertoire/skills.js +15 -24
  219. package/dist/repertoire/stripe-client.js +131 -0
  220. package/dist/repertoire/tasks/board.js +43 -5
  221. package/dist/repertoire/tasks/fix.js +182 -0
  222. package/dist/repertoire/tasks/index.js +26 -1
  223. package/dist/repertoire/tasks/lifecycle.js +2 -2
  224. package/dist/repertoire/tasks/parser.js +3 -2
  225. package/dist/repertoire/tasks/scanner.js +194 -37
  226. package/dist/repertoire/tasks/transitions.js +16 -78
  227. package/dist/repertoire/tool-results.js +29 -0
  228. package/dist/repertoire/tools-attachments.js +317 -0
  229. package/dist/repertoire/tools-base.js +42 -687
  230. package/dist/repertoire/tools-bluebubbles.js +1 -0
  231. package/dist/repertoire/tools-bridge.js +141 -0
  232. package/dist/repertoire/tools-bundle.js +984 -0
  233. package/dist/repertoire/tools-config.js +185 -0
  234. package/dist/repertoire/tools-continuity.js +248 -0
  235. package/dist/repertoire/tools-credential.js +182 -0
  236. package/dist/repertoire/tools-files.js +342 -0
  237. package/dist/repertoire/tools-flight.js +224 -0
  238. package/dist/repertoire/tools-flow.js +105 -0
  239. package/dist/repertoire/tools-github.js +1 -7
  240. package/dist/repertoire/tools-notes.js +376 -0
  241. package/dist/repertoire/tools-session.js +739 -0
  242. package/dist/repertoire/tools-shell.js +120 -0
  243. package/dist/repertoire/tools-stripe.js +180 -0
  244. package/dist/repertoire/tools-surface.js +243 -0
  245. package/dist/repertoire/tools-teams.js +9 -39
  246. package/dist/repertoire/tools-travel.js +125 -0
  247. package/dist/repertoire/tools-user-profile.js +144 -0
  248. package/dist/repertoire/tools-vault.js +40 -0
  249. package/dist/repertoire/tools.js +144 -113
  250. package/dist/repertoire/travel-api-client.js +360 -0
  251. package/dist/repertoire/user-profile.js +118 -0
  252. package/dist/repertoire/vault-setup.js +246 -0
  253. package/dist/repertoire/vault-unlock.js +382 -0
  254. package/dist/scripts/claude-code-hook.js +41 -0
  255. package/dist/scripts/claude-code-stop-hook.js +47 -0
  256. package/dist/senses/attention-queue.js +116 -0
  257. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  258. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  259. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +260 -9
  260. package/dist/senses/bluebubbles/entry.js +70 -0
  261. package/dist/senses/bluebubbles/inbound-log.js +113 -0
  262. package/dist/senses/bluebubbles/index.js +1620 -0
  263. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -70
  264. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +33 -12
  265. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +45 -3
  266. package/dist/senses/bluebubbles/replay.js +129 -0
  267. package/dist/senses/bluebubbles/runtime-state.js +109 -0
  268. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  269. package/dist/senses/cli/bracketed-paste.js +82 -0
  270. package/dist/senses/cli/image-paste.js +287 -0
  271. package/dist/senses/cli/image-ref-navigation.js +75 -0
  272. package/dist/senses/cli/ink-app.js +156 -0
  273. package/dist/senses/cli/inline-diff.js +64 -0
  274. package/dist/senses/cli/input-keys.js +174 -0
  275. package/dist/senses/cli/kill-ring.js +86 -0
  276. package/dist/senses/cli/message-list.js +51 -0
  277. package/dist/senses/cli/ouro-tui.js +605 -0
  278. package/dist/senses/cli/spinner-imperative.js +135 -0
  279. package/dist/senses/cli/spinner.js +101 -0
  280. package/dist/senses/cli/status-line.js +60 -0
  281. package/dist/senses/cli/streaming-markdown.js +526 -0
  282. package/dist/senses/cli/tool-display.js +83 -0
  283. package/dist/senses/cli/tool-render.js +85 -0
  284. package/dist/senses/cli/tui-store.js +240 -0
  285. package/dist/senses/cli/virtual-list.js +35 -0
  286. package/dist/senses/cli-entry.js +60 -8
  287. package/dist/senses/cli-layout.js +187 -0
  288. package/dist/senses/cli.js +526 -211
  289. package/dist/senses/commands.js +66 -3
  290. package/dist/senses/continuity.js +94 -0
  291. package/dist/senses/habit-turn-message.js +108 -0
  292. package/dist/senses/inner-dialog-worker.js +112 -19
  293. package/dist/senses/inner-dialog.js +600 -95
  294. package/dist/senses/pipeline.js +539 -61
  295. package/dist/senses/proactive-content-guard.js +51 -0
  296. package/dist/senses/shared-turn.js +205 -0
  297. package/dist/senses/surface-tool.js +68 -0
  298. package/dist/senses/teams-entry.js +60 -8
  299. package/dist/senses/teams.js +569 -237
  300. package/dist/senses/trust-gate.js +5 -5
  301. package/package.json +28 -7
  302. package/skills/agent-commerce.md +106 -0
  303. package/skills/browser-navigation.md +110 -0
  304. package/skills/commerce-setup-guide.md +116 -0
  305. package/skills/commerce-setup.md +84 -0
  306. package/skills/configure-dev-tools.md +101 -0
  307. package/skills/travel-planning.md +134 -0
  308. package/dist/heart/daemon/ouro-path-installer.js +0 -178
  309. package/dist/heart/daemon/subagent-installer.js +0 -134
  310. package/dist/mind/associative-recall.js +0 -197
  311. package/dist/senses/bluebubbles-entry.js +0 -11
  312. package/dist/senses/bluebubbles.js +0 -832
  313. package/dist/senses/debug-activity.js +0 -127
  314. package/subagents/README.md +0 -60
  315. package/subagents/work-doer.md +0 -235
  316. package/subagents/work-merger.md +0 -618
  317. package/subagents/work-planner.md +0 -382
  318. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  319. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  320. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  321. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  322. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  323. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  324. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  325. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  326. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  327. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  328. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  329. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  330. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  331. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  332. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  333. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -34,12 +34,16 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DEFAULT_FLUSH_INTERVAL_MS = void 0;
37
+ exports.aiLabelEntities = aiLabelEntities;
37
38
  exports.stripMentions = stripMentions;
38
39
  exports.splitMessage = splitMessage;
40
+ exports.sanitizeFeedbackComment = sanitizeFeedbackComment;
41
+ exports.buildFeedbackSyntheticText = buildFeedbackSyntheticText;
42
+ exports.buildWelcomeCard = buildWelcomeCard;
39
43
  exports.createTeamsCallbacks = createTeamsCallbacks;
40
- exports.resolvePendingConfirmation = resolvePendingConfirmation;
41
44
  exports.withConversationLock = withConversationLock;
42
45
  exports.handleTeamsMessage = handleTeamsMessage;
46
+ exports.sendProactiveTeamsMessageToSession = sendProactiveTeamsMessageToSession;
43
47
  exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
44
48
  exports.startTeamsApp = startTeamsApp;
45
49
  const fs = __importStar(require("fs"));
@@ -57,17 +61,34 @@ const context_1 = require("../mind/context");
57
61
  const commands_1 = require("./commands");
58
62
  const nerves_1 = require("../nerves");
59
63
  const runtime_1 = require("../nerves/runtime");
64
+ const proactive_content_guard_1 = require("./proactive-content-guard");
60
65
  const store_file_1 = require("../mind/friends/store-file");
61
66
  const types_1 = require("../mind/friends/types");
62
67
  const resolver_1 = require("../mind/friends/resolver");
63
68
  const tokens_1 = require("../mind/friends/tokens");
64
69
  const turn_coordinator_1 = require("../heart/turn-coordinator");
65
70
  const identity_1 = require("../heart/identity");
71
+ const mcp_manager_1 = require("../repertoire/mcp-manager");
72
+ const progress_story_1 = require("../heart/progress-story");
73
+ const tool_activity_callbacks_1 = require("../heart/tool-activity-callbacks");
74
+ const commands_2 = require("./commands");
66
75
  const http = __importStar(require("http"));
67
76
  const path = __importStar(require("path"));
68
77
  const trust_gate_1 = require("./trust-gate");
69
78
  const pipeline_1 = require("./pipeline");
79
+ const teamsFailoverStates = new Map();
70
80
  const pending_1 = require("../mind/pending");
81
+ const continuity_1 = require("./continuity");
82
+ // AIGeneratedContent entity and feedbackLoopEnabled channelData for all outbound
83
+ // Teams messages. Required by Teams AI UX best practices.
84
+ function aiLabelEntities() {
85
+ return [{
86
+ type: "https://schema.org/Message",
87
+ "@type": "Message",
88
+ "@context": "https://schema.org",
89
+ additionalType: ["AIGeneratedContent"],
90
+ }];
91
+ }
71
92
  // Strip @mention markup from incoming messages.
72
93
  // Removes <at>...</at> tags and trims extra whitespace.
73
94
  // Fallback safety net -- the SDK's activity.mentions.stripText should handle
@@ -122,6 +143,46 @@ function splitMessage(text, maxLen) {
122
143
  }
123
144
  return chunks;
124
145
  }
146
+ // Sanitize user-provided feedback comments: truncate, strip control chars and newlines.
147
+ function sanitizeFeedbackComment(comment) {
148
+ const cleaned = comment.replace(/[\x00-\x1f\n\r]/g, "");
149
+ return cleaned.length > 200 ? cleaned.slice(0, 200) : cleaned;
150
+ }
151
+ // Build synthetic message text from a Teams feedback reaction.
152
+ function buildFeedbackSyntheticText(reaction, comment) {
153
+ const emoji = reaction === "like" ? "thumbs-up" : "thumbs-down";
154
+ if (comment) {
155
+ const sanitized = sanitizeFeedbackComment(comment);
156
+ return `[reacted with ${emoji} to your message: "${sanitized}"]`;
157
+ }
158
+ return `[reacted with ${emoji} to your message]`;
159
+ }
160
+ // Build a welcome Adaptive Card with prompt starters for new bot installs.
161
+ function buildWelcomeCard() {
162
+ const promptStarters = [
163
+ "What can you help me with?",
164
+ "Tell me about yourself",
165
+ "What's on my calendar today?",
166
+ "Summarize my recent emails",
167
+ ];
168
+ return {
169
+ type: "AdaptiveCard",
170
+ version: "1.5",
171
+ body: [
172
+ {
173
+ type: "TextBlock",
174
+ text: "Hey! I'm here and ready to help. Try one of these to get started, or just ask me anything.",
175
+ wrap: true,
176
+ size: "Medium",
177
+ },
178
+ ],
179
+ actions: promptStarters.map((prompt) => ({
180
+ type: "Action.Submit",
181
+ title: prompt,
182
+ data: { msteams: { type: "messageBack", text: prompt, displayText: prompt } },
183
+ })),
184
+ };
185
+ }
125
186
  // Create Teams-specific callbacks for the agent loop.
126
187
  // The SDK handles cumulative text, debouncing (500ms), and the streaming
127
188
  // protocol (streamSequence, streamId, informative/streaming/final types).
@@ -134,12 +195,16 @@ function splitMessage(text, maxLen) {
134
195
  // (transient status) or safeSend (terminal errors). Reasoning is accumulated
135
196
  // and periodically pushed via safeUpdate on the same flush timer tick.
136
197
  function createTeamsCallbacks(stream, controller, sendMessage, options) {
198
+ const MIN_INITIAL_CHARS = 20;
137
199
  let stopped = false; // set when stream signals cancellation (403)
138
200
  let hadToolRun = false;
139
201
  let hadRealOutput = false; // true once reasoning/tool output shown; suppresses phrases
140
202
  let reasoningBuf = ""; // accumulated reasoning text for status display
203
+ let totalEmitted = 0; // cumulative chars emitted via safeEmit (for >4000 finalization)
204
+ let streamFinalized = false; // true after stream.close() — subsequent flushes go to safeSend
141
205
  let textBuffer = ""; // accumulated text output for chunked streaming
142
206
  let streamHasContent = false; // tracks whether primary output has received content
207
+ let firstContentEmitted = false; // true after first content push — disables MIN_INITIAL_CHARS threshold
143
208
  let phraseTimer = null;
144
209
  let lastPhrase = "";
145
210
  let flushTimer = null;
@@ -189,15 +254,16 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
189
254
  result.catch(() => markStopped());
190
255
  }
191
256
  }
192
- // Safely emit a text delta to the stream.
257
+ // Safely emit a text delta to the stream with AI labels.
193
258
  // On error (e.g. 403 from Teams stop button), abort the controller.
194
259
  function safeEmit(text) {
195
260
  /* v8 ignore next -- defensive guard: stopped set by prior 403; tested via flush abort path @preserve */
196
261
  if (stopped)
197
262
  return;
198
263
  try {
199
- catchAsync(stream.emit(text));
264
+ catchAsync(stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } }));
200
265
  streamHasContent = true;
266
+ totalEmitted += text.length;
201
267
  }
202
268
  catch {
203
269
  markStopped();
@@ -212,7 +278,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
212
278
  try {
213
279
  // stream.emit() is typed as void but the Teams SDK returns a Promise
214
280
  // internally (async HTTP). Cast to capture the result for awaiting.
215
- const result = stream.emit(text);
281
+ const result = stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
216
282
  streamHasContent = true;
217
283
  if (result && typeof result.then === "function") {
218
284
  await result;
@@ -268,11 +334,49 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
268
334
  // emitted text into a single streaming message (cumulative), so every
269
335
  // periodic flush appends to the same response — not separate messages.
270
336
  // No preemptive splitting — sends full text. Error recovery happens in flush().
337
+ // Hybrid MIN_INITIAL_CHARS: hold back until >= MIN_INITIAL_CHARS accumulated
338
+ // before the first content emit, so phrase rotation shows while real content
339
+ // buffers. After first emit, flush normally (no threshold).
271
340
  function flushTextBuffer() {
272
341
  if (!textBuffer)
273
342
  return;
343
+ if (!firstContentEmitted && textBuffer.length < MIN_INITIAL_CHARS)
344
+ return;
345
+ // Proactive >4000 finalization: if cumulative emitted + buffer >= RECOVERY_CHUNK_SIZE,
346
+ // finalize the stream and send overflow via safeSend (follow-up message).
347
+ if (!streamFinalized && totalEmitted + textBuffer.length >= RECOVERY_CHUNK_SIZE) {
348
+ const remaining = RECOVERY_CHUNK_SIZE - totalEmitted;
349
+ /* v8 ignore next 2 -- defensive: remaining always > 0 because finalization runs once @preserve */
350
+ if (remaining > 0)
351
+ safeEmit(textBuffer.slice(0, remaining));
352
+ try {
353
+ stream.close();
354
+ }
355
+ catch { /* stream may already be dead */ }
356
+ streamFinalized = true;
357
+ /* v8 ignore next -- defensive ternary: remaining always > 0 at first finalization @preserve */
358
+ const overflow = textBuffer.slice(remaining > 0 ? remaining : 0);
359
+ textBuffer = "";
360
+ if (overflow)
361
+ safeSend(overflow);
362
+ if (!firstContentEmitted) {
363
+ firstContentEmitted = true;
364
+ stopPhraseRotation();
365
+ }
366
+ return;
367
+ }
368
+ if (streamFinalized) {
369
+ // After finalization, all content goes to safeSend (follow-up messages)
370
+ safeSend(textBuffer);
371
+ textBuffer = "";
372
+ return;
373
+ }
274
374
  safeEmit(textBuffer);
275
375
  textBuffer = "";
376
+ if (!firstContentEmitted) {
377
+ firstContentEmitted = true;
378
+ stopPhraseRotation();
379
+ }
276
380
  }
277
381
  function startPhraseRotation(pool) {
278
382
  stopPhraseRotation();
@@ -315,32 +419,45 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
315
419
  onTextChunk: (text) => {
316
420
  if (stopped)
317
421
  return;
318
- stopPhraseRotation();
422
+ // Don't stop phrase rotation here — let it continue until first content
423
+ // emit (handled in flushTextBuffer when MIN_INITIAL_CHARS threshold met).
319
424
  textBuffer += text;
320
425
  startFlushTimer();
321
426
  },
322
427
  onClearText: () => {
323
428
  textBuffer = "";
324
429
  },
325
- onToolStart: (name, args) => {
326
- stopPhraseRotation();
327
- flushTextBuffer();
328
- // Emit a placeholder to satisfy the 15s Copilot timeout for initial
329
- // stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
330
- // never emit before the timeout and the user sees "this response was
331
- // stopped". The placeholder is replaced by actual content on next emit.
332
- // https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
333
- if (!streamHasContent)
334
- safeEmit("⏳");
335
- const argSummary = (0, tools_1.summarizeArgs)(name, args) || Object.keys(args).join(", ");
336
- safeUpdate(`running ${name} (${argSummary})...`);
337
- hadToolRun = true;
338
- },
339
- onToolEnd: (name, summary, success) => {
340
- stopPhraseRotation();
341
- const msg = (0, format_1.formatToolResult)(name, summary, success);
342
- safeUpdate(msg);
343
- },
430
+ ...(() => {
431
+ const toolCbs = (0, tool_activity_callbacks_1.createToolActivityCallbacks)({
432
+ onDescription: (text) => safeUpdate(text),
433
+ /* v8 ignore next -- onResult only called in debug mode; tested via tool-activity-callbacks.test.ts @preserve */
434
+ onResult: (text) => safeUpdate(text),
435
+ /* v8 ignore next -- onFailure tested via onToolEnd failure test @preserve */
436
+ onFailure: (text) => safeUpdate(text),
437
+ isDebug: commands_2.getDebugMode,
438
+ });
439
+ return {
440
+ onToolStart: (name, args) => {
441
+ stopPhraseRotation();
442
+ // Force-flush any accumulated text, bypassing MIN_INITIAL_CHARS threshold
443
+ firstContentEmitted = true;
444
+ flushTextBuffer();
445
+ // Emit a placeholder to satisfy the 15s Copilot timeout for initial
446
+ // stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
447
+ // never emit before the timeout and the user sees "this response was
448
+ // stopped". The placeholder is replaced by actual content on next emit.
449
+ // https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
450
+ if (!streamHasContent)
451
+ safeEmit("\u23f3");
452
+ toolCbs.onToolStart(name, args);
453
+ hadToolRun = true;
454
+ },
455
+ onToolEnd: (name, summary, success) => {
456
+ stopPhraseRotation();
457
+ toolCbs.onToolEnd(name, summary, success);
458
+ },
459
+ };
460
+ })(),
344
461
  onKick: () => {
345
462
  stopPhraseRotation();
346
463
  const msg = (0, format_1.formatKick)();
@@ -350,7 +467,11 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
350
467
  stopPhraseRotation();
351
468
  if (stopped)
352
469
  return;
353
- const msg = (0, format_1.formatError)(error);
470
+ const msg = (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
471
+ scope: "shared-work",
472
+ phase: "errored",
473
+ outcomeText: (0, format_1.formatError)(error),
474
+ }));
354
475
  if (severity === "transient") {
355
476
  safeUpdate(msg);
356
477
  }
@@ -358,38 +479,27 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
358
479
  safeSend(msg);
359
480
  }
360
481
  },
361
- onConfirmAction: options?.conversationId
362
- ? async (name, args) => {
363
- const convId = options.conversationId;
364
- const argsDesc = Object.entries(args).map(([k, v]) => `${k}: ${v}`).join(", ");
365
- safeUpdate(`Confirm action: ${name} (${argsDesc}) -- reply "yes" to confirm or "no" to cancel`);
366
- return new Promise((resolve) => {
367
- _pendingConfirmations.set(convId, resolve);
368
- // Auto-deny after 2 minutes to prevent indefinite blocking
369
- // (e.g. when the stream dies and the user never sees the prompt).
370
- setTimeout(() => {
371
- if (_pendingConfirmations.has(convId)) {
372
- _pendingConfirmations.delete(convId);
373
- resolve("denied");
374
- }
375
- }, 120_000);
376
- });
377
- }
378
- : undefined,
379
482
  flush: async () => {
380
483
  stopFlushTimer();
484
+ stopPhraseRotation();
381
485
  if (textBuffer) {
486
+ // Bypass MIN_INITIAL_CHARS threshold — flush delivers all remaining content
487
+ firstContentEmitted = true;
382
488
  const text = textBuffer;
383
489
  textBuffer = "";
384
- if (!stopped) {
490
+ if (streamFinalized && sendMessage) {
491
+ // Stream already finalized (>4000 path) — send remaining content as follow-up
492
+ safeSend(text);
493
+ }
494
+ else if (!stopped) {
385
495
  // Stream is alive — await the emit so we can catch async 413/failure
386
496
  // and fall through to sendMessage recovery.
387
497
  const ok = await tryEmit(text);
388
498
  if (!ok)
389
499
  markStopped();
390
500
  }
391
- if (stopped && sendMessage) {
392
- // Stream is dead — fall back to sendMessage; split on failure as recovery.
501
+ if (stopped && !streamFinalized && sendMessage) {
502
+ // Stream is dead (not from finalization) — fall back to sendMessage; split on failure as recovery.
393
503
  try {
394
504
  await sendMessage(text);
395
505
  }
@@ -400,32 +510,12 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
400
510
  }
401
511
  }
402
512
  }
403
- else if (!streamHasContent) {
404
- safeEmit("(completed with tool calls only \u2014 no text response)");
513
+ else if (!streamHasContent && !options?.suppressEmptyStreamMessage) {
514
+ safeEmit("(completed with tool calls only no text response)");
405
515
  }
406
516
  },
407
517
  };
408
518
  }
409
- // Per-conversation pending confirmation resolvers.
410
- // When a mutate tool needs confirmation, the resolver is stored here.
411
- // The next message from the same conversation resolves it.
412
- const _pendingConfirmations = new Map();
413
- // Confirmation response words (case-insensitive)
414
- const CONFIRM_WORDS = new Set(["yes", "confirm", "go", "y", "ok", "approve", "proceed"]);
415
- function resolvePendingConfirmation(convId, text) {
416
- const resolver = _pendingConfirmations.get(convId);
417
- if (!resolver)
418
- return false;
419
- _pendingConfirmations.delete(convId);
420
- const word = text.trim().toLowerCase();
421
- if (CONFIRM_WORDS.has(word)) {
422
- resolver("confirmed");
423
- }
424
- else {
425
- resolver("denied");
426
- }
427
- return true;
428
- }
429
519
  const _turnCoordinator = (0, turn_coordinator_1.createTurnCoordinator)();
430
520
  function teamsTurnKey(conversationId) {
431
521
  return `teams:${conversationId}`;
@@ -439,15 +529,48 @@ function getFriendStore() {
439
529
  const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
440
530
  return new store_file_1.FileFriendStore(friendsPath);
441
531
  }
532
+ function createTeamsCommandRegistry() {
533
+ const registry = (0, commands_1.createCommandRegistry)();
534
+ (0, commands_1.registerDefaultCommands)(registry);
535
+ return registry;
536
+ }
537
+ /* v8 ignore start -- superseding follow-up slash command handler; tested via startTeamsApp integration tests @preserve */
538
+ function handleTeamsSlashCommand(text, registry, friendId, conversationId, stream, emitResponse = true) {
539
+ const parsed = (0, commands_1.parseSlashCommand)(text);
540
+ if (!parsed)
541
+ return null;
542
+ const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
543
+ if (!dispatchResult.handled || !dispatchResult.result) {
544
+ return null;
545
+ }
546
+ if (dispatchResult.result.action === "new") {
547
+ (0, context_1.deleteSession)((0, config_2.sessionPath)(friendId, "teams", conversationId));
548
+ if (emitResponse) {
549
+ stream.emit("session cleared");
550
+ }
551
+ return "new";
552
+ }
553
+ if (dispatchResult.result.action === "response") {
554
+ if (emitResponse) {
555
+ stream.emit(dispatchResult.result.message || "");
556
+ }
557
+ return "response";
558
+ }
559
+ return null;
560
+ }
561
+ /* v8 ignore stop */
442
562
  // Handle an incoming Teams message
443
- async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage) {
563
+ async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage, reactionOverrides) {
444
564
  const turnKey = teamsTurnKey(conversationId);
445
565
  // NOTE: Confirmation resolution is handled in the app.on("message") handler
446
566
  // BEFORE the conversation lock. By the time we get here, any pending
447
567
  // confirmation has already been resolved and the reply consumed.
448
568
  // Send first thinking phrase immediately so the user sees feedback
449
569
  // before sync I/O (session load, trim) blocks the event loop.
450
- stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
570
+ // Skip for reaction signals — they should be processed quietly.
571
+ if (!reactionOverrides) {
572
+ stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
573
+ }
451
574
  await new Promise(r => setImmediate(r));
452
575
  // Resolve identity provider early for friend resolution + slash command session path
453
576
  const store = getFriendStore();
@@ -464,29 +587,10 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
464
587
  // Pre-resolve friend for session path + slash commands (pipeline will re-use the cached result)
465
588
  const resolvedContext = await resolver.resolve();
466
589
  const friendId = resolvedContext.friend.id;
467
- const registry = (0, commands_1.createCommandRegistry)();
468
- (0, commands_1.registerDefaultCommands)(registry);
469
- // Check for slash commands (before pipeline -- these are transport-level concerns)
470
- const parsed = (0, commands_1.parseSlashCommand)(text);
471
- if (parsed) {
472
- const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
473
- if (dispatchResult.handled && dispatchResult.result) {
474
- if (dispatchResult.result.action === "new") {
475
- const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
476
- (0, context_1.deleteSession)(sessPath);
477
- stream.emit("session cleared");
478
- return;
479
- }
480
- else if (dispatchResult.result.action === "response") {
481
- stream.emit(dispatchResult.result.message || "");
482
- return;
483
- }
484
- }
485
- }
486
590
  // ── Teams adapter concerns: controller, callbacks, session path ──────────
487
591
  const controller = new AbortController();
488
592
  const channelConfig = (0, config_2.getTeamsChannelConfig)();
489
- const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
593
+ const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs, ...(reactionOverrides?.suppressEmptyStreamMessage ? { suppressEmptyStreamMessage: true } : {}) });
490
594
  const traceId = (0, nerves_1.createTraceId)();
491
595
  const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
492
596
  const teamsCapabilities = (0, channel_1.getChannelCapabilities)("teams");
@@ -497,80 +601,161 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
497
601
  adoToken: teamsContext.adoToken,
498
602
  githubToken: teamsContext.githubToken,
499
603
  signin: teamsContext.signin,
500
- summarize: (0, core_1.createSummarize)(),
604
+ summarize: (0, core_1.createSummarize)("human"),
501
605
  tenantId: teamsContext.tenantId,
502
606
  botApi: teamsContext.botApi,
503
607
  } : {};
504
- // Build runAgentOptions with Teams-specific fields
505
- const agentOptions = {
506
- traceId,
507
- toolContext: teamsToolContext,
508
- drainSteeringFollowUps: () => _turnCoordinator.drainFollowUps(turnKey).map((m) => ({ text: m.text })),
509
- };
510
- if (channelConfig.skipConfirmation)
511
- agentOptions.skipConfirmation = true;
512
- // ── Call shared pipeline ──────────────────────────────────────────
513
- const result = await (0, pipeline_1.handleInboundTurn)({
514
- channel: "teams",
515
- capabilities: teamsCapabilities,
516
- messages: [{ role: "user", content: text }],
517
- callbacks,
518
- friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
519
- sessionLoader: {
520
- loadOrCreate: async () => {
521
- const existing = (0, context_1.loadSession)(sessPath);
522
- const messages = existing?.messages && existing.messages.length > 0
523
- ? existing.messages
524
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, resolvedContext) }];
525
- (0, core_1.repairOrphanedToolCalls)(messages);
526
- return { messages, sessionPath: sessPath };
608
+ let currentText = text;
609
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
610
+ while (true) {
611
+ let drainedSteeringFollowUps = [];
612
+ // Build runAgentOptions with Teams-specific fields
613
+ const agentOptions = {
614
+ traceId,
615
+ toolContext: teamsToolContext,
616
+ mcpManager,
617
+ drainSteeringFollowUps: () => {
618
+ drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
619
+ .map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
620
+ return drainedSteeringFollowUps;
527
621
  },
528
- },
529
- pendingDir,
530
- friendStore: store,
531
- provider,
532
- externalId,
533
- tenantId: teamsContext?.tenantId,
534
- isGroupChat: false,
535
- groupHasFamilyMember: false,
536
- hasExistingGroupWithFamily: false,
537
- enforceTrustGate: trust_gate_1.enforceTrustGate,
538
- drainPending: pending_1.drainPending,
539
- runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
540
- ...opts,
541
- toolContext: {
542
- /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
543
- signin: async () => undefined,
544
- ...opts?.toolContext,
545
- summarize: teamsToolContext.summarize,
622
+ ...(reactionOverrides?.isReactionSignal ? { isReactionSignal: true } : {}),
623
+ };
624
+ // ── Call shared pipeline ──────────────────────────────────────────
625
+ // Capture terminal errors — failover message replaces the error card if it triggers
626
+ let capturedTerminalError = null;
627
+ const teamsFailoverState = (() => {
628
+ if (!teamsFailoverStates.has(conversationId)) {
629
+ teamsFailoverStates.set(conversationId, { pending: null });
630
+ }
631
+ return teamsFailoverStates.get(conversationId);
632
+ })();
633
+ /* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
634
+ const failoverAwareCallbacks = {
635
+ ...callbacks,
636
+ onError: (error, severity) => {
637
+ if (severity === "terminal" && teamsFailoverState) {
638
+ capturedTerminalError = error;
639
+ return;
640
+ }
641
+ callbacks.onError(error, severity);
546
642
  },
547
- }),
548
- postTurn: context_1.postTurn,
549
- accumulateFriendTokens: tokens_1.accumulateFriendTokens,
550
- signal: controller.signal,
551
- runAgentOptions: agentOptions,
552
- });
553
- // ── Handle gate result ────────────────────────────────────────
554
- if (!result.gateResult.allowed) {
555
- if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
556
- stream.emit(result.gateResult.autoReply);
557
- }
558
- return;
559
- }
560
- // Flush any remaining accumulated text at end of turn
561
- await callbacks.flush();
562
- // After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
563
- // This must happen after the stream is done so the OAuth card renders properly.
564
- if (teamsContext && result.messages) {
565
- const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
566
- if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
567
- await teamsContext.signin(teamsContext.graphConnectionName);
568
- if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
569
- await teamsContext.signin(teamsContext.adoConnectionName);
570
- if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
571
- await teamsContext.signin(teamsContext.githubConnectionName);
572
- }
573
- // SDK auto-closes the stream after our handler returns (app.process.js)
643
+ };
644
+ /* v8 ignore stop */
645
+ const result = await (0, pipeline_1.handleInboundTurn)({
646
+ channel: "teams",
647
+ sessionKey: conversationId,
648
+ capabilities: teamsCapabilities,
649
+ messages: [{ role: "user", content: currentText }],
650
+ continuityIngressTexts: [currentText],
651
+ callbacks: failoverAwareCallbacks,
652
+ friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
653
+ sessionLoader: {
654
+ loadOrCreate: async () => {
655
+ const existing = (0, context_1.loadSession)(sessPath);
656
+ const messages = existing?.messages && existing.messages.length > 0
657
+ ? existing.messages
658
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("teams", {}, resolvedContext)) }];
659
+ (0, core_1.repairOrphanedToolCalls)(messages);
660
+ return {
661
+ messages,
662
+ sessionPath: sessPath,
663
+ state: existing?.state,
664
+ events: existing?.events,
665
+ };
666
+ },
667
+ },
668
+ pendingDir,
669
+ friendStore: store,
670
+ provider,
671
+ externalId,
672
+ tenantId: teamsContext?.tenantId,
673
+ isGroupChat: false,
674
+ groupHasFamilyMember: false,
675
+ hasExistingGroupWithFamily: false,
676
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
677
+ drainPending: pending_1.drainPending,
678
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)((0, identity_1.getAgentName)(), deferredFriendId),
679
+ runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
680
+ ...opts,
681
+ toolContext: {
682
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
683
+ signin: async () => undefined,
684
+ ...opts?.toolContext,
685
+ summarize: teamsToolContext.summarize,
686
+ },
687
+ }),
688
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
689
+ const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
690
+ (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
691
+ },
692
+ accumulateFriendTokens: tokens_1.accumulateFriendTokens,
693
+ signal: controller.signal,
694
+ runAgentOptions: agentOptions,
695
+ failoverState: teamsFailoverState,
696
+ });
697
+ // ── Handle pipeline-intercepted commands ────────────────────────
698
+ if (result.turnOutcome === "command") {
699
+ if (result.commandAction === "new") {
700
+ (0, context_1.deleteSession)(sessPath);
701
+ stream.emit("session cleared");
702
+ }
703
+ // For "response" commands: pipeline already emitted the response via onTextChunk
704
+ await callbacks.flush();
705
+ return;
706
+ }
707
+ /* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
708
+ if (result.failoverMessage) {
709
+ stream.emit(result.failoverMessage);
710
+ }
711
+ else if (capturedTerminalError) {
712
+ callbacks.onError(capturedTerminalError, "terminal");
713
+ }
714
+ /* v8 ignore stop */
715
+ // ── Handle gate result ────────────────────────────────────────
716
+ if (!result.gateResult.allowed) {
717
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
718
+ stream.emit(result.gateResult.autoReply);
719
+ }
720
+ return;
721
+ }
722
+ // Flush any remaining accumulated text at end of turn
723
+ await callbacks.flush();
724
+ // After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
725
+ // This must happen after the stream is done so the OAuth card renders properly.
726
+ if (teamsContext && result.messages) {
727
+ const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
728
+ if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
729
+ await teamsContext.signin(teamsContext.graphConnectionName);
730
+ if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
731
+ await teamsContext.signin(teamsContext.adoConnectionName);
732
+ if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
733
+ await teamsContext.signin(teamsContext.githubConnectionName);
734
+ }
735
+ if (result.turnOutcome !== "superseded") {
736
+ return;
737
+ }
738
+ const supersedingIndex = drainedSteeringFollowUps
739
+ .map((followUp) => followUp.effect)
740
+ .lastIndexOf("clear_and_supersede");
741
+ if (supersedingIndex < 0) {
742
+ return;
743
+ }
744
+ const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
745
+ const replayTail = drainedSteeringFollowUps
746
+ .slice(supersedingIndex + 1)
747
+ .map((followUp) => followUp.text.trim())
748
+ .filter((followUpText) => followUpText.length > 0)
749
+ .join("\n");
750
+ if (replayTail) {
751
+ currentText = replayTail;
752
+ continue;
753
+ }
754
+ if (handleTeamsSlashCommand(supersedingFollowUp.text, createTeamsCommandRegistry(), friendId, conversationId, stream, false)) {
755
+ return;
756
+ }
757
+ currentText = supersedingFollowUp.text;
758
+ }
574
759
  }
575
760
  // Internal port for the secondary bot App (not exposed externally).
576
761
  // The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
@@ -646,7 +831,7 @@ function registerBotHandlers(app, label) {
646
831
  // (graph + ado + github). The verifyState activity only carries a `state`
647
832
  // code with no connectionName, so we try each configured connection until
648
833
  // one succeeds.
649
- app.on("signin.verify-state", async (ctx) => {
834
+ app.on("signin.verify-state", (async (ctx) => {
650
835
  const { api, activity } = ctx;
651
836
  if (!activity.value?.state)
652
837
  return { status: 404 };
@@ -665,7 +850,73 @@ function registerBotHandlers(app, label) {
665
850
  }
666
851
  (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
667
852
  return { status: 412 };
853
+ }));
854
+ // Handle Teams feedback reactions (thumbs up/down on AI-generated messages).
855
+ // SDK routes message/submitAction with actionName "feedback" to this event.
856
+ /* v8 ignore start -- Teams SDK invoke handler; requires live SDK context @preserve */
857
+ app.on("message.submit.feedback", async (ctx) => {
858
+ const { stream, activity } = ctx;
859
+ const reaction = activity.value?.actionValue?.reaction;
860
+ const comment = activity.value?.actionValue?.feedback;
861
+ const convId = activity.conversation?.id || "unknown";
862
+ const turnKey = teamsTurnKey(convId);
863
+ // Validate payload — graceful no-op for malformed invocations
864
+ if (activity.value?.actionName !== "feedback" || !reaction) {
865
+ return;
866
+ }
867
+ const syntheticText = buildFeedbackSyntheticText(reaction, comment);
868
+ // Turn coordination: if a turn is active, enqueue as steering follow-up
869
+ if (!_turnCoordinator.tryBeginTurn(turnKey)) {
870
+ _turnCoordinator.enqueueFollowUp(turnKey, {
871
+ conversationId: convId,
872
+ text: syntheticText,
873
+ receivedAt: Date.now(),
874
+ effect: (0, continuity_1.classifySteeringFollowUpEffect)(syntheticText),
875
+ });
876
+ return;
877
+ }
878
+ try {
879
+ const teamsContext = {
880
+ signin: async () => undefined,
881
+ aadObjectId: activity.from?.aadObjectId,
882
+ tenantId: activity.conversation?.tenantId,
883
+ displayName: activity.from?.name,
884
+ };
885
+ const ctxSend = async (t) => {
886
+ await ctx.send({ type: "message", text: t, replyToId: activity.replyToId, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
887
+ };
888
+ await handleTeamsMessage(syntheticText, stream, convId, teamsContext, ctxSend, { isReactionSignal: true, suppressEmptyStreamMessage: true });
889
+ }
890
+ catch (err) {
891
+ const msg = err instanceof Error ? err.message : String(err);
892
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.feedback_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
893
+ }
894
+ finally {
895
+ _turnCoordinator.endTurn(turnKey);
896
+ }
668
897
  });
898
+ /* v8 ignore stop */
899
+ // Handle bot install — send welcome Adaptive Card with prompt starters.
900
+ /* v8 ignore start -- Teams SDK install handler; requires live SDK context @preserve */
901
+ app.on("install.add", async (ctx) => {
902
+ try {
903
+ const card = buildWelcomeCard();
904
+ await ctx.send({
905
+ type: "message",
906
+ attachments: [{
907
+ contentType: "application/vnd.microsoft.card.adaptive",
908
+ content: card,
909
+ }],
910
+ entities: aiLabelEntities(),
911
+ channelData: { feedbackLoopEnabled: true },
912
+ });
913
+ }
914
+ catch (err) {
915
+ const msg = err instanceof Error ? err.message : String(err);
916
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.welcome_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
917
+ }
918
+ });
919
+ /* v8 ignore stop */
669
920
  app.on("message", async (ctx) => {
670
921
  const { stream, activity, api, signin } = ctx;
671
922
  const text = activity.text || "";
@@ -674,12 +925,40 @@ function registerBotHandlers(app, label) {
674
925
  const userId = activity.from?.id || "";
675
926
  const channelId = activity.channelId || "msteams";
676
927
  (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
677
- // Resolve pending confirmations IMMEDIATELY — before token fetches or
678
- // the conversation lock. The original message holds the lock while
679
- // awaiting confirmation, so acquiring it here would deadlock. Token
680
- // fetches are also unnecessary (and slow) for a simple yes/no reply.
681
- if (resolvePendingConfirmation(convId, text)) {
682
- return;
928
+ const commandRegistry = createTeamsCommandRegistry();
929
+ const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
930
+ if (parsedSlashCommand) {
931
+ const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
932
+ if (dispatchResult.handled && dispatchResult.result) {
933
+ if (dispatchResult.result.action === "response") {
934
+ stream.emit(dispatchResult.result.message || "");
935
+ return;
936
+ }
937
+ if (dispatchResult.result.action === "new") {
938
+ const commandStore = getFriendStore();
939
+ const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
940
+ const commandExternalId = activity.from?.aadObjectId || convId;
941
+ const commandResolver = new resolver_1.FriendResolver(commandStore, {
942
+ provider: commandProvider,
943
+ externalId: commandExternalId,
944
+ tenantId: activity.conversation?.tenantId,
945
+ displayName: activity.from?.name || "Unknown",
946
+ channel: "teams",
947
+ });
948
+ const commandContext = await commandResolver.resolve();
949
+ (0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
950
+ stream.emit("session cleared");
951
+ if (_turnCoordinator.isTurnActive(turnKey)) {
952
+ _turnCoordinator.enqueueFollowUp(turnKey, {
953
+ conversationId: convId,
954
+ text,
955
+ receivedAt: Date.now(),
956
+ effect: "clear_and_supersede",
957
+ });
958
+ }
959
+ return;
960
+ }
961
+ }
683
962
  }
684
963
  // If this conversation already has an active turn, steer follow-up input
685
964
  // into that turn and avoid starting a second concurrent turn.
@@ -688,6 +967,7 @@ function registerBotHandlers(app, label) {
688
967
  conversationId: convId,
689
968
  text,
690
969
  receivedAt: Date.now(),
970
+ effect: (0, continuity_1.classifySteeringFollowUpEffect)(text),
691
971
  });
692
972
  return;
693
973
  }
@@ -745,7 +1025,7 @@ function registerBotHandlers(app, label) {
745
1025
  const ctxSend = async (t) => {
746
1026
  // Use send with replyToId (not reply, which adds a blockquote).
747
1027
  // replyToId anchors the message after the user's message in Copilot Chat.
748
- await ctx.send({ type: "message", text: t, replyToId: activity.id });
1028
+ await ctx.send({ type: "message", text: t, replyToId: activity.id, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
749
1029
  };
750
1030
  await handleTeamsMessage(text, stream, convId, teamsContext, ctxSend);
751
1031
  }
@@ -770,6 +1050,108 @@ function findAadObjectId(friend) {
770
1050
  }
771
1051
  return undefined;
772
1052
  }
1053
+ function resolveTeamsFriendStore(deps) {
1054
+ return deps.store
1055
+ ?? deps.createFriendStore?.()
1056
+ ?? new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends"));
1057
+ }
1058
+ function getTeamsConversations(botApi) {
1059
+ return botApi.conversations;
1060
+ }
1061
+ function hasExplicitCrossChatAuthorization(params) {
1062
+ return params.intent === "explicit_cross_chat"
1063
+ && types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
1064
+ }
1065
+ async function sendProactiveTeamsMessageToSession(params, deps) {
1066
+ const store = resolveTeamsFriendStore(deps);
1067
+ const conversations = getTeamsConversations(deps.botApi);
1068
+ let friend;
1069
+ try {
1070
+ friend = await store.get(params.friendId);
1071
+ }
1072
+ catch {
1073
+ friend = null;
1074
+ }
1075
+ if (!friend) {
1076
+ (0, runtime_1.emitNervesEvent)({
1077
+ level: "warn",
1078
+ component: "senses",
1079
+ event: "senses.teams_proactive_no_friend",
1080
+ message: "proactive send skipped: friend not found",
1081
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
1082
+ });
1083
+ return { delivered: false, reason: "friend_not_found" };
1084
+ }
1085
+ if (!hasExplicitCrossChatAuthorization(params) && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
1086
+ (0, runtime_1.emitNervesEvent)({
1087
+ component: "senses",
1088
+ event: "senses.teams_proactive_trust_skip",
1089
+ message: "proactive send skipped: trust level not allowed",
1090
+ meta: {
1091
+ friendId: params.friendId,
1092
+ trustLevel: friend.trustLevel ?? "unknown",
1093
+ intent: params.intent ?? "generic_outreach",
1094
+ authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
1095
+ },
1096
+ });
1097
+ return { delivered: false, reason: "trust_skip" };
1098
+ }
1099
+ const aadInfo = findAadObjectId(friend);
1100
+ if (!aadInfo) {
1101
+ (0, runtime_1.emitNervesEvent)({
1102
+ level: "warn",
1103
+ component: "senses",
1104
+ event: "senses.teams_proactive_no_aad_id",
1105
+ message: "proactive send skipped: no AAD object ID found",
1106
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
1107
+ });
1108
+ return { delivered: false, reason: "missing_target" };
1109
+ }
1110
+ const internalContentBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(params.text);
1111
+ if (internalContentBlockReason) {
1112
+ (0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
1113
+ friendId: params.friendId,
1114
+ sessionKey: params.sessionKey,
1115
+ reason: internalContentBlockReason,
1116
+ source: "session_send",
1117
+ });
1118
+ return { delivered: false, reason: "internal_content_blocked" };
1119
+ }
1120
+ try {
1121
+ const conversation = await conversations.create({
1122
+ bot: { id: deps.botApi.id },
1123
+ members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
1124
+ tenantId: aadInfo.tenantId,
1125
+ isGroup: false,
1126
+ });
1127
+ await conversations.activities(conversation.id).create({
1128
+ type: "message",
1129
+ text: params.text,
1130
+ });
1131
+ (0, runtime_1.emitNervesEvent)({
1132
+ component: "senses",
1133
+ event: "senses.teams_proactive_sent",
1134
+ message: "proactive teams message sent",
1135
+ meta: { friendId: params.friendId, aadObjectId: aadInfo.aadObjectId, sessionKey: params.sessionKey },
1136
+ });
1137
+ return { delivered: true };
1138
+ }
1139
+ catch (error) {
1140
+ (0, runtime_1.emitNervesEvent)({
1141
+ level: "error",
1142
+ component: "senses",
1143
+ event: "senses.teams_proactive_send_error",
1144
+ message: "proactive teams send failed",
1145
+ meta: {
1146
+ friendId: params.friendId,
1147
+ aadObjectId: aadInfo.aadObjectId,
1148
+ sessionKey: params.sessionKey,
1149
+ reason: error instanceof Error ? error.message : String(error),
1150
+ },
1151
+ });
1152
+ return { delivered: false, reason: "send_error" };
1153
+ }
1154
+ }
773
1155
  function scanPendingTeamsFiles(pendingRoot) {
774
1156
  const results = [];
775
1157
  let friendIds;
@@ -815,8 +1197,7 @@ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
815
1197
  const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
816
1198
  const pendingFiles = scanPendingTeamsFiles(root);
817
1199
  const result = { sent: 0, skipped: 0, failed: 0 };
818
- const conversations = botApi.conversations;
819
- for (const { friendId, filePath, content } of pendingFiles) {
1200
+ for (const { friendId, key, filePath, content } of pendingFiles) {
820
1201
  let parsed;
821
1202
  try {
822
1203
  parsed = JSON.parse(content);
@@ -838,95 +1219,46 @@ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
838
1219
  catch { /* ignore */ }
839
1220
  continue;
840
1221
  }
841
- let friend;
842
- try {
843
- friend = await store.get(friendId);
844
- }
845
- catch {
846
- friend = null;
847
- }
848
- if (!friend) {
1222
+ const internalBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(messageText);
1223
+ if (internalBlockReason) {
849
1224
  result.skipped++;
850
1225
  try {
851
1226
  fs.unlinkSync(filePath);
852
1227
  }
853
1228
  catch { /* ignore */ }
854
- (0, runtime_1.emitNervesEvent)({
855
- level: "warn",
856
- component: "senses",
857
- event: "senses.teams_proactive_no_friend",
858
- message: "proactive send skipped: friend not found",
859
- meta: { friendId },
1229
+ (0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
1230
+ friendId,
1231
+ reason: internalBlockReason,
1232
+ source: "pending_drain",
860
1233
  });
861
1234
  continue;
862
1235
  }
863
- if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
864
- result.skipped++;
1236
+ const sendResult = await sendProactiveTeamsMessageToSession({
1237
+ friendId,
1238
+ sessionKey: key,
1239
+ text: messageText,
1240
+ intent: "generic_outreach",
1241
+ }, {
1242
+ botApi,
1243
+ store,
1244
+ });
1245
+ if (sendResult.delivered) {
1246
+ result.sent++;
865
1247
  try {
866
1248
  fs.unlinkSync(filePath);
867
1249
  }
868
1250
  catch { /* ignore */ }
869
- (0, runtime_1.emitNervesEvent)({
870
- component: "senses",
871
- event: "senses.teams_proactive_trust_skip",
872
- message: "proactive send skipped: trust level not allowed",
873
- meta: { friendId, trustLevel: friend.trustLevel ?? "unknown" },
874
- });
875
1251
  continue;
876
1252
  }
877
- const aadInfo = findAadObjectId(friend);
878
- if (!aadInfo) {
1253
+ if (sendResult.reason === "friend_not_found" || sendResult.reason === "trust_skip" || sendResult.reason === "missing_target") {
879
1254
  result.skipped++;
880
1255
  try {
881
1256
  fs.unlinkSync(filePath);
882
1257
  }
883
1258
  catch { /* ignore */ }
884
- (0, runtime_1.emitNervesEvent)({
885
- level: "warn",
886
- component: "senses",
887
- event: "senses.teams_proactive_no_aad_id",
888
- message: "proactive send skipped: no AAD object ID found",
889
- meta: { friendId },
890
- });
891
1259
  continue;
892
1260
  }
893
- try {
894
- const conversation = await conversations.create({
895
- bot: { id: botApi.id },
896
- members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
897
- tenantId: aadInfo.tenantId,
898
- isGroup: false,
899
- });
900
- await conversations.activities(conversation.id).create({
901
- type: "message",
902
- text: messageText,
903
- });
904
- result.sent++;
905
- try {
906
- fs.unlinkSync(filePath);
907
- }
908
- catch { /* ignore */ }
909
- (0, runtime_1.emitNervesEvent)({
910
- component: "senses",
911
- event: "senses.teams_proactive_sent",
912
- message: "proactive teams message sent",
913
- meta: { friendId, aadObjectId: aadInfo.aadObjectId },
914
- });
915
- }
916
- catch (error) {
917
- result.failed++;
918
- (0, runtime_1.emitNervesEvent)({
919
- level: "error",
920
- component: "senses",
921
- event: "senses.teams_proactive_send_error",
922
- message: "proactive teams send failed",
923
- meta: {
924
- friendId,
925
- aadObjectId: aadInfo.aadObjectId,
926
- reason: error instanceof Error ? error.message : String(error),
927
- },
928
- });
929
- }
1261
+ result.failed++;
930
1262
  }
931
1263
  if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
932
1264
  (0, runtime_1.emitNervesEvent)({