@ouro.bot/cli 0.1.0-alpha.7 → 0.1.0-alpha.700

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 (514) hide show
  1. package/README.md +235 -183
  2. package/RepairGuide.ouro/agent.json +5 -0
  3. package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
  4. package/RepairGuide.ouro/psyche/SOUL.md +55 -0
  5. package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
  6. package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
  7. package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
  8. package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
  9. package/SerpentGuide.ouro/agent.json +83 -0
  10. package/SerpentGuide.ouro/psyche/SOUL.md +25 -0
  11. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +2 -2
  12. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  13. package/assets/ouroboros.png +0 -0
  14. package/changelog.json +4827 -0
  15. package/dist/a2a/card.js +57 -0
  16. package/dist/a2a/client.js +180 -0
  17. package/dist/a2a/config.js +50 -0
  18. package/dist/a2a/delegation-stores.js +56 -0
  19. package/dist/a2a/did-resolution.js +93 -0
  20. package/dist/a2a/identity.js +170 -0
  21. package/dist/a2a/inbound-share.js +107 -0
  22. package/dist/a2a/mission-result-wire.js +196 -0
  23. package/dist/a2a/onboarding.js +115 -0
  24. package/dist/a2a/pin-store.js +132 -0
  25. package/dist/a2a/seen-ledger.js +120 -0
  26. package/dist/a2a/server.js +604 -0
  27. package/dist/a2a/task-store.js +69 -0
  28. package/dist/a2a/transport.js +45 -0
  29. package/dist/a2a/types.js +3 -0
  30. package/dist/arc/attention-types.js +8 -0
  31. package/dist/arc/cares.js +144 -0
  32. package/dist/arc/episodes.js +118 -0
  33. package/dist/arc/evolution.js +490 -0
  34. package/dist/arc/flight-recorder.js +928 -0
  35. package/dist/arc/intentions.js +134 -0
  36. package/dist/arc/json-store.js +117 -0
  37. package/dist/arc/obligations.js +348 -0
  38. package/dist/arc/packets.js +336 -0
  39. package/dist/arc/presence.js +185 -0
  40. package/dist/arc/task-lifecycle.js +57 -0
  41. package/dist/commerce/store.js +755 -0
  42. package/dist/commerce/types.js +3 -0
  43. package/dist/heart/active-work.js +1002 -0
  44. package/dist/heart/agent-entry.js +190 -3
  45. package/dist/heart/attachments/image-normalize.js +194 -0
  46. package/dist/heart/attachments/materialize.js +97 -0
  47. package/dist/heart/attachments/originals.js +88 -0
  48. package/dist/heart/attachments/render.js +29 -0
  49. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  50. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  51. package/dist/heart/attachments/sources/index.js +16 -0
  52. package/dist/heart/attachments/store.js +103 -0
  53. package/dist/heart/attachments/types.js +93 -0
  54. package/dist/heart/auth/auth-flow.js +479 -0
  55. package/dist/heart/autonomy-budget.js +434 -0
  56. package/dist/heart/awaiting/await-alert.js +146 -0
  57. package/dist/heart/awaiting/await-expiry.js +108 -0
  58. package/dist/heart/awaiting/await-loader.js +91 -0
  59. package/dist/heart/awaiting/await-parser.js +141 -0
  60. package/dist/heart/awaiting/await-runtime-state.js +100 -0
  61. package/dist/heart/awaiting/await-scheduler.js +378 -0
  62. package/dist/heart/background-operations.js +281 -0
  63. package/dist/heart/bridges/manager.js +478 -0
  64. package/dist/heart/bridges/state-machine.js +135 -0
  65. package/dist/heart/bridges/store.js +135 -0
  66. package/dist/heart/bundle-state.js +168 -0
  67. package/dist/heart/commitments.js +135 -0
  68. package/dist/heart/config-registry.js +331 -0
  69. package/dist/heart/config.js +172 -135
  70. package/dist/heart/context-loss-gauntlet.js +354 -0
  71. package/dist/heart/context-loss-sentinel.js +1155 -0
  72. package/dist/heart/core.js +1480 -262
  73. package/dist/heart/cross-chat-delivery.js +132 -0
  74. package/dist/heart/daemon/agent-config-check.js +451 -0
  75. package/dist/heart/daemon/agent-discovery.js +263 -0
  76. package/dist/heart/daemon/agent-service.js +523 -0
  77. package/dist/heart/daemon/agentic-repair.js +547 -0
  78. package/dist/heart/daemon/await-private-wake.js +59 -0
  79. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  80. package/dist/heart/daemon/boot-sync-probe.js +197 -0
  81. package/dist/heart/daemon/cadence.js +180 -0
  82. package/dist/heart/daemon/cli-defaults.js +780 -0
  83. package/dist/heart/daemon/cli-desk.js +322 -0
  84. package/dist/heart/daemon/cli-exec.js +8142 -0
  85. package/dist/heart/daemon/cli-help.js +675 -0
  86. package/dist/heart/daemon/cli-parse.js +2461 -0
  87. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  88. package/dist/heart/daemon/cli-render.js +770 -0
  89. package/dist/heart/daemon/cli-types.js +8 -0
  90. package/dist/heart/daemon/connect-bay.js +323 -0
  91. package/dist/heart/daemon/daemon-cli.js +30 -784
  92. package/dist/heart/daemon/daemon-entry.js +874 -13
  93. package/dist/heart/daemon/daemon-health.js +176 -0
  94. package/dist/heart/daemon/daemon-rollup.js +57 -0
  95. package/dist/heart/daemon/daemon-runtime-sync.js +287 -0
  96. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  97. package/dist/heart/daemon/daemon.js +1412 -26
  98. package/dist/heart/daemon/dns-workflow.js +394 -0
  99. package/dist/heart/daemon/doctor-types.js +8 -0
  100. package/dist/heart/daemon/doctor.js +1126 -0
  101. package/dist/heart/daemon/habit-private-wake.js +56 -0
  102. package/dist/heart/daemon/health-monitor.js +142 -11
  103. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  104. package/dist/heart/daemon/hooks/bundle-meta.js +226 -0
  105. package/dist/heart/daemon/http-health-probe.js +80 -0
  106. package/dist/heart/daemon/human-command-screens.js +234 -0
  107. package/dist/heart/daemon/human-readiness.js +114 -0
  108. package/dist/heart/daemon/inner-status.js +78 -0
  109. package/dist/heart/daemon/interactive-repair.js +394 -0
  110. package/dist/heart/daemon/launchd.js +188 -0
  111. package/dist/heart/daemon/log-tailer.js +82 -12
  112. package/dist/heart/daemon/logs-prune.js +110 -0
  113. package/dist/heart/daemon/mcp-canary.js +297 -0
  114. package/dist/heart/daemon/message-router.js +60 -11
  115. package/dist/heart/daemon/migrate-to-desk.js +848 -0
  116. package/dist/heart/daemon/os-cron-deps.js +135 -0
  117. package/dist/heart/daemon/os-cron.js +22 -14
  118. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  119. package/dist/heart/daemon/ouro-entry.js +3 -1
  120. package/dist/heart/daemon/plugin-cli.js +432 -0
  121. package/dist/heart/daemon/process-manager.js +525 -33
  122. package/dist/heart/daemon/provider-discovery.js +137 -0
  123. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  124. package/dist/heart/daemon/pulse.js +511 -0
  125. package/dist/heart/daemon/readiness-repair.js +365 -0
  126. package/dist/heart/daemon/run-hooks.js +39 -0
  127. package/dist/heart/daemon/runtime-logging.js +86 -16
  128. package/dist/heart/daemon/runtime-metadata.js +191 -0
  129. package/dist/{repertoire/tasks/lifecycle.js → heart/daemon/runtime-mode.js} +26 -39
  130. package/dist/heart/daemon/safe-mode.js +161 -0
  131. package/dist/heart/daemon/sense-manager.js +929 -0
  132. package/dist/heart/daemon/session-id-resolver.js +131 -0
  133. package/dist/heart/daemon/skill-management-installer.js +94 -0
  134. package/dist/heart/daemon/socket-client.js +361 -0
  135. package/dist/heart/daemon/stale-bundle-prune.js +113 -0
  136. package/dist/heart/daemon/startup-tui.js +338 -0
  137. package/dist/heart/daemon/task-scheduler.js +117 -39
  138. package/dist/heart/daemon/terminal-ui.js +499 -0
  139. package/dist/heart/daemon/thoughts.js +591 -0
  140. package/dist/heart/daemon/up-progress.js +366 -0
  141. package/dist/heart/daemon/vault-items.js +56 -0
  142. package/dist/heart/delegation.js +59 -0
  143. package/dist/heart/external-events/router.js +121 -0
  144. package/dist/heart/habits/habit-migration.js +189 -0
  145. package/dist/heart/habits/habit-parser.js +214 -0
  146. package/dist/heart/habits/habit-runtime-state.js +114 -0
  147. package/dist/heart/habits/habit-scheduler.js +431 -0
  148. package/dist/heart/habits/habit-session-summary.js +318 -0
  149. package/dist/heart/habits/habit-session.js +617 -0
  150. package/dist/heart/{daemon → hatch}/hatch-animation.js +10 -3
  151. package/dist/heart/{daemon → hatch}/hatch-flow.js +53 -137
  152. package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
  153. package/dist/heart/hatch/specialist-orchestrator.js +129 -0
  154. package/dist/heart/hatch/specialist-prompt.js +102 -0
  155. package/dist/heart/hatch/specialist-tools.js +318 -0
  156. package/dist/heart/identity.js +287 -67
  157. package/dist/heart/kept-notes.js +289 -0
  158. package/dist/heart/kicks.js +2 -20
  159. package/dist/heart/machine-identity.js +161 -0
  160. package/dist/heart/mail-import-discovery.js +353 -0
  161. package/dist/heart/mailbox/mailbox-http-hooks.js +96 -0
  162. package/dist/heart/mailbox/mailbox-http-response.js +7 -0
  163. package/dist/heart/mailbox/mailbox-http-routes.js +375 -0
  164. package/dist/heart/mailbox/mailbox-http-static.js +103 -0
  165. package/dist/heart/mailbox/mailbox-http-transport.js +116 -0
  166. package/dist/heart/mailbox/mailbox-http.js +99 -0
  167. package/dist/heart/mailbox/mailbox-read.js +37 -0
  168. package/dist/heart/mailbox/mailbox-types.js +27 -0
  169. package/dist/heart/mailbox/mailbox-view.js +197 -0
  170. package/dist/heart/mailbox/readers/agent-machine.js +428 -0
  171. package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
  172. package/dist/heart/mailbox/readers/mail.js +375 -0
  173. package/dist/heart/mailbox/readers/runtime-readers.js +815 -0
  174. package/dist/heart/mailbox/readers/sessions.js +232 -0
  175. package/dist/heart/mailbox/readers/shared.js +111 -0
  176. package/dist/heart/mcp/mcp-server.js +696 -0
  177. package/dist/heart/migrate-config.js +100 -0
  178. package/dist/heart/model-capabilities.js +59 -0
  179. package/dist/heart/orientation-frame.js +217 -0
  180. package/dist/heart/platform.js +81 -0
  181. package/dist/heart/primitives.js +1 -1
  182. package/dist/heart/private-runtime/decision-renderer.js +156 -0
  183. package/dist/heart/private-runtime/index.js +18 -0
  184. package/dist/heart/private-runtime/ledger.js +218 -0
  185. package/dist/heart/private-runtime/policy.js +214 -0
  186. package/dist/heart/private-runtime/types.js +2 -0
  187. package/dist/heart/progress-story.js +42 -0
  188. package/dist/heart/provider-attempt.js +134 -0
  189. package/dist/heart/provider-binding-resolver.js +273 -0
  190. package/dist/heart/provider-credentials.js +441 -0
  191. package/dist/heart/provider-failover.js +311 -0
  192. package/dist/heart/provider-models.js +81 -0
  193. package/dist/heart/provider-ping.js +262 -0
  194. package/dist/heart/provider-readiness-cache.js +119 -0
  195. package/dist/heart/provider-visibility.js +188 -0
  196. package/dist/heart/providers/anthropic-token.js +131 -0
  197. package/dist/heart/providers/anthropic.js +202 -50
  198. package/dist/heart/providers/azure.js +104 -13
  199. package/dist/heart/providers/error-classification.js +127 -0
  200. package/dist/heart/providers/github-copilot.js +145 -0
  201. package/dist/heart/providers/minimax-vlm.js +189 -0
  202. package/dist/heart/providers/minimax.js +29 -7
  203. package/dist/heart/providers/openai-codex-token.js +349 -0
  204. package/dist/heart/providers/openai-codex.js +63 -39
  205. package/dist/heart/run-ledger.js +247 -0
  206. package/dist/heart/runtime-capability-check.js +170 -0
  207. package/dist/heart/runtime-credentials.js +398 -0
  208. package/dist/heart/runtime-cwd.js +87 -0
  209. package/dist/heart/sense-truth.js +77 -0
  210. package/dist/heart/session-activity.js +235 -0
  211. package/dist/heart/session-events.js +1133 -0
  212. package/dist/heart/session-playback-cli-main.js +5 -0
  213. package/dist/heart/session-playback-cli.js +36 -0
  214. package/dist/heart/session-playback.js +231 -0
  215. package/dist/heart/session-stats-cli-main.js +5 -0
  216. package/dist/heart/session-stats.js +182 -0
  217. package/dist/heart/session-transcript.js +133 -0
  218. package/dist/heart/start-of-turn-packet.js +365 -0
  219. package/dist/heart/streaming.js +129 -34
  220. package/dist/heart/structured-output.js +196 -0
  221. package/dist/heart/sync-classification.js +176 -0
  222. package/dist/heart/sync.js +449 -0
  223. package/dist/heart/target-resolution.js +127 -0
  224. package/dist/heart/tempo.js +93 -0
  225. package/dist/heart/temporal-view.js +41 -0
  226. package/dist/heart/timeouts.js +101 -0
  227. package/dist/heart/tool-activity-callbacks.js +59 -0
  228. package/dist/heart/tool-description.js +155 -0
  229. package/dist/heart/tool-friction.js +55 -0
  230. package/dist/heart/tool-loop.js +200 -0
  231. package/dist/heart/turn-context.js +472 -0
  232. package/dist/heart/turn-coordinator.js +28 -0
  233. package/dist/heart/versioning/ouro-bot-global-installer.js +129 -0
  234. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  235. package/dist/heart/versioning/ouro-path-installer.js +426 -0
  236. package/dist/heart/{daemon → versioning}/ouro-uti.js +11 -2
  237. package/dist/heart/versioning/ouro-version-manager.js +409 -0
  238. package/dist/heart/versioning/staged-restart.js +146 -0
  239. package/dist/heart/versioning/update-checker.js +116 -0
  240. package/dist/heart/versioning/update-hooks.js +154 -0
  241. package/dist/heart/versioning/wrapper-publish-guard.js +86 -0
  242. package/dist/heart/work-card.js +380 -0
  243. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
  244. package/dist/mailbox-ui/assets/index-J8zDwHmE.js +1 -0
  245. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  246. package/dist/mailbox-ui/index.html +16 -0
  247. package/dist/mailroom/attention.js +167 -0
  248. package/dist/mailroom/autonomy.js +209 -0
  249. package/dist/mailroom/blob-store.js +715 -0
  250. package/dist/mailroom/body-cache.js +61 -0
  251. package/dist/mailroom/core.js +788 -0
  252. package/dist/mailroom/entry.js +160 -0
  253. package/dist/mailroom/file-store.js +568 -0
  254. package/dist/mailroom/mbox-import.js +393 -0
  255. package/dist/mailroom/migration.js +164 -0
  256. package/dist/mailroom/outbound.js +380 -0
  257. package/dist/mailroom/policy.js +263 -0
  258. package/dist/mailroom/reader.js +233 -0
  259. package/dist/mailroom/search-cache.js +334 -0
  260. package/dist/mailroom/search-relevance.js +319 -0
  261. package/dist/mailroom/smtp-ingress.js +176 -0
  262. package/dist/mailroom/source-state.js +176 -0
  263. package/dist/mailroom/thread.js +109 -0
  264. package/dist/mailroom/travel-extract.js +89 -0
  265. package/dist/mind/bundle-manifest.js +93 -1
  266. package/dist/mind/context.js +259 -94
  267. package/dist/mind/desk-section.js +362 -0
  268. package/dist/mind/diary-integrity.js +60 -0
  269. package/dist/mind/{memory.js → diary.js} +90 -98
  270. package/dist/mind/embedding-provider.js +60 -0
  271. package/dist/mind/file-state.js +179 -0
  272. package/dist/mind/first-impressions.js +16 -2
  273. package/dist/mind/{associative-recall.js → note-search.js} +61 -60
  274. package/dist/mind/obligation-steering.js +221 -0
  275. package/dist/mind/pending.js +77 -9
  276. package/dist/mind/phrases.js +1 -0
  277. package/dist/mind/prompt-budget.js +380 -0
  278. package/dist/mind/prompt-refresh.js +3 -2
  279. package/dist/mind/prompt.js +1279 -139
  280. package/dist/mind/provenance-trust.js +26 -0
  281. package/dist/mind/record-paths.js +312 -0
  282. package/dist/mind/scrutiny.js +173 -0
  283. package/dist/mind/token-estimate.js +8 -12
  284. package/dist/nerves/cli-logging.js +22 -3
  285. package/dist/nerves/coverage/audit-rules.js +15 -6
  286. package/dist/nerves/coverage/audit.js +28 -2
  287. package/dist/nerves/coverage/cli.js +1 -1
  288. package/dist/nerves/coverage/contract.js +5 -5
  289. package/dist/nerves/coverage/file-completeness.js +139 -5
  290. package/dist/nerves/coverage/run-artifacts.js +1 -1
  291. package/dist/nerves/event-buffer.js +111 -0
  292. package/dist/nerves/index.js +234 -4
  293. package/dist/nerves/observation.js +20 -0
  294. package/dist/nerves/redact.js +79 -0
  295. package/dist/nerves/review/cli-main.js +5 -0
  296. package/dist/nerves/review/cli.js +156 -0
  297. package/dist/nerves/review/core.js +152 -0
  298. package/dist/nerves/runtime.js +5 -1
  299. package/dist/repertoire/ado-client.js +17 -56
  300. package/dist/repertoire/ado-semantic.js +16 -10
  301. package/dist/repertoire/api-client.js +97 -0
  302. package/dist/repertoire/bitwarden-store.js +1042 -0
  303. package/dist/repertoire/bundle-templates.js +71 -0
  304. package/dist/repertoire/bw-installer.js +180 -0
  305. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  306. package/dist/repertoire/coding/context-pack.js +331 -0
  307. package/dist/repertoire/coding/feedback.js +322 -0
  308. package/dist/repertoire/coding/index.js +18 -3
  309. package/dist/repertoire/coding/manager.js +228 -14
  310. package/dist/repertoire/coding/spawner.js +61 -14
  311. package/dist/repertoire/coding/tools.js +279 -12
  312. package/dist/repertoire/coding/workbench-client.js +272 -0
  313. package/dist/repertoire/coding/workbench-manager.js +346 -0
  314. package/dist/repertoire/commerce-errors.js +109 -0
  315. package/dist/repertoire/commerce-self-test.js +156 -0
  316. package/dist/repertoire/credential-access.js +178 -0
  317. package/dist/repertoire/data/ado-endpoints.json +188 -0
  318. package/dist/repertoire/desk/classifier.js +362 -0
  319. package/dist/repertoire/duffel-client.js +185 -0
  320. package/dist/repertoire/github-client.js +14 -55
  321. package/dist/repertoire/graph-client.js +11 -52
  322. package/dist/repertoire/guardrails.js +408 -0
  323. package/dist/repertoire/mcp-client.js +295 -0
  324. package/dist/repertoire/mcp-manager.js +434 -0
  325. package/dist/repertoire/mcp-tools.js +83 -0
  326. package/dist/repertoire/plugin-mcp.js +175 -0
  327. package/dist/repertoire/plugins.js +253 -0
  328. package/dist/repertoire/shell-sessions.js +133 -0
  329. package/dist/repertoire/skills.js +45 -24
  330. package/dist/repertoire/stripe-client.js +131 -0
  331. package/dist/repertoire/tool-results.js +29 -0
  332. package/dist/repertoire/tools-a2a.js +699 -0
  333. package/dist/repertoire/tools-attachments.js +317 -0
  334. package/dist/repertoire/tools-awaiting.js +372 -0
  335. package/dist/repertoire/tools-base.js +68 -707
  336. package/dist/repertoire/tools-bluebubbles.js +95 -0
  337. package/dist/repertoire/tools-bridge.js +144 -0
  338. package/dist/repertoire/tools-bundle.js +993 -0
  339. package/dist/repertoire/tools-commerce.js +253 -0
  340. package/dist/repertoire/tools-config.js +186 -0
  341. package/dist/repertoire/tools-continuity.js +252 -0
  342. package/dist/repertoire/tools-credential.js +383 -0
  343. package/dist/repertoire/tools-evolution.js +527 -0
  344. package/dist/repertoire/tools-files.js +344 -0
  345. package/dist/repertoire/tools-flight.js +290 -0
  346. package/dist/repertoire/tools-flow.js +119 -0
  347. package/dist/repertoire/tools-github.js +3 -8
  348. package/dist/repertoire/tools-mail.js +1975 -0
  349. package/dist/repertoire/tools-notes.js +459 -0
  350. package/dist/repertoire/tools-obligations.js +182 -0
  351. package/dist/repertoire/tools-orientation.js +31 -0
  352. package/dist/repertoire/tools-record.js +482 -0
  353. package/dist/repertoire/tools-rsvp.js +139 -0
  354. package/dist/repertoire/tools-runtime.js +150 -0
  355. package/dist/repertoire/tools-session.js +881 -0
  356. package/dist/repertoire/tools-shell.js +120 -0
  357. package/dist/repertoire/tools-stripe.js +224 -0
  358. package/dist/repertoire/tools-surface.js +355 -0
  359. package/dist/repertoire/tools-teams.js +67 -61
  360. package/dist/repertoire/tools-travel.js +125 -0
  361. package/dist/repertoire/tools-trip.js +982 -0
  362. package/dist/repertoire/tools-user-profile.js +146 -0
  363. package/dist/repertoire/tools-vault.js +40 -0
  364. package/dist/repertoire/tools-voice.js +145 -0
  365. package/dist/repertoire/tools.js +313 -102
  366. package/dist/repertoire/travel-api-client.js +360 -0
  367. package/dist/repertoire/user-profile.js +131 -0
  368. package/dist/repertoire/vault-setup.js +246 -0
  369. package/dist/repertoire/vault-unlock.js +594 -0
  370. package/dist/rsvp/aisleplanner-client.js +354 -0
  371. package/dist/rsvp/cli.js +704 -0
  372. package/dist/rsvp/config.js +327 -0
  373. package/dist/rsvp/cutover.js +431 -0
  374. package/dist/rsvp/diagnostics.js +222 -0
  375. package/dist/rsvp/diff-renderer.js +161 -0
  376. package/dist/rsvp/habit-policy.js +85 -0
  377. package/dist/rsvp/habit-stage.js +93 -0
  378. package/dist/rsvp/incident-bundle.js +97 -0
  379. package/dist/rsvp/migration.js +204 -0
  380. package/dist/rsvp/outbound-state.js +254 -0
  381. package/dist/rsvp/query.js +118 -0
  382. package/dist/rsvp/replay.js +233 -0
  383. package/dist/rsvp/snapshot.js +232 -0
  384. package/dist/scripts/claude-code-hook.js +41 -0
  385. package/dist/scripts/claude-code-stop-hook.js +47 -0
  386. package/dist/senses/a2a-entry.js +115 -0
  387. package/dist/senses/attention-queue.js +186 -0
  388. package/dist/senses/await-turn-message.js +58 -0
  389. package/dist/senses/bluebubbles/active-turns.js +216 -0
  390. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  391. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  392. package/dist/senses/bluebubbles/client.js +691 -0
  393. package/dist/senses/bluebubbles/entry.js +77 -0
  394. package/dist/senses/bluebubbles/inbound-log.js +126 -0
  395. package/dist/senses/bluebubbles/index.js +2937 -0
  396. package/dist/senses/bluebubbles/media.js +389 -0
  397. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +45 -16
  398. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +46 -6
  399. package/dist/senses/bluebubbles/outbound-state.js +304 -0
  400. package/dist/senses/bluebubbles/processed-log.js +133 -0
  401. package/dist/senses/bluebubbles/replay.js +284 -0
  402. package/dist/senses/bluebubbles/runtime-state.js +137 -0
  403. package/dist/senses/bluebubbles/session-cleanup.js +72 -0
  404. package/dist/senses/bluebubbles-meta-guard.js +39 -0
  405. package/dist/senses/cli/bracketed-paste.js +82 -0
  406. package/dist/senses/cli/image-paste.js +287 -0
  407. package/dist/senses/cli/image-ref-navigation.js +75 -0
  408. package/dist/senses/cli/ink-app.js +156 -0
  409. package/dist/senses/cli/inline-diff.js +64 -0
  410. package/dist/senses/cli/input-keys.js +174 -0
  411. package/dist/senses/cli/kill-ring.js +86 -0
  412. package/dist/senses/cli/message-list.js +51 -0
  413. package/dist/senses/cli/ouro-tui.js +607 -0
  414. package/dist/senses/cli/spinner-imperative.js +135 -0
  415. package/dist/senses/cli/spinner.js +101 -0
  416. package/dist/senses/cli/status-line.js +60 -0
  417. package/dist/senses/cli/streaming-markdown.js +526 -0
  418. package/dist/senses/cli/tool-display.js +85 -0
  419. package/dist/senses/cli/tool-render.js +85 -0
  420. package/dist/senses/cli/tui-store.js +240 -0
  421. package/dist/senses/cli/virtual-list.js +35 -0
  422. package/dist/senses/cli-entry.js +60 -8
  423. package/dist/senses/cli-layout.js +187 -0
  424. package/dist/senses/cli.js +780 -269
  425. package/dist/senses/commands.js +66 -3
  426. package/dist/senses/context-packet-ledger.js +245 -0
  427. package/dist/senses/context-packets.js +154 -0
  428. package/dist/senses/continuity.js +94 -0
  429. package/dist/senses/habit-turn-message.js +160 -0
  430. package/dist/senses/mail-entry.js +66 -0
  431. package/dist/senses/mail.js +395 -0
  432. package/dist/senses/pipeline.js +1052 -0
  433. package/dist/senses/private-runtime-worker.js +723 -0
  434. package/dist/senses/private-runtime.js +1157 -0
  435. package/dist/senses/proactive-content-guard.js +51 -0
  436. package/dist/senses/shared-turn.js +416 -0
  437. package/dist/senses/surface-tool.js +109 -0
  438. package/dist/senses/teams-entry.js +60 -8
  439. package/dist/senses/teams.js +928 -199
  440. package/dist/senses/trust-gate.js +207 -2
  441. package/dist/senses/voice/audio-playback.js +237 -0
  442. package/dist/senses/voice/audio-routing.js +119 -0
  443. package/dist/senses/voice/elevenlabs.js +202 -0
  444. package/dist/senses/voice/floor-control.js +431 -0
  445. package/dist/senses/voice/floor-controller.js +115 -0
  446. package/dist/senses/voice/golden-path.js +116 -0
  447. package/dist/senses/voice/index.js +29 -0
  448. package/dist/senses/voice/meeting.js +113 -0
  449. package/dist/senses/voice/outbound.js +190 -0
  450. package/dist/senses/voice/phone.js +33 -0
  451. package/dist/senses/voice/playback.js +139 -0
  452. package/dist/senses/voice/realtime-eval.js +496 -0
  453. package/dist/senses/voice/realtime-trace.js +531 -0
  454. package/dist/senses/voice/transcript.js +70 -0
  455. package/dist/senses/voice/turn.js +191 -0
  456. package/dist/senses/voice/twilio-phone-runtime.js +807 -0
  457. package/dist/senses/voice/twilio-phone.js +5081 -0
  458. package/dist/senses/voice/types.js +2 -0
  459. package/dist/senses/voice/whisper.js +161 -0
  460. package/dist/senses/voice-entry.js +81 -0
  461. package/dist/senses/voice-realtime-eval-command.js +99 -0
  462. package/dist/senses/voice-realtime-eval-entry.js +21 -0
  463. package/dist/senses/voice-twilio-entry.js +87 -0
  464. package/dist/trips/core.js +138 -0
  465. package/dist/trips/store.js +265 -0
  466. package/dist/util/frontmatter.js +69 -0
  467. package/package.json +67 -11
  468. package/skills/agent-commerce.md +113 -0
  469. package/skills/browser-navigation.md +117 -0
  470. package/skills/commerce-setup-guide.md +116 -0
  471. package/skills/commerce-setup.md +84 -0
  472. package/skills/configure-dev-tools.md +99 -0
  473. package/skills/travel-planning.md +138 -0
  474. package/AdoptionSpecialist.ouro/agent.json +0 -20
  475. package/AdoptionSpecialist.ouro/psyche/SOUL.md +0 -22
  476. package/dist/heart/daemon/specialist-orchestrator.js +0 -160
  477. package/dist/heart/daemon/specialist-prompt.js +0 -40
  478. package/dist/heart/daemon/specialist-session.js +0 -142
  479. package/dist/heart/daemon/specialist-tools.js +0 -128
  480. package/dist/heart/daemon/subagent-installer.js +0 -125
  481. package/dist/inner-worker-entry.js +0 -4
  482. package/dist/mind/friends/channel.js +0 -49
  483. package/dist/mind/friends/resolver.js +0 -84
  484. package/dist/mind/friends/store-file.js +0 -171
  485. package/dist/mind/friends/store.js +0 -4
  486. package/dist/mind/friends/tokens.js +0 -26
  487. package/dist/mind/friends/types.js +0 -21
  488. package/dist/repertoire/tasks/board.js +0 -122
  489. package/dist/repertoire/tasks/index.js +0 -210
  490. package/dist/repertoire/tasks/middleware.js +0 -65
  491. package/dist/repertoire/tasks/parser.js +0 -173
  492. package/dist/repertoire/tasks/scanner.js +0 -132
  493. package/dist/repertoire/tasks/transitions.js +0 -145
  494. package/dist/senses/bluebubbles-client.js +0 -279
  495. package/dist/senses/bluebubbles-entry.js +0 -11
  496. package/dist/senses/bluebubbles.js +0 -332
  497. package/dist/senses/inner-dialog-worker.js +0 -61
  498. package/dist/senses/inner-dialog.js +0 -231
  499. package/subagents/README.md +0 -73
  500. package/subagents/work-doer.md +0 -233
  501. package/subagents/work-merger.md +0 -624
  502. package/subagents/work-planner.md +0 -373
  503. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  504. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  505. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  506. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  507. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  508. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  509. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  510. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  511. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  512. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  513. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  514. /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
