Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ product overview.

## Unreleased

- Teams reaction activation now accepts the exact Heart eyes robot picker shortcut
`:hearteyesrobot:` as well as its bare event ID. 👍 Like is an alternative activation reaction. Native `like` and Graph Unicode
thumbs-up values, including skin-tone variants, use the existing authorized reaction path.
Teams help shows the Heart eyes robot shortcut and Like alternative; legacy robot IDs remain
recognized. A persisted cutoff prevents newly recognized aliases from replaying older reactions.

- Teams help now shows Teams reaction IDs and picker names instead of Slack shortcodes.
Heart eyes robot no longer displays the mismatched plain robot glyph, and the guide explains
that reaction codes must be selected through the picker rather than sent as text.
Expand Down
13 changes: 7 additions & 6 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@
Examples use Teams controls and explain their permissions and configuration requirements.
The authorized native command responds before an engine turn, in the requesting conversation.
Quoting the help card in a group chat continues the same session.
- Reaction labels show Teams picker names and actual IDs: Heart eyes robot (`hearteyesrobot`),
- Reaction labels show Teams picker names and actual IDs: Heart eyes robot (`:hearteyesrobot:`),
Stop sign (`stopsign`), and Tick button / Checkmark button (`2705_whiteheavycheckmark`).
Heart eyes robot uses its picker name without a misleading plain robot glyph. The guide explains
that typing an ID as a message does not add a reaction. Teams intake recognizes the documented
robot IDs,
`stopsign` and `2705_whiteheavycheckmark`, plus Unicode/legacy aliases. Robot starts a request
The guide explains that typing an ID as a message does not add a reaction. Teams intake recognizes
both `hearteyesrobot` and the picker shortcut `:hearteyesrobot:` as activation IDs. Like (`like` or
Unicode 👍, including skin tones) is an alternative. Existing robot IDs remain supported alongside
`stopsign` and `2705_whiteheavycheckmark`, plus Unicode/legacy aliases. Activation starts a request
as the reactor. Stop cancels only the selected session’s active and queued work with the same
author/admin checks as `/stop`. Channel replies and group quotes resolve to their stored root.
Tick closes a tracked reminder’s acknowledgment chain from its original or escalation message;
reaction removal cannot reopen it. A tick on an ordinary message reports no pending reminder.
Teams personal follow-up dismissal remains unavailable. All actions pass conversation admission,
reactor authorization and the sudo thread gate before any mutation; target text cannot execute
a slash command. Graph and native events retain single transport ownership, stable deduplication
and fresh/current reaction checks. Group/channel reactions need configured event delivery and
and fresh/current reaction checks. Newly accepted alias spellings have a persisted subscription
cutoff so older reactions cannot activate on later unrelated Graph updates. Group/channel reactions need configured event delivery and
Microsoft permissions. The handlers precede engine dispatch for Stop/Tick.

## Claude login for nested commands
Expand Down
34 changes: 31 additions & 3 deletions TEST-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@

## Microsoft Teams reaction actions acceptance (2026-10-07)

- Shortcut/Like verification: 79 focused tests passed, zero failures/skips. Independent review
identified old-reaction replay when adding aliases; a persisted cutoff fixed it and the updated
review found no blocking defects. Static checks and secret scanning passed. Live event delivery
and private QA registration remain pending; no live trigger success is claimed.

