crewly 1.20.85 β 1.20.87
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.
- package/config/skills/agent/core/reply-channel/SKILL.md +23 -0
- package/config/skills/agent/core/reply-channel/execute.sh +28 -0
- package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js +44 -0
- package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +40 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +61 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +68 -5
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +210 -49
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +40 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +61 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +68 -5
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +210 -49
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -66,7 +66,7 @@ export interface TeamChannelStorageApi {
|
|
|
66
66
|
onStorageEvent(listener: (event: StorageEvent) => Promise<void> | void): () => void;
|
|
67
67
|
}
|
|
68
68
|
/** Dispatcher slice β resolved lazily because it is built after the bridge. */
|
|
69
|
-
export type TeamChannelDispatcherApi = Pick<ChatV2DispatcherService, 'dispatchMessage'
|
|
69
|
+
export type TeamChannelDispatcherApi = Pick<ChatV2DispatcherService, 'dispatchMessage'> & Partial<Pick<ChatV2DispatcherService, 'planHuddleTargets'>>;
|
|
70
70
|
/** Constructor dependencies. */
|
|
71
71
|
export interface SlackTeamChannelServiceDeps {
|
|
72
72
|
slack: TeamChannelSlackApi;
|
|
@@ -439,12 +439,46 @@ export declare class SlackTeamChannelService {
|
|
|
439
439
|
* @returns The routing result, or null when the channel is not mapped
|
|
440
440
|
*/
|
|
441
441
|
routeInbound(message: SlackIncomingMessage): Promise<RouteInboundResult | null>;
|
|
442
|
+
/**
|
|
443
|
+
* The local agent whose own app delivered this copy, if any.
|
|
444
|
+
*
|
|
445
|
+
* A direct message is never a room: an agent's DM is handled by the DM
|
|
446
|
+
* service, and anything that falls through from there belongs to the
|
|
447
|
+
* orchestrator path, not to an ad-hoc huddle.
|
|
448
|
+
*
|
|
449
|
+
* @param message - The inbound message
|
|
450
|
+
* @returns The agent's session name, or null
|
|
451
|
+
*/
|
|
452
|
+
private localReceivingAgent;
|
|
453
|
+
/**
|
|
454
|
+
* Put π on the message β one per agent that will receive it.
|
|
455
|
+
*
|
|
456
|
+
* It used to be a single reaction meaning "Crewly got this". The owner
|
|
457
|
+
* reads it as "how many agents saw this", and with several agents in a
|
|
458
|
+
* room that is the more useful answer: three agents who will each weigh a
|
|
459
|
+
* message should show three eyes, and an agent that was not handed the
|
|
460
|
+
* message should show none, so the count is honest rather than "everyone
|
|
461
|
+
* in the channel". Each agent reacts with its own bot.
|
|
462
|
+
*
|
|
463
|
+
* When nobody in particular receives it, or no receiving agent has a bot
|
|
464
|
+
* of its own, one reaction still goes on so the owner can see the message
|
|
465
|
+
* arrived β the 2026-09-21 lesson: no eyes reads as nothing arrived.
|
|
466
|
+
*
|
|
467
|
+
* @param message - The inbound message
|
|
468
|
+
* @param mapping - Its channel mapping
|
|
469
|
+
* @param mentions - Agents @'d in it
|
|
470
|
+
* @param planned - Who will receive it, from the dispatcher; null when unknown
|
|
471
|
+
*/
|
|
472
|
+
private acknowledgeSeen;
|
|
442
473
|
/**
|
|
443
474
|
* Any Slack channel becomes routable the moment a local agent's bot is
|
|
444
|
-
* @'d in it
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
475
|
+
* @'d in it, or a message reaches us through a local agent's own app
|
|
476
|
+
* (Slack only delivers to apps that are members, so that proves the bot
|
|
477
|
+
* is in the channel). The owner invited agents from different teams into
|
|
478
|
+
* a private channel, say: a huddle is created for the channel with those
|
|
479
|
+
* agents as members and mapped under the synthetic team id
|
|
480
|
+
* `adhoc:<channel>`. Anything else is left to the caller (orchestrator
|
|
481
|
+
* path).
|
|
448
482
|
*
|
|
449
483
|
* @param message - The inbound message
|
|
450
484
|
* @returns The new mapping, or null when no local agent was @'d
|
|
@@ -474,6 +508,35 @@ export declare class SlackTeamChannelService {
|
|
|
474
508
|
* `slackThreadTs`; failing that, the latest Slack-origin root in the
|
|
475
509
|
* huddle; failing that, the channel top level.
|
|
476
510
|
*/
|
|
511
|
+
/**
|
|
512
|
+
* Show "<agent> is working on itβ¦" in the thread, at the agent's request.
|
|
513
|
+
*
|
|
514
|
+
* Agents that must answer get this placeholder the moment the message
|
|
515
|
+
* arrives. Agents that were only *told* β a message nobody addressed to
|
|
516
|
+
* them, passed along so they can judge whether it concerns them β get none,
|
|
517
|
+
* because a placeholder promises a reply and most of them will not send
|
|
518
|
+
* one. When one of them does decide to answer, it calls this, so the owner
|
|
519
|
+
* sees who has taken the message on: two agents deciding to answer show two
|
|
520
|
+
* placeholders, which is exactly the signal the owner asked for.
|
|
521
|
+
*
|
|
522
|
+
* The key matches the one the reply is mirrored under, so the agent's
|
|
523
|
+
* actual answer replaces the placeholder rather than landing beside it.
|
|
524
|
+
*
|
|
525
|
+
* @param input - Chat channel and thread the agent is answering in
|
|
526
|
+
* @returns Whether a placeholder is now showing, or why not
|
|
527
|
+
*/
|
|
528
|
+
beginWorkingForAgent(input: {
|
|
529
|
+
chatChannelId: string;
|
|
530
|
+
agentSession: string;
|
|
531
|
+
threadId?: string;
|
|
532
|
+
}): Promise<{
|
|
533
|
+
ok: true;
|
|
534
|
+
slackChannelId: string;
|
|
535
|
+
threadTs?: string;
|
|
536
|
+
} | {
|
|
537
|
+
ok: false;
|
|
538
|
+
reason: string;
|
|
539
|
+
}>;
|
|
477
540
|
/**
|
|
478
541
|
* Put a file into the Slack channel an agent is replying in.
|
|
479
542
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-team-channel.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/slack/slack-team-channel.service.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"slack-team-channel.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/slack/slack-team-channel.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -792,6 +792,18 @@ export class SlackTeamChannelService {
|
|
|
792
792
|
mapping = await this.ensureAdhocChannel(message);
|
|
793
793
|
if (!mapping)
|
|
794
794
|
return null;
|
|
795
|
+
// Slack delivers a channel message to every app in the channel, so a copy
|
|
796
|
+
// that arrived through one of our agents' own apps proves that agent is
|
|
797
|
+
// in the room β whether or not anyone has ever @'d it there. An ad-hoc
|
|
798
|
+
// roster used to be "agents that were @'d here", which left an agent the
|
|
799
|
+
// owner had added to a private channel out of it until someone happened
|
|
800
|
+
// to address it by name.
|
|
801
|
+
const receiving = await this.localReceivingAgent(message);
|
|
802
|
+
if (receiving && isAdhocMapping(mapping) && !(mapping.members ?? []).includes(receiving)) {
|
|
803
|
+
mapping.members = [...(mapping.members ?? []), receiving];
|
|
804
|
+
this.deps.chat.setHuddleMembers(mapping.chatChannelId, mapping.members);
|
|
805
|
+
await this.save();
|
|
806
|
+
}
|
|
795
807
|
// One Slack message can reach us twice with different event types
|
|
796
808
|
// (`app_mention` for an @'d agent's app plus `message.channels`); the
|
|
797
809
|
// team must see it once β a second copy is acknowledged, not dispatched.
|
|
@@ -880,29 +892,6 @@ export class SlackTeamChannelService {
|
|
|
880
892
|
if (oldest !== undefined)
|
|
881
893
|
this.seenInbound.delete(oldest);
|
|
882
894
|
}
|
|
883
|
-
// In an ad-hoc (often private) channel the master bot may not be a
|
|
884
|
-
// member; an agent's own bot reacts instead β the first @'d agent, or
|
|
885
|
-
// (nobody @'d, e.g. "@Crewly who leads content?") any agent already in
|
|
886
|
-
// the huddle, whose bot is by definition in the channel.
|
|
887
|
-
const reactAs = isAdhocMapping(mapping)
|
|
888
|
-
? [...resolved.mentions, ...(mapping.members ?? [])]
|
|
889
|
-
.map((m) => this.deps.identities?.getInstalled(m)?.botToken)
|
|
890
|
-
.find((t) => !!t)
|
|
891
|
-
: undefined;
|
|
892
|
-
// Swallowing this outright cost an evening: the owner saw no eyes and no
|
|
893
|
-
// placeholder and reasonably concluded nothing had arrived, while the
|
|
894
|
-
// message was in fact routed, dispatched and answered (2026-09-21).
|
|
895
|
-
// Cosmetic, so still non-fatal β but never again silent.
|
|
896
|
-
await this.deps.slack
|
|
897
|
-
.addReaction(message.channelId, message.ts, SLACK_TEAM_CHANNEL_CONSTANTS.INBOUND_REACTION, reactAs)
|
|
898
|
-
.catch((err) => {
|
|
899
|
-
this.logger.warn('Could not acknowledge the message with a reaction', {
|
|
900
|
-
slackChannel: mapping.slackChannelName ?? message.channelId,
|
|
901
|
-
adhoc: isAdhocMapping(mapping),
|
|
902
|
-
reactedAs: reactAs ? 'agent-bot' : 'master-bot',
|
|
903
|
-
error: describeSlackError(err).code,
|
|
904
|
-
});
|
|
905
|
-
});
|
|
906
895
|
// Ad-hoc channels grow their huddle as new agents get @'d there.
|
|
907
896
|
if (isAdhocMapping(mapping) && resolved.mentions.length > 0) {
|
|
908
897
|
const next = [...new Set([...(mapping.members ?? []), ...resolved.mentions])];
|
|
@@ -912,22 +901,45 @@ export class SlackTeamChannelService {
|
|
|
912
901
|
await this.save();
|
|
913
902
|
}
|
|
914
903
|
}
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
904
|
+
// Who will receive this, and who owes a reply β decided by the same rules
|
|
905
|
+
// delivery uses, but *before* delivery, which can take a minute or two
|
|
906
|
+
// when an agent has to be cold-started. The owner should not look at an
|
|
907
|
+
// unacknowledged message for that long.
|
|
908
|
+
const dispatcherForPlan = this.deps.getDispatcher();
|
|
909
|
+
const dispatchOptions = {
|
|
910
|
+
threadId: threadId ?? persisted.id,
|
|
911
|
+
replyVia: 'reply-channel',
|
|
912
|
+
// A local agent's own message (fanned out to the colleagues it @'d)
|
|
913
|
+
// must not come back to its author.
|
|
914
|
+
...(remoteAgent ? { excludeSessions: [remoteAgent] } : {}),
|
|
915
|
+
};
|
|
916
|
+
const planned = dispatcherForPlan?.planHuddleTargets
|
|
917
|
+
? await dispatcherForPlan.planHuddleTargets(channel, persisted, dispatchOptions).catch(() => null)
|
|
918
|
+
: null;
|
|
919
|
+
await this.acknowledgeSeen(message, mapping, resolved.mentions, planned);
|
|
920
|
+
// Agents that must reply get a placeholder straight away β "waking upβ¦"
|
|
921
|
+
// for an idle agent (a cold start is 1β2 minutes), "is working on itβ¦"
|
|
922
|
+
// once it holds the message. Agents that were only told, and may or may
|
|
923
|
+
// not decide to answer, get none: a placeholder is a promise of a reply,
|
|
924
|
+
// and they announce their own with `reply-channel --working` if they
|
|
925
|
+
// take it on. With a plan this covers the case the old heuristic missed β
|
|
926
|
+
// a bare follow-up in a thread, which the last speaker must answer.
|
|
927
|
+
let owing;
|
|
928
|
+
if (planned) {
|
|
929
|
+
owing = [...planned].filter(([, mode]) => mode === 'required').map(([session]) => session);
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
owing = resolved.mentions;
|
|
933
|
+
if (owing.length === 0 && !message.threadTs && team) {
|
|
925
934
|
// Same rule as the dispatcher's huddleLeaderFor: the team leader, else the first member.
|
|
926
935
|
const leader = members.find((m) => String(m.role) === 'team-leader' || String(m.role) === 'tech-lead') ?? members[0];
|
|
927
936
|
if (leader)
|
|
928
|
-
|
|
937
|
+
owing = [leader.sessionName];
|
|
929
938
|
}
|
|
930
|
-
|
|
939
|
+
}
|
|
940
|
+
const typingTargets = [];
|
|
941
|
+
if (this.deps.typing) {
|
|
942
|
+
for (const session of owing) {
|
|
931
943
|
const member = members.find((m) => m.sessionName === session);
|
|
932
944
|
const installed = this.deps.identities?.getInstalled(session);
|
|
933
945
|
// Own bot when installed; otherwise the master bot wearing the agent's
|
|
@@ -955,12 +967,8 @@ export class SlackTeamChannelService {
|
|
|
955
967
|
.join(' Β· ');
|
|
956
968
|
}
|
|
957
969
|
dispatch = await dispatcher.dispatchMessage(channel, persisted, {
|
|
958
|
-
|
|
959
|
-
replyVia: 'reply-channel',
|
|
970
|
+
...dispatchOptions,
|
|
960
971
|
...(roster ? { channelRoster: roster } : {}),
|
|
961
|
-
// A local agent's own message (fanned out to the colleagues it @'d)
|
|
962
|
-
// must not come back to its author.
|
|
963
|
-
...(remoteAgent ? { excludeSessions: [remoteAgent] } : {}),
|
|
964
972
|
});
|
|
965
973
|
}
|
|
966
974
|
else {
|
|
@@ -993,19 +1001,120 @@ export class SlackTeamChannelService {
|
|
|
993
1001
|
});
|
|
994
1002
|
return { mapping, message: persisted, mentions: resolved.mentions, dispatch };
|
|
995
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* The local agent whose own app delivered this copy, if any.
|
|
1006
|
+
*
|
|
1007
|
+
* A direct message is never a room: an agent's DM is handled by the DM
|
|
1008
|
+
* service, and anything that falls through from there belongs to the
|
|
1009
|
+
* orchestrator path, not to an ad-hoc huddle.
|
|
1010
|
+
*
|
|
1011
|
+
* @param message - The inbound message
|
|
1012
|
+
* @returns The agent's session name, or null
|
|
1013
|
+
*/
|
|
1014
|
+
async localReceivingAgent(message) {
|
|
1015
|
+
const session = message.agentSession;
|
|
1016
|
+
if (!session || message.channelId.startsWith('D'))
|
|
1017
|
+
return null;
|
|
1018
|
+
if (this.deps.isLocalAgent)
|
|
1019
|
+
return this.deps.isLocalAgent(session) ? session : null;
|
|
1020
|
+
const teams = await this.deps.storage.getTeams();
|
|
1021
|
+
return teams.some((t) => teamChannelMembers(t).some((m) => m.sessionName === session)) ? session : null;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Put π on the message β one per agent that will receive it.
|
|
1025
|
+
*
|
|
1026
|
+
* It used to be a single reaction meaning "Crewly got this". The owner
|
|
1027
|
+
* reads it as "how many agents saw this", and with several agents in a
|
|
1028
|
+
* room that is the more useful answer: three agents who will each weigh a
|
|
1029
|
+
* message should show three eyes, and an agent that was not handed the
|
|
1030
|
+
* message should show none, so the count is honest rather than "everyone
|
|
1031
|
+
* in the channel". Each agent reacts with its own bot.
|
|
1032
|
+
*
|
|
1033
|
+
* When nobody in particular receives it, or no receiving agent has a bot
|
|
1034
|
+
* of its own, one reaction still goes on so the owner can see the message
|
|
1035
|
+
* arrived β the 2026-09-21 lesson: no eyes reads as nothing arrived.
|
|
1036
|
+
*
|
|
1037
|
+
* @param message - The inbound message
|
|
1038
|
+
* @param mapping - Its channel mapping
|
|
1039
|
+
* @param mentions - Agents @'d in it
|
|
1040
|
+
* @param planned - Who will receive it, from the dispatcher; null when unknown
|
|
1041
|
+
*/
|
|
1042
|
+
async acknowledgeSeen(message, mapping, mentions, planned) {
|
|
1043
|
+
const react = (token) => this.deps.slack.addReaction(message.channelId, message.ts, SLACK_TEAM_CHANNEL_CONSTANTS.INBOUND_REACTION, token);
|
|
1044
|
+
let seen = 0;
|
|
1045
|
+
const absent = [];
|
|
1046
|
+
for (const session of planned?.keys() ?? []) {
|
|
1047
|
+
const token = this.deps.identities?.getInstalled(session)?.botToken;
|
|
1048
|
+
if (!token)
|
|
1049
|
+
continue;
|
|
1050
|
+
try {
|
|
1051
|
+
await react(token);
|
|
1052
|
+
seen += 1;
|
|
1053
|
+
}
|
|
1054
|
+
catch (err) {
|
|
1055
|
+
const code = describeSlackError(err).code;
|
|
1056
|
+
if (code === 'channel_not_found' || code === 'not_in_channel')
|
|
1057
|
+
absent.push(session);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (absent.length > 0) {
|
|
1061
|
+
// The agent still gets the message; its bot is just not in the room
|
|
1062
|
+
// (removed from a private channel after it joined the roster, say).
|
|
1063
|
+
this.logger.info('Agent bot not in the channel β no eyes from it', {
|
|
1064
|
+
slackChannel: mapping.slackChannelName ?? message.channelId,
|
|
1065
|
+
agents: absent,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
if (seen > 0)
|
|
1069
|
+
return;
|
|
1070
|
+
// Nobody in particular, or nobody with a bot of their own. In an ad-hoc
|
|
1071
|
+
// (often private) channel the master bot may not be a member, so try the
|
|
1072
|
+
// agents' bots β @'d ones first β and the master bot last. Only a
|
|
1073
|
+
// not-a-member failure is worth another identity: already-reacted, rate
|
|
1074
|
+
// limits and a bad ts fail identically for every bot.
|
|
1075
|
+
const fallback = isAdhocMapping(mapping)
|
|
1076
|
+
? [
|
|
1077
|
+
...new Set([...mentions, ...(mapping.members ?? [])]
|
|
1078
|
+
.map((m) => this.deps.identities?.getInstalled(m)?.botToken)
|
|
1079
|
+
.filter((t) => !!t)),
|
|
1080
|
+
undefined,
|
|
1081
|
+
]
|
|
1082
|
+
: [undefined];
|
|
1083
|
+
let lastError = '';
|
|
1084
|
+
for (const token of fallback) {
|
|
1085
|
+
try {
|
|
1086
|
+
await react(token);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
catch (err) {
|
|
1090
|
+
lastError = describeSlackError(err).code;
|
|
1091
|
+
if (lastError !== 'channel_not_found')
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// Swallowing this outright cost an evening (2026-09-21): cosmetic, so
|
|
1096
|
+
// non-fatal, but never silent.
|
|
1097
|
+
this.logger.warn('Could not acknowledge the message with a reaction', {
|
|
1098
|
+
slackChannel: mapping.slackChannelName ?? message.channelId,
|
|
1099
|
+
adhoc: isAdhocMapping(mapping),
|
|
1100
|
+
identitiesTried: fallback.length,
|
|
1101
|
+
error: lastError,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
996
1104
|
/**
|
|
997
1105
|
* Any Slack channel becomes routable the moment a local agent's bot is
|
|
998
|
-
* @'d in it
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1106
|
+
* @'d in it, or a message reaches us through a local agent's own app
|
|
1107
|
+
* (Slack only delivers to apps that are members, so that proves the bot
|
|
1108
|
+
* is in the channel). The owner invited agents from different teams into
|
|
1109
|
+
* a private channel, say: a huddle is created for the channel with those
|
|
1110
|
+
* agents as members and mapped under the synthetic team id
|
|
1111
|
+
* `adhoc:<channel>`. Anything else is left to the caller (orchestrator
|
|
1112
|
+
* path).
|
|
1002
1113
|
*
|
|
1003
1114
|
* @param message - The inbound message
|
|
1004
1115
|
* @returns The new mapping, or null when no local agent was @'d
|
|
1005
1116
|
*/
|
|
1006
1117
|
async ensureAdhocChannel(message) {
|
|
1007
|
-
if (!message.text || !message.text.includes('<@') && !message.text.includes('@'))
|
|
1008
|
-
return null;
|
|
1009
1118
|
const teams = await this.deps.storage.getTeams();
|
|
1010
1119
|
const members = teams.flatMap((t) => teamChannelMembers(t));
|
|
1011
1120
|
if (members.length === 0)
|
|
@@ -1020,10 +1129,17 @@ export class SlackTeamChannelService {
|
|
|
1020
1129
|
// Only a real Slack mention of an agent's bot user counts here β a bare
|
|
1021
1130
|
// "@name" in some unrelated channel must not hijack it.
|
|
1022
1131
|
const botIds = new Set(candidates.map((c) => c.botUserId).filter((id) => !!id));
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
return null;
|
|
1132
|
+
const text = message.text ?? '';
|
|
1133
|
+
const mentionedBots = [...text.matchAll(/<@([A-Z0-9]+)>/g)].map((m) => m[1]).filter((id) => botIds.has(id));
|
|
1026
1134
|
const sessions = candidates.filter((c) => c.botUserId && mentionedBots.includes(c.botUserId)).map((c) => c.sessionName);
|
|
1135
|
+
// The copy came through a local agent's own app: that agent's bot is in
|
|
1136
|
+
// this channel, which makes the channel a room it belongs to even if
|
|
1137
|
+
// nobody has addressed it yet.
|
|
1138
|
+
const receiving = await this.localReceivingAgent(message);
|
|
1139
|
+
if (receiving && !sessions.includes(receiving))
|
|
1140
|
+
sessions.push(receiving);
|
|
1141
|
+
if (sessions.length === 0)
|
|
1142
|
+
return null;
|
|
1027
1143
|
// The master bot is usually not in this channel (private); the name is
|
|
1028
1144
|
// best-effort and falls back to the id.
|
|
1029
1145
|
const info = await this.deps.slack.getChannelInfo(message.channelId).catch(() => null);
|
|
@@ -1161,6 +1277,51 @@ export class SlackTeamChannelService {
|
|
|
1161
1277
|
* `slackThreadTs`; failing that, the latest Slack-origin root in the
|
|
1162
1278
|
* huddle; failing that, the channel top level.
|
|
1163
1279
|
*/
|
|
1280
|
+
/**
|
|
1281
|
+
* Show "<agent> is working on itβ¦" in the thread, at the agent's request.
|
|
1282
|
+
*
|
|
1283
|
+
* Agents that must answer get this placeholder the moment the message
|
|
1284
|
+
* arrives. Agents that were only *told* β a message nobody addressed to
|
|
1285
|
+
* them, passed along so they can judge whether it concerns them β get none,
|
|
1286
|
+
* because a placeholder promises a reply and most of them will not send
|
|
1287
|
+
* one. When one of them does decide to answer, it calls this, so the owner
|
|
1288
|
+
* sees who has taken the message on: two agents deciding to answer show two
|
|
1289
|
+
* placeholders, which is exactly the signal the owner asked for.
|
|
1290
|
+
*
|
|
1291
|
+
* The key matches the one the reply is mirrored under, so the agent's
|
|
1292
|
+
* actual answer replaces the placeholder rather than landing beside it.
|
|
1293
|
+
*
|
|
1294
|
+
* @param input - Chat channel and thread the agent is answering in
|
|
1295
|
+
* @returns Whether a placeholder is now showing, or why not
|
|
1296
|
+
*/
|
|
1297
|
+
async beginWorkingForAgent(input) {
|
|
1298
|
+
const mapping = (this.store?.mappings ?? []).find((m) => m.chatChannelId === input.chatChannelId);
|
|
1299
|
+
if (!mapping)
|
|
1300
|
+
return { ok: false, reason: 'not_a_slack_channel' };
|
|
1301
|
+
if (!this.deps.typing)
|
|
1302
|
+
return { ok: false, reason: 'placeholders_unavailable' };
|
|
1303
|
+
if (!this.deps.slack.isConnected())
|
|
1304
|
+
return { ok: false, reason: 'slack_not_connected' };
|
|
1305
|
+
const threadTs = this.resolveOutboundThreadTs(mapping, {
|
|
1306
|
+
channelId: input.chatChannelId,
|
|
1307
|
+
senderId: input.agentSession,
|
|
1308
|
+
...(input.threadId ? { threadId: input.threadId } : {}),
|
|
1309
|
+
});
|
|
1310
|
+
const team = (await this.deps.storage.getTeams()).find((t) => (t.members ?? []).some((m) => m.sessionName === input.agentSession));
|
|
1311
|
+
const member = team?.members.find((m) => m.sessionName === input.agentSession);
|
|
1312
|
+
const installed = this.deps.identities?.getInstalled(input.agentSession) ?? null;
|
|
1313
|
+
const displayName = member?.name ?? input.agentSession;
|
|
1314
|
+
const identity = installed
|
|
1315
|
+
? { botToken: installed.botToken, displayName }
|
|
1316
|
+
: { displayName, ...slackIdentityFor(member, input.agentSession) };
|
|
1317
|
+
await this.deps.typing.begin({ agentSession: input.agentSession, slackChannelId: mapping.slackChannelId, ...(threadTs ? { threadTs } : {}) }, identity, 'typing');
|
|
1318
|
+
this.logger.info('Agent took a message on', {
|
|
1319
|
+
agentSession: input.agentSession,
|
|
1320
|
+
slackChannel: mapping.slackChannelName,
|
|
1321
|
+
threaded: Boolean(threadTs),
|
|
1322
|
+
});
|
|
1323
|
+
return { ok: true, slackChannelId: mapping.slackChannelId, ...(threadTs ? { threadTs } : {}) };
|
|
1324
|
+
}
|
|
1164
1325
|
/**
|
|
1165
1326
|
* Put a file into the Slack channel an agent is replying in.
|
|
1166
1327
|
*
|