@@ -34,16 +34,24 @@ 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;
47
+ exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
43
48
  exports.startTeamsApp = startTeamsApp;
49
+ const fs = __importStar(require("fs"));
44
50
  const teams_apps_1 = require("@microsoft/teams.apps");
45
51
  const teams_dev_1 = require("@microsoft/teams.dev");
46
52
  const core_1 = require("../heart/core");
53
+ const tools_1 = require("../repertoire/tools");
54
+ const friends_1 = require("@ouro.bot/friends");
47
55
  const config_1 = require("../heart/config");
48
56
  const prompt_1 = require("../mind/prompt");
49
57
  const phrases_1 = require("../mind/phrases");
@@ -53,13 +61,30 @@ const context_1 = require("../mind/context");
53
61
  const commands_1 = require("./commands");
54
62
  const nerves_1 = require("../nerves");
55
63
  const runtime_1 = require("../nerves/runtime");
56
- const store_file_1 = require("../mind/friends/store-file");
57
- const resolver_1 = require("../mind/friends/resolver");
58
- const tokens_1 = require("../mind/friends/tokens");
64
+ const proactive_content_guard_1 = require("./proactive-content-guard");
59
65
  const turn_coordinator_1 = require("../heart/turn-coordinator");
60
66
  const identity_1 = require("../heart/identity");
