@ouro.bot/cli 0.1.0-alpha.33 → 0.1.0-alpha.331

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