- Activation shortcut and Like alternative: `test/platform-teams-events.test.js` and
`test/platform-teams-graph-activity.test.js` cover `hearteyesrobot`, exact picker shortcut `:hearteyesrobot:`, native `like`, Unicode 👍
and valid skin tones,
removal suppression and unsupported emoji rejection. `test/teams-reaction-actions.test.js`
carries the exact Heart eyes robot shortcut and Like through normalization and shared ingest in
personal, group and channel sessions,
checks reactor identity/session target, and rejects unapproved reactors before engine dispatch.
`test/platform-teams-graph-events.test.js` verifies persisted alias-introduction cutoff initialization
before cached subscription reuse and message fetch, preservation on context refresh/renew/restart,
and Graph normalizer tests reject older alias additions and missing cutoffs without changing
legacy robot/control admission. New aliases cannot replay pre-upgrade history on unrelated edits.
- [ ] LIVE (Claude and Codex): use approved beta Teams QA personal, group and channel fixtures
with reaction event delivery enabled. In a group/channel, post an unmentioned message
"Reply exactly LIKE_TRIGGER_OK", then add 👍 **Like** using the Teams reaction picker as an
approved actor. Pass: exactly one response LIKE_TRIGGER_OK in that source session under the
reactor's identity on each engine; no response to removal and no run for an unapproved actor.
Repeat with **Heart eyes robot**, picker shortcut `:hearteyesrobot:`, on a separate fixture.
In personal chat, add either activation reaction to an existing bot reply: pass only if that session resumes once.
Capture the actual reaction type and delivered event when investigating a failed trigger.
A recognized ID alone does not establish event delivery. The failed message link/live fixture
remains requested; private QA registration requires the unavailable requester personal connection.