67
+ const mcp_manager_1 = require("../repertoire/mcp-manager");
68
+ const progress_story_1 = require("../heart/progress-story");
69
+ const tool_activity_callbacks_1 = require("../heart/tool-activity-callbacks");
70
+ const commands_2 = require("./commands");
71
+ const http = __importStar(require("http"));
61
72
  const path = __importStar(require("path"));
62
73
  const trust_gate_1 = require("./trust-gate");
74
+ const pipeline_1 = require("./pipeline");
75
+ const teamsFailoverStates = new Map();
76
+ const pending_1 = require("../mind/pending");
77
+ const continuity_1 = require("./continuity");
78
+ // AIGeneratedContent entity and feedbackLoopEnabled channelData for all outbound
79
+ // Teams messages. Required by Teams AI UX best practices.
80
+ function aiLabelEntities() {
81
+ return [{
82
+ type: "https://schema.org/Message",
83
+ "@type": "Message",
84
+ "@context": "https://schema.org",
85
+ additionalType: ["AIGeneratedContent"],
86
+ }];
87
+ }
63
88
  // Strip @mention markup from incoming messages.
64
89
  // Removes <at>...</at> tags and trims extra whitespace.
65
90
  // Fallback safety net -- the SDK's activity.mentions.stripText should handle
@@ -114,6 +139,46 @@ function splitMessage(text, maxLen) {
114
139
  }
115
140
  return chunks;
116
141
  }
142
+ // Sanitize user-provided feedback comments: truncate, strip control chars and newlines.
143
+ function sanitizeFeedbackComment(comment) {
144
+ const cleaned = comment.replace(/[\x00-\x1f\n\r]/g, "");
145
+ return cleaned.length > 200 ? cleaned.slice(0, 200) : cleaned;
146
+ }
147
+ // Build synthetic message text from a Teams feedback reaction.
148
+ function buildFeedbackSyntheticText(reaction, comment) {
149
+ const emoji = reaction === "like" ? "thumbs-up" : "thumbs-down";
150
+ if (comment) {
151
+ const sanitized = sanitizeFeedbackComment(comment);
152
+ return `[reacted with ${emoji} to your message: "${sanitized}"]`;
153
+ }
154
+ return `[reacted with ${emoji} to your message]`;
155
+ }
156
+ // Build a welcome Adaptive Card with prompt starters for new bot installs.
157
+ function buildWelcomeCard() {
158
+ const promptStarters = [
159
+ "What can you help me with?",
160
+ "Tell me about yourself",
161
+ "What's on my calendar today?",
162
+ "Summarize my recent emails",
163
+ ];
164
+ return {
165
+ type: "AdaptiveCard",
166
+ version: "1.5",
167
+ body: [
168
+ {
169
+ type: "TextBlock",
170
+ text: "Hey! I'm here and ready to help. Try one of these to get started, or just ask me anything.",
171
+ wrap: true,
172
+ size: "Medium",
173
+ },
174
+ ],
175
+ actions: promptStarters.map((prompt) => ({
176
+ type: "Action.Submit",
177
+ title: prompt,
178
+ data: { msteams: { type: "messageBack", text: prompt, displayText: prompt } },
179
+ })),
180
+ };
181
+ }
117
182
  // Create Teams-specific callbacks for the agent loop.
118
183
  // The SDK handles cumulative text, debouncing (500ms), and the streaming
119
184
  // protocol (streamSequence, streamId, informative/streaming/final types).
@@ -126,12 +191,16 @@ function splitMessage(text, maxLen) {
126
191
  // (transient status) or safeSend (terminal errors). Reasoning is accumulated
127
192
  // and periodically pushed via safeUpdate on the same flush timer tick.
128
193
  function createTeamsCallbacks(stream, controller, sendMessage, options) {
194
+ const MIN_INITIAL_CHARS = 20;
129
195
  let stopped = false; // set when stream signals cancellation (403)
130
196
  let hadToolRun = false;
131
197
  let hadRealOutput = false; // true once reasoning/tool output shown; suppresses phrases
132
198
  let reasoningBuf = ""; // accumulated reasoning text for status display
199
+ let totalEmitted = 0; // cumulative chars emitted via safeEmit (for >4000 finalization)
200
+ let streamFinalized = false; // true after stream.close() — subsequent flushes go to safeSend
133
201
  let textBuffer = ""; // accumulated text output for chunked streaming
134
202
  let streamHasContent = false; // tracks whether primary output has received content
203
+ let firstContentEmitted = false; // true after first content push — disables MIN_INITIAL_CHARS threshold
135
204
  let phraseTimer = null;
136
205
  let lastPhrase = "";
137
206
  let flushTimer = null;
@@ -181,15 +250,16 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
181
250
  result.catch(() => markStopped());
182
251
  }
183
252
  }
184
- // Safely emit a text delta to the stream.
253
+ // Safely emit a text delta to the stream with AI labels.
185
254
  // On error (e.g. 403 from Teams stop button), abort the controller.
186
255
  function safeEmit(text) {
187
256
  /* v8 ignore next -- defensive guard: stopped set by prior 403; tested via flush abort path @preserve */
188
257
  if (stopped)
189
258
  return;
190
259
  try {
191
- catchAsync(stream.emit(text));
260
+ catchAsync(stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } }));
192
261
  streamHasContent = true;
262
+ totalEmitted += text.length;
193
263
  }