- Automated: `test/teams-reaction-actions.test.js` exercises native reaction → normalization →
shared ingest → control/ack store. Native and Graph Stop additions cancel active and queued
requests for personal, group and channel sessions; reject an approved non-author; permit
Expand All @@ -26,7 +53,7 @@
with "Reply QUEUED_EMOJI_DONE". An approved other actor adds Stop sign to the working reply:
pass only if work continues with an author/admin notice. Author adds Stop sign: pass if active
work stops, queued work never starts and unrelated sessions continue. Repeat with admin stopper.
Heart eyes robot on "Reply exactly EMOJI_ROBOT_OK" must run once as the reactor on each engine.
Like on "Reply exactly EMOJI_LIKE_OK" must run once as the reactor on each engine.
- [ ] LIVE, engine-independent: create a Teams QA reminder with acknowledgment enabled, default
Tick button and short escalation interval. Add Tick button to the original reminder, then repeat
on a separate reminder’s second notice. Pass: acknowledgment notice, pending ack removed,
Expand Down Expand Up @@ -67,7 +94,7 @@
static checks and secret scanning passed. The expanded guide still fits one Teams message.
- [ ] LIVE (Claude and Codex): in separate approved beta Teams QA group/channel sessions with
reaction event delivery enabled, post "Reply exactly ROBOT_MAPPING_OK", then as an approved
test actor add **Heart eyes robot** from the Teams picker. Pass: one run under the reactor's
test actor add 👍 **Like** from the Teams picker. Pass: one run under the reactor's
gateway identity replies ROBOT_MAPPING_OK in the source session for each engine. Remove the
reaction; delayed removed events must not run. Stop sign and Tick button invoke only their
authorized session-stop/reminder-acknowledgment controls (see reaction actions acceptance).
Expand All @@ -88,7 +115,8 @@
mention it with `/help`. Pass: each guide appears in its source conversation/thread, reads
correctly on desktop/mobile, with a distinct title, section dividers, spaced paragraphs,
separate reaction/command rows and monospace commands. Check the reaction legend shows
`hearteyesrobot`, `stopsign` and `2705_whiteheavycheckmark` beside their Teams picker names;
`hearteyesrobot` (shown as picker shortcut `:hearteyesrobot:`), `stopsign` and
`2705_whiteheavycheckmark` beside their Teams picker names;
no Slack shortcodes or misleading plain robot icon appear. It includes **How to use me** and the
complete supported command list, and advertises no Slack-only controls. Quote the group help
card with a mentioned `/status`; it must address the original session. No agent turn, runtime
Expand Down
10 changes: 6 additions & 4 deletions docs/PLATFORMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,13 @@ Task-module dialogs and broadcast mentions remain unavailable.
connections, skills, memory, reminders, schedules and background work. In channels and group
chats, mention the bot with the command; quote the original message or bot reply in a group
chat to address that session. Help is returned by the gateway before invoking an engine.
The reaction legend uses Teams picker names and IDs: Heart eyes robot (`hearteyesrobot`),
The reaction legend uses Teams picker names and IDs: Heart eyes robot (`:hearteyesrobot:`),
Stop sign (`stopsign`), and Tick button / Checkmark button (`2705_whiteheavycheckmark`).
Heart eyes robot has no equivalent Unicode glyph, so the guide uses its actual picker name.
Typing an ID as a message does not add a reaction. Robot activation accepts Teams' `hearteyesrobot`
and `smilerobot` event IDs. Microsoft calls the green tick **Checkmark button**, ID
Typing an ID as a message does not add a reaction. Heart eyes robot activates a request as the reactor; bare
`hearteyesrobot` and the picker shortcut `:hearteyesrobot:` are accepted. Like is an alternative;
intake recognizes `like`, Unicode 👍 and their skin-tone variants. Legacy `hearteyesrobot` and
`smilerobot` event IDs remain recognized. New alias recognition starts at a saved subscription
cutoff to prevent old reactions replaying on unrelated updates. Microsoft calls the green tick **Checkmark button**, ID
`2705_whiteheavycheckmark`; Stop sign is `stopsign`. See the
[Teams reactions reference](https://learn.microsoft.com/en-us/microsoftteams/platform/agents-in-teams/teams-reactions-reference).
Stop sign cancels the selected session’s active and queued requests, with author/admin checks.
Expand Down
5 changes: 4 additions & 1 deletion src/gateway/gateway-usage/platforms/msteams/platform.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ complete oversized edits use the authenticated browser settings.
## Reaction controls

When Teams reaction events are enabled and delivered to the gateway, **Heart eyes robot**
(`hearteyesrobot`, also Smile robot) starts a request as the reactor. **Stop sign** (`stopsign`)
(`hearteyesrobot`, picker shortcut `:hearteyesrobot:`) starts a request as the reactor. 👍 **Like**
(`like`, or Unicode 👍 with an optional skin tone) is also accepted. Use the reaction picker;
sending a shortcut as text does not add a reaction. Smile robot IDs remain recognized
when Teams delivers them. **Stop sign** (`stopsign`)
requests cancellation of that session’s active and queued work; only the author or an administrator
can stop it. React to the original message or a bot reply to select the session. **Tick button**
(`2705_whiteheavycheckmark`) acknowledges a tracked reminder, including its second notice.
Expand Down
6 changes: 5 additions & 1 deletion src/platforms/msteams/graph-activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { activityConversationName } from "./conversation-name.js";
import { makeInbound } from "../inbound.js";
import { quotedReplyId, stripMentionTags } from "./activity.js";

import { teamsReactionAction } from "./reactions.js";
import { teamsReactionAction, teamsReactionRequiresCutover } from "./reactions.js";

const digest = parts => createHash("sha256").update(JSON.stringify(parts)).digest("hex");
const userId = identity => String(identity?.user?.id || "");
Expand Down Expand Up @@ -97,6 +97,10 @@ export async function normalizeGraphEvents(message, row, { botId, resolveMember,
if (item.transition !== "added") continue;
const action = teamsReactionAction(item.reaction?.reactionType);
const stamp = item.modifiedDateTime;
if (teamsReactionRequiresCutover(item.reaction?.reactionType)) {
const cutover = Date.parse(row.reactionAliasesStartedAt);
if (!Number.isFinite(cutover) || !(Date.parse(stamp) > cutover)) continue;
}
const actor = userId(item.reaction?.user);
// A removed reaction must not start a new run when a delayed notification is fetched.
if (!(message.reactions || []).some(reaction => teamsReactionAction(reaction.reactionType) === action && userId(reaction.user) === actor)) continue;
Expand Down
24 changes: 19 additions & 5 deletions src/platforms/msteams/graph-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,16 @@ export function createTeamsGraphEvents({ auth, notificationUrl, tenantId, store,
}
return res.status === 204 ? null : res.json();
}
// Persist before fetching history or reusing a live subscription. Once set, this survives
// renewals/restarts; newly recognized aliases must never activate pre-upgrade reactions.
async function prepareReactionAliases(row) {
if (Number.isFinite(Date.parse(row.reactionAliasesStartedAt))) return row;
const prepared = { ...row, reactionAliasesStartedAt: new Date(now()).toISOString() };
await store.put(prepared);
return prepared;
}
async function maintain(row) {
row = await prepareReactionAliases(row);
const { resource, apiVersion } = baseResource(row.resource);
const current = now();
const sameEndpoint = row.notificationUrl === endpoint.href;
Expand Down Expand Up @@ -134,11 +143,16 @@ export function createTeamsGraphEvents({ auth, notificationUrl, tenantId, store,
}
async function processNotifications(accepted) {
for (const { event } of accepted) {
const row = (await store.list()).find(item => item.subscriptionId && item.subscriptionId === event?.subscriptionId);
const path = row && messagePath(event, row);
// Uninstall or rotation may revoke an envelope after durable acceptance, before its GET.
if (!row || !path || event.tenantId !== tenantId || !sameSecret(event.clientState, row.clientState)
|| !["created", "updated"].includes(event.changeType)) continue;
const row = await serialized(async () => {
const current = (await store.list()).find(item => item.subscriptionId && item.subscriptionId === event?.subscriptionId);
// Uninstall or rotation may revoke an envelope after durable acceptance, before its GET.
if (!current || !messagePath(event, current) || event.tenantId !== tenantId
|| !sameSecret(event.clientState, current.clientState)
|| !["created", "updated"].includes(event.changeType)) return null;
return prepareReactionAliases(current);
});
if (!row) continue;
const path = messagePath(event, row);
let message;
try { message = await request(row.apiVersion || baseResource(row.resource).apiVersion, path); }
catch (error) { if (error.status === 404) continue; throw error; }
Expand Down
2 changes: 1 addition & 1 deletion src/platforms/msteams/help.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion src/platforms/msteams/reactions.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
// Microsoft Teams reaction IDs, plus the Unicode/legacy aliases used by older activities.
// These intents are explicit controls; the reacted message body never supplies a command.
export function teamsReactionAction(value) {
const name = String(value || '').replace(/\uFE0F/g, '').toLowerCase();
// Accept the picker shortcut spelling as well as bare event IDs. Unpaired/doubled
// colons stay unknown; normal message text never reaches this reaction mapper.
const name = String(value || '').replace(/\uFE0F/g, '').toLowerCase().trim()
.replace(/^:([a-z0-9_]+(?:-tone[1-5])?):$/, '$1');
// Like is Teams' standard activation reaction. Graph can return its Unicode form,
// including a skin tone; native activities use the documented `like` ID.
if (/^👍[\u{1F3FB}-\u{1F3FF}]?$/u.test(name) || /^like(?:-tone[1-5])?$/.test(name)) return 'engage';
if (['🤖', 'robot', 'robot_face', 'smilerobot', 'hearteyesrobot'].includes(name)) return 'engage';
if (['🛑', 'stopsign', 'stop_sign', 'octagonal_sign'].includes(name)) return 'stop';
if (['✅', '2705_whiteheavycheckmark', 'white_check_mark'].includes(name)) return 'ack';
return '';
}

// These spellings were ignored before the shortcut/Like expansion. Graph history can contain
// old current reactions, so they need a persisted introduction cutoff before replay admission.
export function teamsReactionRequiresCutover(value) {
const raw = String(value || '').replace(/\uFE0F/g, '').toLowerCase();
return Boolean(teamsReactionAction(value)) && (raw.includes(':') || raw !== raw.trim()
|| /^👍/u.test(raw) || /^like(?:-tone[1-5])?$/.test(raw));
}
Loading
Loading