194
264
  catch {
195
265
  markStopped();
@@ -204,7 +274,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
204
274
  try {
205
275
  // stream.emit() is typed as void but the Teams SDK returns a Promise
206
276
  // internally (async HTTP). Cast to capture the result for awaiting.
207
- const result = stream.emit(text);
277
+ const result = stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
208
278
  streamHasContent = true;
209
279
  if (result && typeof result.then === "function") {
210
280
  await result;
@@ -216,6 +286,26 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
216
286
  return false;
217
287
  }
218
288
  }
289
+ // Non-aborting awaitable emit — returns true on success, false on failure WITHOUT
290
+ // calling markStopped() / aborting the controller. Used by flushNow (speak) so a
291
+ // primary-stream failure followed by a successful sendMessage fallback does NOT
292
+ // poison the rest of the turn. tryEmit's abort-on-failure behavior is correct for
293
+ // end-of-turn flush() (no fallback path forward) but wrong for mid-turn speak,
294
+ // which has a sendMessage fallback that may still succeed. Caller (flushNow) is
295
+ // responsible for the `!stopped` precondition; no defensive guard here.
296
+ async function tryEmitNoAbort(text) {
297
+ try {
298
+ const result = stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
299
+ streamHasContent = true;
300
+ if (result && typeof result.then === "function") {
301
+ await result;
302
+ }
303
+ return true;
304
+ }
305
+ catch {
306
+ return false;
307
+ }
308
+ }
219
309
  // Safely send a status update to the stream.
220
310
  // On error (e.g. 403 from Teams stop button), abort the controller.
221
311
  function safeUpdate(text) {
@@ -260,11 +350,49 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
260
350
  // emitted text into a single streaming message (cumulative), so every
261
351
  // periodic flush appends to the same response — not separate messages.
262
352
  // No preemptive splitting — sends full text. Error recovery happens in flush().
353
+ // Hybrid MIN_INITIAL_CHARS: hold back until >= MIN_INITIAL_CHARS accumulated
354
+ // before the first content emit, so phrase rotation shows while real content
355
+ // buffers. After first emit, flush normally (no threshold).
263
356
  function flushTextBuffer() {
264
357
  if (!textBuffer)
265
358
  return;
359
+ if (!firstContentEmitted && textBuffer.length < MIN_INITIAL_CHARS)
360
+ return;
361
+ // Proactive >4000 finalization: if cumulative emitted + buffer >= RECOVERY_CHUNK_SIZE,
362
+ // finalize the stream and send overflow via safeSend (follow-up message).
363
+ if (!streamFinalized && totalEmitted + textBuffer.length >= RECOVERY_CHUNK_SIZE) {
364
+ const remaining = RECOVERY_CHUNK_SIZE - totalEmitted;
365
+ /* v8 ignore next 2 -- defensive: remaining always > 0 because finalization runs once @preserve */
366
+ if (remaining > 0)
367
+ safeEmit(textBuffer.slice(0, remaining));
368
+ try {
369
+ stream.close();
370
+ }
371
+ catch { /* stream may already be dead */ }
372
+ streamFinalized = true;
373
+ /* v8 ignore next -- defensive ternary: remaining always > 0 at first finalization @preserve */
374
+ const overflow = textBuffer.slice(remaining > 0 ? remaining : 0);
375
+ textBuffer = "";
376
+ if (overflow)
377
+ safeSend(overflow);
378
+ if (!firstContentEmitted) {
379
+ firstContentEmitted = true;
380
+ stopPhraseRotation();
381
+ }
382
+ return;
383
+ }
384
+ if (streamFinalized) {
385
+ // After finalization, all content goes to safeSend (follow-up messages)
386
+ safeSend(textBuffer);
387
+ textBuffer = "";
388
+ return;
389
+ }
266
390
  safeEmit(textBuffer);
267
391
  textBuffer = "";
392
+ if (!firstContentEmitted) {
393
+ firstContentEmitted = true;
394
+ stopPhraseRotation();
395
+ }
268
396
  }
269
397
  function startPhraseRotation(pool) {
270
398
  stopPhraseRotation();
@@ -307,25 +435,108 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
307
435
  onTextChunk: (text) => {
308
436
  if (stopped)
309
437
  return;
310
- stopPhraseRotation();
438
+ // Don't stop phrase rotation here — let it continue until first content
439
+ // emit (handled in flushTextBuffer when MIN_INITIAL_CHARS threshold met).
311
440
  textBuffer += text;
312
441
  startFlushTimer();
313
442
  },
314
443
  onClearText: () => {
315
444
  textBuffer = "";
316
445
  },
317
- onToolStart: (name, args) => {
318
- stopPhraseRotation();
319
- flushTextBuffer();
320
- const argSummary = Object.values(args).join(", ");
321
- safeUpdate(`running ${name} (${argSummary})...`);
322
- hadToolRun = true;
323
- },
324
- onToolEnd: (name, summary, success) => {
446
+ flushNow: async () => {
447
+ const trimmed = textBuffer.trim();
448
+ if (!trimmed)
449
+ return;
450
+ // Cancel pending periodic flush — we're delivering now.
451
+ stopFlushTimer();
452
+ // The actual speak message replaces any "thinking..." phrase cycling.
325
453
  stopPhraseRotation();
326
- const msg = (0, format_1.formatToolResult)(name, summary, success);
327
- safeUpdate(msg);
454
+ // Bypass MIN_INITIAL_CHARS threshold — speak delivers immediately.
455
+ firstContentEmitted = true;
456
+ textBuffer = "";
457
+ // Try the stream first via the NON-ABORTING variant; on failure, fall back
458
+ // to sendMessage. Critical: do NOT call markStopped() / abort the controller
459
+ // when only the primary stream fails — the sendMessage fallback may still
460
+ // deliver the speak, and a successful fallback must not poison the rest of
461
+ // the turn. Only abort when ALL delivery paths fail (handled below).
462
+ // Contract: throws if the message could not be delivered through any available path.
463
+ let delivered = false;
464
+ let lastError = null;
465
+ if (!stopped) {
466
+ const ok = await tryEmitNoAbort(trimmed);
467
+ if (ok)
468
+ delivered = true;
469
+ else
470
+ lastError = new Error("stream emit failed");
471
+ }
472
+ if (!delivered && sendMessage) {
473
+ try {
474
+ await sendMessage(trimmed);
475
+ delivered = true;
476
+ lastError = null;
477
+ }
478
+ catch (err) {
479
+ lastError = err instanceof Error ? err : new Error(String(err));
480
+ }
481
+ }
482
+ (0, runtime_1.emitNervesEvent)({
483
+ component: "senses",
484
+ event: "teams.speak_flush",
485
+ message: "teams flushed mid-turn speak",
486
+ meta: { messageLength: trimmed.length, delivered },
487
+ });
488
+ if (!delivered) {
489
+ // All delivery paths exhausted — now it is correct to abort the turn.
490
+ // markStopped() halts further stream activity and aborts the controller
491
+ // so the engine catches up and ends the turn cleanly.
492
+ markStopped();
493
+ throw new Error(`teams speak delivery failed: ${lastError?.message ?? "no fallback available"}`);
494
+ }
328
495
  },
496
+ ...(() => {
497
+ const toolCbs = (0, tool_activity_callbacks_1.createToolActivityCallbacks)({
498
+ onDescription: (text) => safeUpdate(text),
499
+ /* v8 ignore next -- onResult only called in debug mode; tested via tool-activity-callbacks.test.ts @preserve */
500
+ onResult: (text) => safeUpdate(text),
501
+ /* v8 ignore next -- onFailure tested via onToolEnd failure test @preserve */
502
+ onFailure: (text) => safeUpdate(text),
503
+ isDebug: commands_2.getDebugMode,
504
+ });
505
+ return {
506
+ onToolStart: (name, args) => {
507
+ // speak is flow-control: its visible output is the message itself
508
+ // (delivered via onTextChunk + flushNow). Do NOT stop phrase rotation
509
+ // here, do NOT emit the \u23f3 placeholder, do NOT post a tool-activity
510
+ // status update \u2014 all of those would create UI churn right before the
511
+ // actual speak content arrives.
512
+ if (name === "speak")
513
+ return;
514
+ stopPhraseRotation();
515
+ // Force-flush any accumulated text, bypassing MIN_INITIAL_CHARS threshold
516
+ firstContentEmitted = true;
517
+ flushTextBuffer();
518
+ // Emit a placeholder to satisfy the 15s Copilot timeout for initial
519
+ // stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
520
+ // never emit before the timeout and the user sees "this response was
521
+ // stopped". The placeholder is replaced by actual content on next emit.
522
+ // https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
523
+ if (!streamHasContent)
524
+ safeEmit("\u23f3");
525
+ toolCbs.onToolStart(name, args);
526
+ hadToolRun = true;
527
+ },
528
+ onToolEnd: (name, summary, success) => {
529
+ // speak is flow-control: skip phrase-rotation stop and tool-activity end
530
+ // callback (no safeUpdate for \u2713/\u2717). The flushNow call inside the engine
531
+ // already emitted the actual message and stopped any rotation as part of
532
+ // tryEmit's first-content-emitted flag.
533
+ if (name === "speak")
534
+ return;
535
+ stopPhraseRotation();
536
+ toolCbs.onToolEnd(name, summary, success);
537
+ },
538
+ };
539
+ })(),
329
540
  onKick: () => {
330
541
  stopPhraseRotation();
331
542
  const msg = (0, format_1.formatKick)();
@@ -335,7 +546,11 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
335
546
  stopPhraseRotation();
336
547
  if (stopped)
337
548
  return;
338
- const msg = (0, format_1.formatError)(error);
549
+ const msg = (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
550
+ scope: "shared-work",
551
+ phase: "errored",
552
+ outcomeText: (0, format_1.formatError)(error),
553
+ }));
339
554
  if (severity === "transient") {
340
555
  safeUpdate(msg);
341
556
  }
@@ -343,38 +558,27 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
343
558
  safeSend(msg);
344
559
  }
345
560
  },
346
- onConfirmAction: options?.conversationId
347
- ? async (name, args) => {
348
- const convId = options.conversationId;
349
- const argsDesc = Object.entries(args).map(([k, v]) => `${k}: ${v}`).join(", ");
350
- safeUpdate(`Confirm action: ${name} (${argsDesc}) -- reply "yes" to confirm or "no" to cancel`);
351
- return new Promise((resolve) => {
352
- _pendingConfirmations.set(convId, resolve);
353
- // Auto-deny after 2 minutes to prevent indefinite blocking
354
- // (e.g. when the stream dies and the user never sees the prompt).
355
- setTimeout(() => {
356
- if (_pendingConfirmations.has(convId)) {
357
- _pendingConfirmations.delete(convId);
358
- resolve("denied");
359
- }
360
- }, 120_000);
361
- });
362
- }
363
- : undefined,
364
561
  flush: async () => {
365
562
  stopFlushTimer();
563
+ stopPhraseRotation();
366
564
  if (textBuffer) {
565
+ // Bypass MIN_INITIAL_CHARS threshold — flush delivers all remaining content
566
+ firstContentEmitted = true;
367
567
  const text = textBuffer;
368
568
  textBuffer = "";
369
- if (!stopped) {
569
+ if (streamFinalized && sendMessage) {
570
+ // Stream already finalized (>4000 path) — send remaining content as follow-up
571
+ safeSend(text);
572
+ }
573
+ else if (!stopped) {
370
574
  // Stream is alive — await the emit so we can catch async 413/failure
371
575
  // and fall through to sendMessage recovery.
372
576
  const ok = await tryEmit(text);
373
577
  if (!ok)
374
578
  markStopped();
375
579
  }
376
- if (stopped && sendMessage) {
377
- // Stream is dead — fall back to sendMessage; split on failure as recovery.
580
+ if (stopped && !streamFinalized && sendMessage) {
581
+ // Stream is dead (not from finalization) — fall back to sendMessage; split on failure as recovery.
378
582
  try {
379
583
  await sendMessage(text);
380
584
  }
@@ -385,32 +589,12 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
385
589
  }
386
590
  }
387
591
  }
388
- else if (!streamHasContent) {
389
- safeEmit("(completed with tool calls only \u2014 no text response)");
592
+ else if (!streamHasContent && !options?.suppressEmptyStreamMessage) {
593
+ safeEmit("(completed with tool calls only — no text response)");
390
594
  }
391
595
  },
392
596
  };
393
597
  }
394
- // Per-conversation pending confirmation resolvers.
395
- // When a mutate tool needs confirmation, the resolver is stored here.
396
- // The next message from the same conversation resolves it.
397
- const _pendingConfirmations = new Map();
398
- // Confirmation response words (case-insensitive)
399
- const CONFIRM_WORDS = new Set(["yes", "confirm", "go", "y", "ok", "approve", "proceed"]);
400
- function resolvePendingConfirmation(convId, text) {
401
- const resolver = _pendingConfirmations.get(convId);
402
- if (!resolver)
403
- return false;
404
- _pendingConfirmations.delete(convId);
405
- const word = text.trim().toLowerCase();
406
- if (CONFIRM_WORDS.has(word)) {
407
- resolver("confirmed");
408
- }
409
- else {
410
- resolver("denied");
411
- }
412
- return true;
413
- }
414
598
  const _turnCoordinator = (0, turn_coordinator_1.createTurnCoordinator)();
415
599
  function teamsTurnKey(conversationId) {
416
600
  return `teams:${conversationId}`;
@@ -422,160 +606,316 @@ async function withConversationLock(convId, fn) {
422
606
  // are deleted while the process is alive.
423
607
  function getFriendStore() {
424
608
  const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
425
- return new store_file_1.FileFriendStore(friendsPath);
609
+ return new friends_1.FileFriendStore(friendsPath);
610
+ }
611
+ function createTeamsCommandRegistry() {
612
+ const registry = (0, commands_1.createCommandRegistry)();
613
+ (0, commands_1.registerDefaultCommands)(registry);
614
+ return registry;
615
+ }
616
+ /* v8 ignore start -- superseding follow-up slash command handler; tested via startTeamsApp integration tests @preserve */
617
+ function handleTeamsSlashCommand(text, registry, friendId, conversationId, stream, emitResponse = true) {
618
+ const parsed = (0, commands_1.parseSlashCommand)(text);
619
+ if (!parsed)
620
+ return null;
621
+ const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
622
+ if (!dispatchResult.handled || !dispatchResult.result) {
623
+ return null;
624
+ }
625
+ if (dispatchResult.result.action === "new") {
626
+ (0, context_1.deleteSession)((0, config_2.sessionPath)(friendId, "teams", conversationId));
627
+ if (emitResponse) {
628
+ stream.emit("session cleared");
629
+ }
630
+ return "new";
631
+ }
632
+ if (dispatchResult.result.action === "response") {
633
+ if (emitResponse) {
634
+ stream.emit(dispatchResult.result.message || "");
635
+ }
636
+ return "response";
637
+ }
638
+ return null;
426
639
  }
640
+ /* v8 ignore stop */
427
641
  // Handle an incoming Teams message
428
- async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage) {
642
+ async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage, reactionOverrides) {
429
643
  const turnKey = teamsTurnKey(conversationId);
430
644
  // NOTE: Confirmation resolution is handled in the app.on("message") handler
431
645
  // BEFORE the conversation lock. By the time we get here, any pending
432
646
  // confirmation has already been resolved and the reply consumed.
433
647
  // Send first thinking phrase immediately so the user sees feedback
434
648
  // before sync I/O (session load, trim) blocks the event loop.
435
- stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
649
+ // Skip for reaction signals — they should be processed quietly.
650
+ if (!reactionOverrides) {
651
+ stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
652
+ }
436
653
  await new Promise(r => setImmediate(r));
437
- // Resolve context kernel (identity + channel) early so we can use the friend UUID for session path
654
+ // Resolve identity provider early for friend resolution + slash command session path
438
655
  const store = getFriendStore();
439
656
  const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
440
657
  const externalId = teamsContext?.aadObjectId || conversationId;
441
- const toolContext = teamsContext ? {
658
+ // Build FriendResolver for the pipeline
659
+ const resolver = new friends_1.FriendResolver(store, {
660
+ provider,
661
+ externalId,
662
+ tenantId: teamsContext?.tenantId,
663
+ displayName: teamsContext?.displayName || "Unknown",
664
+ channel: "teams",
665
+ });
666
+ // Pre-resolve friend for session path + slash commands (pipeline will re-use the cached result)
667
+ const resolvedContext = await resolver.resolve();
668
+ const friendId = resolvedContext.friend.id;
669
+ // ── Teams adapter concerns: controller, callbacks, session path ──────────
670
+ const controller = new AbortController();
671
+ const channelConfig = (0, config_2.getTeamsChannelConfig)();
672
+ const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs, ...(reactionOverrides?.suppressEmptyStreamMessage ? { suppressEmptyStreamMessage: true } : {}) });
673
+ const traceId = (0, nerves_1.createTraceId)();
674
+ const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
675
+ const teamsCapabilities = (0, friends_1.getChannelCapabilities)("teams");
676
+ const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "teams", conversationId);
677
+ // Build Teams-specific toolContext fields for injection into the pipeline
678
+ const teamsToolContext = teamsContext ? {
442
679
  graphToken: teamsContext.graphToken,
443
680
  adoToken: teamsContext.adoToken,
444
681
  githubToken: teamsContext.githubToken,
445
682
  signin: teamsContext.signin,
446
- friendStore: store,
447
- summarize: (0, core_1.createSummarize)(),
448
- } : undefined;
449
- if (toolContext) {
450
- const resolver = new resolver_1.FriendResolver(store, {
451
- provider,
452
- externalId,
453
- tenantId: teamsContext?.tenantId,
454
- displayName: teamsContext?.displayName || "Unknown",
683
+ summarize: (0, core_1.createSummarize)("human"),
684
+ tenantId: teamsContext.tenantId,
685
+ botApi: teamsContext.botApi,
686
+ } : {};
687
+ let currentText = text;
688
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
689
+ while (true) {
690
+ let drainedSteeringFollowUps = [];
691
+ // Build runAgentOptions with Teams-specific fields
692
+ const agentOptions = {
693
+ traceId,
694
+ toolContext: teamsToolContext,
695
+ mcpManager,
696
+ drainSteeringFollowUps: () => {
697
+ drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
698
+ .map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
699
+ return drainedSteeringFollowUps;
700
+ },
701
+ ...(reactionOverrides?.isReactionSignal ? { isReactionSignal: true } : {}),
702
+ };
703
+ // ── Call shared pipeline ──────────────────────────────────────────
704
+ // Capture terminal errors — failover message replaces the error card if it triggers
705
+ let capturedTerminalError = null;
706
+ const teamsFailoverState = (() => {
707
+ if (!teamsFailoverStates.has(conversationId)) {
708
+ teamsFailoverStates.set(conversationId, { pending: null });
709
+ }
710
+ return teamsFailoverStates.get(conversationId);
711
+ })();
712
+ /* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
713
+ const failoverAwareCallbacks = {
714
+ ...callbacks,
715
+ onError: (error, severity) => {
716
+ if (severity === "terminal" && teamsFailoverState) {
717
+ capturedTerminalError = error;
718
+ return;
719
+ }
720
+ callbacks.onError(error, severity);
721
+ },
722
+ };
723
+ /* v8 ignore stop */
724
+ const result = await (0, pipeline_1.handleInboundTurn)({
455
725
  channel: "teams",
456
- });
457
- toolContext.context = await resolver.resolve();
458
- }
459
- const friendId = toolContext?.context?.friend?.id || "default";
460
- if (toolContext?.context?.friend) {
461
- const trustGate = (0, trust_gate_1.enforceTrustGate)({
462
- friend: toolContext.context.friend,
726
+ sessionKey: conversationId,
727
+ capabilities: teamsCapabilities,
728
+ messages: [{ role: "user", content: currentText }],
729
+ continuityIngressTexts: [currentText],
730
+ callbacks: failoverAwareCallbacks,
731
+ friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
732
+ sessionLoader: {
733
+ loadOrCreate: async () => {
734
+ const existing = (0, context_1.loadSession)(sessPath);
735
+ const messages = existing?.messages && existing.messages.length > 0
736
+ ? existing.messages
737
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("teams", {}, resolvedContext)) }];
738
+ (0, core_1.repairOrphanedToolCalls)(messages);
739
+ return {
740
+ messages,
741
+ sessionPath: sessPath,
742
+ state: existing?.state,
743
+ events: existing?.events,
744
+ structuredOutputs: existing?.structuredOutputs,
745
+ };
746
+ },
747
+ },
748
+ pendingDir,
749
+ friendStore: store,
463
750
  provider,
464
751
  externalId,
465
752
  tenantId: teamsContext?.tenantId,
466
- channel: "teams",
753
+ isGroupChat: false,
754
+ groupHasFamilyMember: false,
755
+ hasExistingGroupWithFamily: false,
756
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
757
+ drainPending: pending_1.drainPending,
758
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)((0, identity_1.getAgentName)(), deferredFriendId),
759
+ runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
760
+ ...opts,
761
+ toolContext: {
762
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
763
+ signin: async () => undefined,
764
+ ...opts?.toolContext,
765
+ summarize: teamsToolContext.summarize,
766
+ },
767
+ }),
768
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
769
+ const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
770
+ (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
771
+ },
772
+ accumulateFriendTokens: friends_1.accumulateFriendTokens,
773
+ signal: controller.signal,
774
+ runAgentOptions: agentOptions,
775
+ failoverState: teamsFailoverState,
467
776
  });
468
- if (!trustGate.allowed) {
469
- if (trustGate.reason === "stranger_first_reply") {
470
- stream.emit(trustGate.autoReply);
471
- }
472
- return;
473
- }
474
- }
475
- const registry = (0, commands_1.createCommandRegistry)();
476
- (0, commands_1.registerDefaultCommands)(registry);
477
- // Check for slash commands
478
- const parsed = (0, commands_1.parseSlashCommand)(text);
479
- if (parsed) {
480
- const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
481
- if (dispatchResult.handled && dispatchResult.result) {
482
- if (dispatchResult.result.action === "new") {
483
- const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
777
+ // ── Handle pipeline-intercepted commands ────────────────────────
778
+ if (result.turnOutcome === "command") {
779
+ if (result.commandAction === "new") {
484
780
  (0, context_1.deleteSession)(sessPath);
485
781
  stream.emit("session cleared");
486
- return;
487
782
  }
488
- else if (dispatchResult.result.action === "response") {
489
- stream.emit(dispatchResult.result.message || "");
490
- return;
783
+ // For "response" commands: pipeline already emitted the response via onTextChunk
784
+ await callbacks.flush();
785
+ return;
786
+ }
787
+ /* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
788
+ if (result.failoverMessage) {
789
+ stream.emit(result.failoverMessage);
790
+ }
791
+ else if (capturedTerminalError) {
792
+ callbacks.onError(capturedTerminalError, "terminal");
793
+ }
794
+ /* v8 ignore stop */
795
+ // ── Handle gate result ────────────────────────────────────────
796
+ if (!result.gateResult.allowed) {
797
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
798
+ stream.emit(result.gateResult.autoReply);
491
799
  }
800
+ return;
801
+ }
802
+ // Flush any remaining accumulated text at end of turn
803
+ await callbacks.flush();
804
+ // After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
805
+ // This must happen after the stream is done so the OAuth card renders properly.
806
+ if (teamsContext && result.messages) {
807
+ const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
808
+ if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
809
+ await teamsContext.signin(teamsContext.graphConnectionName);
810
+ if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
811
+ await teamsContext.signin(teamsContext.adoConnectionName);
812
+ if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
813
+ await teamsContext.signin(teamsContext.githubConnectionName);
814
+ }
815
+ if (result.turnOutcome !== "superseded") {
816
+ return;
817
+ }
818
+ const supersedingIndex = drainedSteeringFollowUps
819
+ .map((followUp) => followUp.effect)
820
+ .lastIndexOf("clear_and_supersede");
821
+ if (supersedingIndex < 0) {
822
+ return;
823
+ }
824
+ const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
825
+ const replayTail = drainedSteeringFollowUps
826
+ .slice(supersedingIndex + 1)
827
+ .map((followUp) => followUp.text.trim())
828
+ .filter((followUpText) => followUpText.length > 0)
829
+ .join("\n");
830
+ if (replayTail) {
831
+ currentText = replayTail;
832
+ continue;
492
833
  }
834
+ if (handleTeamsSlashCommand(supersedingFollowUp.text, createTeamsCommandRegistry(), friendId, conversationId, stream, false)) {
835
+ return;
836
+ }
837
+ currentText = supersedingFollowUp.text;
493
838
  }
494
- // Load or create session
495
- const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
496
- const existing = (0, context_1.loadSession)(sessPath);
497
- const messages = existing?.messages && existing.messages.length > 0
498
- ? existing.messages
499
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, toolContext?.context) }];
500
- // Push user message
501
- messages.push({ role: "user", content: text });
502
- // Run agent
503
- const controller = new AbortController();
504
- const channelConfig = (0, config_2.getTeamsChannelConfig)();
505
- const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
506
- const traceId = (0, nerves_1.createTraceId)();
507
- const agentOptions = {};
508
- agentOptions.traceId = traceId;
509
- if (toolContext)
510
- agentOptions.toolContext = toolContext;
511
- if (channelConfig.skipConfirmation)
512
- agentOptions.skipConfirmation = true;
513
- agentOptions.drainSteeringFollowUps = () => _turnCoordinator.drainFollowUps(turnKey).map((m) => ({ text: m.text }));
514
- const result = await (0, core_1.runAgent)(messages, callbacks, "teams", controller.signal, agentOptions);
515
- // Flush any remaining accumulated text at end of turn
516
- await callbacks.flush();
517
- // After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
518
- // This must happen after the stream is done so the OAuth card renders properly.
519
- if (teamsContext) {
520
- const allContent = messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
521
- if (allContent.includes("AUTH_REQUIRED:graph"))
522
- await teamsContext.signin("graph");
523
- if (allContent.includes("AUTH_REQUIRED:ado"))
524
- await teamsContext.signin("ado");
525
- if (allContent.includes("AUTH_REQUIRED:github"))
526
- await teamsContext.signin("github");
527
- }
528
- // Trim context and save session
529
- (0, context_1.postTurn)(messages, sessPath, result.usage);
530
- // Accumulate token usage on friend record
531
- if (toolContext?.context?.friend?.id) {
532
- await (0, tokens_1.accumulateFriendTokens)(store, toolContext.context.friend.id, result.usage);
533
- }
534
- // SDK auto-closes the stream after our handler returns (app.process.js)
535
839
  }
536
- // Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
537
- // Mode is determined by getTeamsConfig().clientId.
538
- // Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
539
- function startTeamsApp() {
840
+ // Internal port for the secondary bot App (not exposed externally).
841
+ // The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
842
+ const SECONDARY_INTERNAL_PORT = 3979;
843
+ // Collect all unique OAuth connection names across top-level config and tenant overrides.
844
+ /* v8 ignore start -- runtime Teams SDK config; no unit-testable surface @preserve */
845
+ function allOAuthConnectionNames() {
846
+ const oauthConfig = (0, config_1.getOAuthConfig)();
847
+ const names = new Set();
848
+ if (oauthConfig.graphConnectionName)
849
+ names.add(oauthConfig.graphConnectionName);
850
+ if (oauthConfig.adoConnectionName)
851
+ names.add(oauthConfig.adoConnectionName);
852
+ if (oauthConfig.githubConnectionName)
853
+ names.add(oauthConfig.githubConnectionName);
854
+ if (oauthConfig.tenantOverrides) {
855
+ for (const ov of Object.values(oauthConfig.tenantOverrides)) {
856
+ if (ov.graphConnectionName)
857
+ names.add(ov.graphConnectionName);
858
+ if (ov.adoConnectionName)
859
+ names.add(ov.adoConnectionName);
860
+ if (ov.githubConnectionName)
861
+ names.add(ov.githubConnectionName);
862
+ }
863
+ }
864
+ return [...names];
865
+ }
866
+ // Create an App instance from a TeamsConfig. Returns { app, mode }.
867
+ function createBotApp(teamsConfig) {
540
868
  const mentionStripping = { activity: { mentions: { stripText: true } } };
541
- const teamsConfig = (0, config_2.getTeamsConfig)();
542
- let app;
543
- let mode;
544
869
  const oauthConfig = (0, config_1.getOAuthConfig)();
545
- if (teamsConfig.clientId) {
546
- // Bot Service mode -- real Teams connection with SingleTenant credentials
547
- app = new teams_apps_1.App({
548
- clientId: teamsConfig.clientId,
549
- clientSecret: teamsConfig.clientSecret,
550
- tenantId: teamsConfig.tenantId,
551
- oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
552
- ...mentionStripping,
553
- });
554
- mode = "Bot Service";
870
+ if (teamsConfig.clientId && teamsConfig.clientSecret) {
871
+ return {
872
+ app: new teams_apps_1.App({
873
+ clientId: teamsConfig.clientId,
874
+ clientSecret: teamsConfig.clientSecret,
875
+ tenantId: teamsConfig.tenantId,
876
+ oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
877
+ ...mentionStripping,
878
+ }),
879
+ mode: "Bot Service (client secret)",
880
+ };
881
+ }
882
+ else if (teamsConfig.clientId) {
883
+ return {
884
+ app: new teams_apps_1.App({
885
+ clientId: teamsConfig.clientId,
886
+ tenantId: teamsConfig.tenantId,
887
+ ...(teamsConfig.managedIdentityClientId ? { managedIdentityClientId: teamsConfig.managedIdentityClientId } : {}),
888
+ oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
889
+ ...mentionStripping,
890
+ }),
891
+ mode: "Bot Service (managed identity)",
892
+ };
555
893
  }
556
894
  else {
557
- // DevtoolsPlugin mode -- local development with Teams DevtoolsPlugin UI
558
- app = new teams_apps_1.App({
559
- plugins: [new teams_dev_1.DevtoolsPlugin()],
560
- ...mentionStripping,
561
- });
562
- mode = "DevtoolsPlugin";
895
+ return {
896
+ app: new teams_apps_1.App({
897
+ plugins: [new teams_dev_1.DevtoolsPlugin()],
898
+ ...mentionStripping,
899
+ }),
900
+ mode: "DevtoolsPlugin",
901
+ };
563
902
  }
903
+ }
904
+ /* v8 ignore stop */
905
+ // Register message, verify-state, and error handlers on an App instance.
906
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
907
+ function registerBotHandlers(app, label) {
908
+ const connectionNames = allOAuthConnectionNames();
564
909
  // Override default OAuth verify-state handler. The SDK's built-in handler
565
910
  // uses a single defaultConnectionName, which breaks multi-connection setups
566
911
  // (graph + ado + github). The verifyState activity only carries a `state`
567
912
  // code with no connectionName, so we try each configured connection until
568
913
  // one succeeds.
569
- const allConnectionNames = [
570
- oauthConfig.graphConnectionName,
571
- oauthConfig.adoConnectionName,
572
- oauthConfig.githubConnectionName,
573
- ].filter(Boolean);
574
- app.on("signin.verify-state", async (ctx) => {
914
+ app.on("signin.verify-state", (async (ctx) => {
575
915
  const { api, activity } = ctx;
576
916
  if (!activity.value?.state)
577
917
  return { status: 404 };
578
- for (const cn of allConnectionNames) {
918
+ for (const cn of connectionNames) {
579
919
  try {
580
920
  await api.users.token.get({
581
921
  channelId: activity.channelId,
@@ -583,14 +923,82 @@ function startTeamsApp() {
583
923
  connectionName: cn,
584
924
  code: activity.value.state,
585
925
  });
586
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
926
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
587
927
  return { status: 200 };
588
928
  }
589
929
  catch { /* try next */ }
590
930
  }
591
- (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: "verify-state failed for all connections", meta: {} });
931
+ (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
592
932
  return { status: 412 };
933
+ }));
934
+ // Handle Teams feedback reactions (thumbs up/down on AI-generated messages).
935
+ // SDK routes message/submitAction with actionName "feedback" to this event.
936
+ /* v8 ignore start -- Teams SDK invoke handler; requires live SDK context @preserve */
937
+ app.on("message.submit.feedback", async (ctx) => {
938
+ const { stream, activity } = ctx;
939
+ const reaction = activity.value?.actionValue?.reaction;
940
+ const comment = activity.value?.actionValue?.feedback;
941
+ const convId = activity.conversation?.id || "unknown";
942
+ const turnKey = teamsTurnKey(convId);
943
+ // Validate payload — graceful no-op for malformed invocations
944
+ if (activity.value?.actionName !== "feedback" || !reaction) {
945
+ return undefined;
946
+ }
947
+ const syntheticText = buildFeedbackSyntheticText(reaction, comment);
948
+ // Turn coordination: if a turn is active, enqueue as steering follow-up
949
+ if (!_turnCoordinator.tryBeginTurn(turnKey)) {
950
+ _turnCoordinator.enqueueFollowUp(turnKey, {
951
+ conversationId: convId,
952
+ text: syntheticText,
953
+ receivedAt: Date.now(),
954
+ effect: (0, continuity_1.classifySteeringFollowUpEffect)(syntheticText),
955
+ });
956
+ return undefined;
957
+ }
958
+ try {
959
+ const teamsContext = {
960
+ signin: async () => undefined,
961
+ aadObjectId: activity.from?.aadObjectId,
962
+ tenantId: activity.conversation?.tenantId,
963
+ displayName: activity.from?.name,
964
+ };
965
+ const ctxSend = async (t) => {
966
+ await ctx.send({ type: "message", text: t, replyToId: activity.replyToId, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
967
+ };
968
+ await handleTeamsMessage(syntheticText, stream, convId, teamsContext, ctxSend, { isReactionSignal: true, suppressEmptyStreamMessage: true });
969
+ }
970
+ catch (err) {
971
+ const msg = err instanceof Error ? err.message : String(err);
972
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.feedback_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
973
+ }
974
+ finally {
975
+ _turnCoordinator.endTurn(turnKey);
976
+ }
977
+ return undefined;
978
+ });
979
+ /* v8 ignore stop */
980
+ // Handle bot install — send welcome Adaptive Card with prompt starters.
981
+ /* v8 ignore start -- Teams SDK install handler; requires live SDK context @preserve */
982
+ app.on("install.add", async (ctx) => {
983
+ try {
984
+ const card = buildWelcomeCard();
985
+ await ctx.send({
986
+ type: "message",
987
+ attachments: [{
988
+ contentType: "application/vnd.microsoft.card.adaptive",
989
+ content: card,
990
+ }],
991
+ entities: aiLabelEntities(),
992
+ channelData: { feedbackLoopEnabled: true },
993
+ });
994
+ }
995
+ catch (err) {
996
+ const msg = err instanceof Error ? err.message : String(err);
997
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.welcome_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
998
+ }
999
+ return undefined;
593
1000
  });
1001
+ /* v8 ignore stop */
594
1002
  app.on("message", async (ctx) => {
595
1003
  const { stream, activity, api, signin } = ctx;
596
1004
  const text = activity.text || "";
@@ -598,17 +1006,41 @@ function startTeamsApp() {
598
1006
  const turnKey = teamsTurnKey(convId);
599
1007
  const userId = activity.from?.id || "";
600
1008
  const channelId = activity.channelId || "msteams";
601
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: "incoming teams message", meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
602
- // Resolve pending confirmations IMMEDIATELY — before token fetches or
603
- // the conversation lock. The original message holds the lock while
604
- // awaiting confirmation, so acquiring it here would deadlock. Token
605
- // fetches are also unnecessary (and slow) for a simple yes/no reply.
606
- if (resolvePendingConfirmation(convId, text)) {
607
- // Don't emit on this stream — the original message's stream is still
608
- // active. Opening a second streaming response in the same conversation
609
- // can corrupt the first. The original stream will show tool progress
610
- // once the confirmation Promise resolves.
611
- return;
1009
+ (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) } });
1010
+ const commandRegistry = createTeamsCommandRegistry();
1011
+ const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
1012
+ if (parsedSlashCommand) {
1013
+ const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
1014
+ if (dispatchResult.handled && dispatchResult.result) {
1015
+ if (dispatchResult.result.action === "response") {
1016
+ stream.emit(dispatchResult.result.message || "");
1017
+ return;
1018
+ }
1019
+ if (dispatchResult.result.action === "new") {
1020
+ const commandStore = getFriendStore();
1021
+ const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
1022
+ const commandExternalId = activity.from?.aadObjectId || convId;
1023
+ const commandResolver = new friends_1.FriendResolver(commandStore, {
1024
+ provider: commandProvider,
1025
+ externalId: commandExternalId,
1026
+ tenantId: activity.conversation?.tenantId,
1027
+ displayName: activity.from?.name || "Unknown",
1028
+ channel: "teams",
1029
+ });
1030
+ const commandContext = await commandResolver.resolve();
1031
+ (0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
1032
+ stream.emit("session cleared");
1033
+ if (_turnCoordinator.isTurnActive(turnKey)) {
1034
+ _turnCoordinator.enqueueFollowUp(turnKey, {
1035
+ conversationId: convId,
1036
+ text,
1037
+ receivedAt: Date.now(),
1038
+ effect: "clear_and_supersede",
1039
+ });
1040
+ }
1041
+ return;
1042
+ }
1043
+ }
612
1044
  }
613
1045
  // If this conversation already has an active turn, steer follow-up input
614
1046
  // into that turn and avoid starting a second concurrent turn.
@@ -617,31 +1049,35 @@ function startTeamsApp() {
617
1049
  conversationId: convId,
618
1050
  text,
619
1051
  receivedAt: Date.now(),
1052
+ effect: (0, continuity_1.classifySteeringFollowUpEffect)(text),
620
1053
  });
621
1054
  return;
622
1055
  }
623
1056
  try {
1057
+ // Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
1058
+ const tenantId = activity.conversation?.tenantId;
1059
+ const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
624
1060
  // Fetch tokens for both OAuth connections independently.
625
1061
  // Failures are silently caught -- the tool handler will request signin if needed.
626
1062
  let graphToken;
627
1063
  let adoToken;
628
1064
  let githubToken;
629
1065
  try {
630
- const graphRes = await api.users.token.get({ userId, connectionName: oauthConfig.graphConnectionName, channelId });
1066
+ const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
631
1067
  graphToken = graphRes?.token;
632
1068
  }
633
1069
  catch { /* no token yet — tool handler will trigger signin */ }
634
1070
  try {
635
- const adoRes = await api.users.token.get({ userId, connectionName: oauthConfig.adoConnectionName, channelId });
1071
+ const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
636
1072
  adoToken = adoRes?.token;
637
1073
  }
638
1074
  catch { /* no token yet — tool handler will trigger signin */ }
639
1075
  try {
640
- const githubRes = await api.users.token.get({ userId, connectionName: oauthConfig.githubConnectionName, channelId });
1076
+ const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
641
1077
  githubToken = githubRes?.token;
642
1078
  }
643
1079
  catch { /* no token yet — tool handler will trigger signin */ }
644
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken } });
1080
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
645
1081
  const teamsContext = {
646
1082
  graphToken,
647
1083
  adoToken,
@@ -661,12 +1097,17 @@ function startTeamsApp() {
661
1097
  aadObjectId: activity.from?.aadObjectId,
662
1098
  tenantId: activity.conversation?.tenantId,
663
1099
  displayName: activity.from?.name,
1100
+ graphConnectionName: tenantOAuth.graphConnectionName,
1101
+ adoConnectionName: tenantOAuth.adoConnectionName,
1102
+ githubConnectionName: tenantOAuth.githubConnectionName,
1103
+ /* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
1104
+ botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
664
1105
  };
665
1106
  /* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
666
1107
  const ctxSend = async (t) => {
667
1108
  // Use send with replyToId (not reply, which adds a blockquote).
668
1109
  // replyToId anchors the message after the user's message in Copilot Chat.
669
- await ctx.send({ type: "message", text: t, replyToId: activity.id });
1110
+ await ctx.send({ type: "message", text: t, replyToId: activity.id, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
670
1111
  };
671
1112
  await handleTeamsMessage(text, stream, convId, teamsContext, ctxSend);
672
1113
  }
@@ -678,6 +1119,251 @@ function startTeamsApp() {
678
1119
  _turnCoordinator.endTurn(turnKey);
679
1120
  }
680
1121
  });
1122
+ app.event("error", ({ error }) => {
1123
+ const msg = error instanceof Error ? error.message : String(error);
1124
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
1125
+ });
1126
+ }
1127
+ function findAadObjectId(friend) {
1128
+ for (const ext of friend.externalIds) {
1129
+ if (ext.provider === "aad" && !ext.externalId.startsWith("group:")) {
1130
+ return { aadObjectId: ext.externalId, tenantId: ext.tenantId };
1131
+ }
1132
+ }
1133
+ return undefined;
1134
+ }
1135
+ function resolveTeamsFriendStore(deps) {
1136
+ return deps.store
1137
+ ?? deps.createFriendStore?.()
1138
+ ?? new friends_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends"));
1139
+ }
1140
+ function getTeamsConversations(botApi) {
1141
+ return botApi.conversations;
1142
+ }
1143
+ function hasExplicitCrossChatAuthorization(params) {
1144
+ return params.intent === "explicit_cross_chat"
1145
+ && friends_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
1146
+ }
1147
+ async function sendProactiveTeamsMessageToSession(params, deps) {
1148
+ const store = resolveTeamsFriendStore(deps);
1149
+ const conversations = getTeamsConversations(deps.botApi);
1150
+ let friend;
1151
+ try {
1152
+ friend = await store.get(params.friendId);
1153
+ }
1154
+ catch {
1155
+ friend = null;
1156
+ }
1157
+ if (!friend) {
1158
+ (0, runtime_1.emitNervesEvent)({
1159
+ level: "warn",
1160
+ component: "senses",
1161
+ event: "senses.teams_proactive_no_friend",
1162
+ message: "proactive send skipped: friend not found",
1163
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
1164
+ });
1165
+ return { delivered: false, reason: "friend_not_found" };
1166
+ }
1167
+ if (!hasExplicitCrossChatAuthorization(params) && !friends_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
1168
+ (0, runtime_1.emitNervesEvent)({
1169
+ component: "senses",
1170
+ event: "senses.teams_proactive_trust_skip",
1171
+ message: "proactive send skipped: trust level not allowed",
1172
+ meta: {
1173
+ friendId: params.friendId,
1174
+ trustLevel: friend.trustLevel ?? "unknown",
1175
+ intent: params.intent ?? "generic_outreach",
1176
+ authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
1177
+ },
1178
+ });
1179
+ return { delivered: false, reason: "trust_skip" };
1180
+ }
1181
+ const aadInfo = findAadObjectId(friend);
1182
+ if (!aadInfo) {
1183
+ (0, runtime_1.emitNervesEvent)({
1184
+ level: "warn",
1185
+ component: "senses",
1186
+ event: "senses.teams_proactive_no_aad_id",
1187
+ message: "proactive send skipped: no AAD object ID found",
1188
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
1189
+ });
1190
+ return { delivered: false, reason: "missing_target" };
1191
+ }
1192
+ const internalContentBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(params.text);
1193
+ if (internalContentBlockReason) {
1194
+ (0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
1195
+ friendId: params.friendId,
1196
+ sessionKey: params.sessionKey,
1197
+ reason: internalContentBlockReason,
1198
+ source: "session_send",
1199
+ });
1200
+ return { delivered: false, reason: "internal_content_blocked" };
1201
+ }
1202
+ try {
1203
+ const conversation = await conversations.create({
1204
+ bot: { id: deps.botApi.id },
1205
+ members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
1206
+ tenantId: aadInfo.tenantId,
1207
+ isGroup: false,
1208
+ });
1209
+ await conversations.activities(conversation.id).create({
1210
+ type: "message",
1211
+ text: params.text,
1212
+ });
1213
+ (0, runtime_1.emitNervesEvent)({
1214
+ component: "senses",
1215
+ event: "senses.teams_proactive_sent",
1216
+ message: "proactive teams message sent",
1217
+ meta: { friendId: params.friendId, aadObjectId: aadInfo.aadObjectId, sessionKey: params.sessionKey },
1218
+ });
1219
+ return { delivered: true };
1220
+ }
1221
+ catch (error) {
1222
+ (0, runtime_1.emitNervesEvent)({
1223
+ level: "error",
1224
+ component: "senses",
1225
+ event: "senses.teams_proactive_send_error",
1226
+ message: "proactive teams send failed",
1227
+ meta: {
1228
+ friendId: params.friendId,
1229
+ aadObjectId: aadInfo.aadObjectId,
1230
+ sessionKey: params.sessionKey,
1231
+ reason: error instanceof Error ? error.message : String(error),
1232
+ },
1233
+ });
1234
+ return { delivered: false, reason: "send_error" };
1235
+ }
1236
+ }
1237
+ function scanPendingTeamsFiles(pendingRoot) {
1238
+ const results = [];
1239
+ let friendIds;
1240
+ try {
1241
+ friendIds = fs.readdirSync(pendingRoot);
1242
+ }
1243
+ catch {
1244
+ return results;
1245
+ }
1246
+ for (const friendId of friendIds) {
1247
+ const teamsDir = path.join(pendingRoot, friendId, "teams");
1248
+ let keys;
1249
+ try {
1250
+ keys = fs.readdirSync(teamsDir);
1251
+ }
1252
+ catch {
1253
+ continue;
1254
+ }
1255
+ for (const key of keys) {
1256
+ const keyDir = path.join(teamsDir, key);
1257
+ let files;
1258
+ try {
1259
+ files = fs.readdirSync(keyDir);
1260
+ }
1261
+ catch {
1262
+ continue;
1263
+ }
1264
+ for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
1265
+ const filePath = path.join(keyDir, file);
1266
+ try {
1267
+ const content = fs.readFileSync(filePath, "utf-8");
1268
+ results.push({ friendId, key, filePath, content });
1269
+ }
1270
+ catch {
1271
+ // skip unreadable files
1272
+ }
1273
+ }
1274
+ }
1275
+ }
1276
+ return results;
1277
+ }
1278
+ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
1279
+ const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
1280
+ const pendingFiles = scanPendingTeamsFiles(root);
1281
+ const result = { sent: 0, skipped: 0, failed: 0 };
1282
+ for (const { friendId, key, filePath, content } of pendingFiles) {
1283
+ let parsed;
1284
+ try {
1285
+ parsed = JSON.parse(content);
1286
+ }
1287
+ catch {
1288
+ result.failed++;
1289
+ try {
1290
+ fs.unlinkSync(filePath);
1291
+ }
1292
+ catch { /* ignore */ }
1293
+ continue;
1294
+ }
1295
+ const messageText = typeof parsed.content === "string" ? parsed.content : "";
1296
+ if (!messageText.trim()) {
1297
+ result.skipped++;
1298
+ try {
1299
+ fs.unlinkSync(filePath);
1300
+ }
1301
+ catch { /* ignore */ }
1302
+ continue;
1303
+ }
1304
+ const internalBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(messageText);
1305
+ if (internalBlockReason) {
1306
+ result.skipped++;
1307
+ try {
1308
+ fs.unlinkSync(filePath);
1309
+ }
1310
+ catch { /* ignore */ }
1311
+ (0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
1312
+ friendId,
1313
+ reason: internalBlockReason,
1314
+ source: "pending_drain",
1315
+ });
1316
+ continue;
1317
+ }
1318
+ const sendResult = await sendProactiveTeamsMessageToSession({
1319
+ friendId,
1320
+ sessionKey: key,
1321
+ text: messageText,
1322
+ intent: "generic_outreach",
1323
+ }, {
1324
+ botApi,
1325
+ store,
1326
+ });
1327
+ if (sendResult.delivered) {
1328
+ result.sent++;
1329
+ try {
1330
+ fs.unlinkSync(filePath);
1331
+ }
1332
+ catch { /* ignore */ }
1333
+ continue;
1334
+ }
1335
+ if (sendResult.reason === "friend_not_found" || sendResult.reason === "trust_skip" || sendResult.reason === "missing_target") {
1336
+ result.skipped++;
1337
+ try {
1338
+ fs.unlinkSync(filePath);
1339
+ }
1340
+ catch { /* ignore */ }
1341
+ continue;
1342
+ }
1343
+ result.failed++;
1344
+ }
1345
+ if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
1346
+ (0, runtime_1.emitNervesEvent)({
1347
+ component: "senses",
1348
+ event: "senses.teams_proactive_drain_complete",
1349
+ message: "teams proactive drain complete",
1350
+ meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
1351
+ });
1352
+ }
1353
+ return result;
1354
+ }
1355
+ // Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
1356
+ // Mode is determined by getTeamsConfig().clientId.
1357
+ // Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
1358
+ //
1359
+ // Dual-bot support: if teamsSecondary is configured with a clientId, a second App
1360
+ // instance starts on an internal port and the primary app proxies requests from
1361
+ // /api/messages-secondary to it. This lets a single App Service serve two bot
1362
+ // registrations (e.g. one per tenant) without SDK modifications.
1363
+ function startTeamsApp() {
1364
+ const teamsConfig = (0, config_2.getTeamsConfig)();
1365
+ const { app, mode } = createBotApp(teamsConfig);
1366
+ registerBotHandlers(app, "primary");
681
1367
  if (!process.listeners("unhandledRejection").some((l) => l.__agentHandler)) {
682
1368
  const handler = (err) => {
683
1369
  const msg = err instanceof Error ? err.message : String(err);
@@ -686,11 +1372,54 @@ function startTeamsApp() {
686
1372
  handler.__agentHandler = true;
687
1373
  process.on("unhandledRejection", handler);
688
1374
  }
689
- app.event("error", ({ error }) => {
690
- const msg = error instanceof Error ? error.message : String(error);
691
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: msg, meta: {} });
692
- });
693
- const port = (0, config_2.getTeamsChannelConfig)().port;
1375
+ /* v8 ignore next -- PORT env branch; runtime-only @preserve */
1376
+ const port = process.env.PORT ? Number(process.env.PORT) : (0, config_2.getTeamsChannelConfig)().port;
694
1377
  app.start(port);
695
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode } });
1378
+ // Diagnostic: log tool count at startup to verify deploy
1379
+ const startupTools = (0, tools_1.getToolsForChannel)((0, friends_1.getChannelCapabilities)("teams"));
1380
+ const toolNames = startupTools.map((t) => t.function.name);
1381
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode, toolCount: toolNames.length, hasProactive: toolNames.includes("teams_send_message") } });
1382
+ // --- Secondary bot (dual-bot support) ---
1383
+ // If teamsSecondary has a clientId, start a second App on an internal port
1384
+ // and proxy /api/messages-secondary on the primary app to it.
1385
+ /* v8 ignore start -- dual-bot proxy wiring; requires live Teams SDK + HTTP @preserve */
1386
+ const secondaryConfig = (0, config_1.getTeamsSecondaryConfig)();
1387
+ if (secondaryConfig.clientId) {
1388
+ const { app: secondaryApp, mode: secondaryMode } = createBotApp(secondaryConfig);
1389
+ registerBotHandlers(secondaryApp, "secondary");
1390
+ secondaryApp.start(SECONDARY_INTERNAL_PORT);
1391
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Secondary bot started on internal port ${SECONDARY_INTERNAL_PORT} with ${secondaryMode}`, meta: { port: SECONDARY_INTERNAL_PORT, mode: secondaryMode } });
1392
+ // Proxy: forward /api/messages-secondary on the primary app's Express
1393
+ // to localhost:SECONDARY_INTERNAL_PORT/api/messages.
1394
+ // The SDK's HttpPlugin exposes .post() bound to its Express instance.
1395
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1396
+ const httpPlugin = app.http;
1397
+ httpPlugin.post("/api/messages-secondary", (req, res) => {
1398
+ const body = JSON.stringify(req.body);
1399
+ const proxyReq = http.request({
1400
+ hostname: "127.0.0.1",
1401
+ port: SECONDARY_INTERNAL_PORT,
1402
+ path: "/api/messages",
1403
+ method: "POST",
1404
+ headers: {
1405
+ ...req.headers,
1406
+ "content-length": Buffer.byteLength(body).toString(),
1407
+ },
1408
+ }, (proxyRes) => {
1409
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
1410
+ proxyRes.pipe(res);
1411
+ });
1412
+ proxyReq.on("error", (err) => {
1413
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.proxy_error", component: "channels", message: `secondary proxy error: ${err.message}`, meta: {} });
1414
+ if (!res.headersSent) {
1415
+ res.writeHead(502);
1416
+ res.end("Bad Gateway");
1417
+ }
1418
+ });
1419
+ proxyReq.write(body);
1420
+ proxyReq.end();
1421
+ });
1422
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.proxy_ready", component: "channels", message: "proxy /api/messages-secondary → secondary bot ready", meta: {} });
1423
+ }
1424
+ /* v8 ignore stop */
696
1425
  }