crewly 1.20.356 → 1.20.358
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/constants.ts +11 -0
- package/dist/backend/backend/src/constants.d.ts +254 -17
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +257 -17
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +70 -4
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +10 -2
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +27 -2
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.d.ts +34 -1
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js +69 -13
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts +8 -0
- package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.js +26 -2
- package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +9 -0
- package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/resource-mode.service.js +16 -0
- package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts +44 -3
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.js +126 -11
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +2 -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 +9 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.d.ts +150 -0
- package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.js +319 -0
- package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.js.map +1 -0
- package/dist/backend/backend/src/services/memory/auto-learning.subscriber.d.ts +9 -7
- package/dist/backend/backend/src/services/memory/auto-learning.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/auto-learning.subscriber.js +19 -17
- package/dist/backend/backend/src/services/memory/auto-learning.subscriber.js.map +1 -1
- package/dist/backend/backend/src/services/memory/memory.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/memory.service.js +11 -0
- package/dist/backend/backend/src/services/memory/memory.service.js.map +1 -1
- package/dist/backend/backend/src/services/memory/startup-briefing.service.d.ts +104 -0
- package/dist/backend/backend/src/services/memory/startup-briefing.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/memory/startup-briefing.service.js +302 -0
- package/dist/backend/backend/src/services/memory/startup-briefing.service.js.map +1 -0
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.d.ts +33 -1
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.js +69 -4
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.wiring.d.ts +24 -1
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.wiring.js +54 -3
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/orc/owner-completion-report.service.d.ts +9 -0
- package/dist/backend/backend/src/services/orc/owner-completion-report.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/owner-completion-report.service.js +16 -2
- package/dist/backend/backend/src/services/orc/owner-completion-report.service.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-prefixes.d.ts +30 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-prefixes.d.ts.map +1 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-prefixes.js +48 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-prefixes.js.map +1 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +8 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +29 -8
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.d.ts +27 -0
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.js +78 -24
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.js.map +1 -1
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.d.ts +23 -0
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js +38 -1
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/slack/agent-coordination.d.ts +119 -0
- package/dist/backend/backend/src/services/slack/agent-coordination.d.ts.map +1 -0
- package/dist/backend/backend/src/services/slack/agent-coordination.js +174 -0
- package/dist/backend/backend/src/services/slack/agent-coordination.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.js +7 -0
- package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +167 -0
- 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 +619 -9
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-thread-context.service.d.ts +22 -0
- package/dist/backend/backend/src/services/slack/slack-thread-context.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-thread-context.service.js +49 -12
- package/dist/backend/backend/src/services/slack/slack-thread-context.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts +7 -0
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js +25 -4
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-chat.d.ts +21 -1
- package/dist/backend/backend/src/services/team-move/team-move-chat.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-chat.js +54 -2
- package/dist/backend/backend/src/services/team-move/team-move-chat.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-exporter.d.ts +9 -4
- package/dist/backend/backend/src/services/team-move/team-move-exporter.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-exporter.js +32 -10
- package/dist/backend/backend/src/services/team-move/team-move-exporter.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-importer.d.ts +19 -0
- package/dist/backend/backend/src/services/team-move/team-move-importer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-importer.js +78 -3
- package/dist/backend/backend/src/services/team-move/team-move-importer.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-runtime.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-runtime.js +8 -0
- package/dist/backend/backend/src/services/team-move/team-move-runtime.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-transcripts.d.ts +106 -0
- package/dist/backend/backend/src/services/team-move/team-move-transcripts.d.ts.map +1 -0
- package/dist/backend/backend/src/services/team-move/team-move-transcripts.js +212 -0
- package/dist/backend/backend/src/services/team-move/team-move-transcripts.js.map +1 -0
- package/dist/backend/backend/src/services/team-move/team-move.types.d.ts +20 -0
- package/dist/backend/backend/src/services/team-move/team-move.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move.types.js.map +1 -1
- package/dist/backend/backend/src/services/v3/agent-auto-claim.service.d.ts +2 -2
- package/dist/backend/backend/src/services/v3/agent-auto-claim.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/agent-auto-claim.service.js +11 -4
- package/dist/backend/backend/src/services/v3/agent-auto-claim.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.js +3 -0
- package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep.service.js +4 -1
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.js +4 -0
- package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-ingest.service.d.ts +33 -4
- package/dist/backend/backend/src/services/wiki/wiki-ingest.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-ingest.service.js +75 -7
- package/dist/backend/backend/src/services/wiki/wiki-ingest.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-lint.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-lint.service.js +3 -1
- package/dist/backend/backend/src/services/wiki/wiki-lint.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-log-compile.service.d.ts +116 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-compile.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-compile.service.js +227 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-compile.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-entries.d.ts +142 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-entries.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-entries.js +222 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-entries.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.d.ts +93 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.js +142 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.d.ts +104 -0
- package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.js +192 -0
- package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-query.service.d.ts +3 -2
- package/dist/backend/backend/src/services/wiki/wiki-query.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-query.service.js +23 -39
- package/dist/backend/backend/src/services/wiki/wiki-query.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-queue-drain.service.d.ts +105 -0
- package/dist/backend/backend/src/services/wiki/wiki-queue-drain.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-queue-drain.service.js +195 -0
- package/dist/backend/backend/src/services/wiki/wiki-queue-drain.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-recent.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-recent.service.js +3 -0
- package/dist/backend/backend/src/services/wiki/wiki-recent.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-search.service.d.ts +28 -5
- package/dist/backend/backend/src/services/wiki/wiki-search.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-search.service.js +96 -25
- package/dist/backend/backend/src/services/wiki/wiki-search.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-vault-walk.d.ts +11 -0
- package/dist/backend/backend/src/services/wiki/wiki-vault-walk.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-vault-walk.js +17 -1
- package/dist/backend/backend/src/services/wiki/wiki-vault-walk.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/backend/config/constants.d.ts +11 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +11 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +254 -17
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +257 -17
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +10 -2
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +27 -2
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.d.ts +34 -1
- package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js +69 -13
- package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +9 -0
- package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/resource-mode.service.js +16 -0
- package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts +44 -3
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.js +126 -11
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +2 -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 +9 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/cli/backend/src/services/memory/memory.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/memory/memory.service.js +11 -0
- package/dist/cli/backend/src/services/memory/memory.service.js.map +1 -1
- package/dist/cli/backend/src/services/memory/startup-briefing.service.d.ts +104 -0
- package/dist/cli/backend/src/services/memory/startup-briefing.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/memory/startup-briefing.service.js +302 -0
- package/dist/cli/backend/src/services/memory/startup-briefing.service.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.d.ts +33 -1
- package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.js +69 -4
- package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/agent-coordination.d.ts +119 -0
- package/dist/cli/backend/src/services/slack/agent-coordination.d.ts.map +1 -0
- package/dist/cli/backend/src/services/slack/agent-coordination.js +174 -0
- package/dist/cli/backend/src/services/slack/agent-coordination.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +167 -0
- 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 +619 -9
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-thread-context.service.d.ts +22 -0
- package/dist/cli/backend/src/services/slack/slack-thread-context.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-thread-context.service.js +49 -12
- package/dist/cli/backend/src/services/slack/slack-thread-context.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts +7 -0
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js +25 -4
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
- package/dist/cli/backend/src/services/wiki/wiki-ingest.service.d.ts +33 -4
- package/dist/cli/backend/src/services/wiki/wiki-ingest.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/wiki/wiki-ingest.service.js +75 -7
- package/dist/cli/backend/src/services/wiki/wiki-ingest.service.js.map +1 -1
- package/dist/cli/backend/src/services/wiki/wiki-log-entries.d.ts +142 -0
- package/dist/cli/backend/src/services/wiki/wiki-log-entries.d.ts.map +1 -0
- package/dist/cli/backend/src/services/wiki/wiki-log-entries.js +222 -0
- package/dist/cli/backend/src/services/wiki/wiki-log-entries.js.map +1 -0
- package/dist/cli/backend/src/services/wiki/wiki-recent.service.d.ts +51 -0
- package/dist/cli/backend/src/services/wiki/wiki-recent.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/wiki/wiki-recent.service.js +105 -0
- package/dist/cli/backend/src/services/wiki/wiki-recent.service.js.map +1 -0
- package/dist/cli/backend/src/services/wiki/wiki-search.service.d.ts +28 -5
- package/dist/cli/backend/src/services/wiki/wiki-search.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/wiki/wiki-search.service.js +96 -25
- package/dist/cli/backend/src/services/wiki/wiki-search.service.js.map +1 -1
- package/dist/cli/backend/src/services/wiki/wiki-vault-walk.d.ts +11 -0
- package/dist/cli/backend/src/services/wiki/wiki-vault-walk.d.ts.map +1 -1
- package/dist/cli/backend/src/services/wiki/wiki-vault-walk.js +17 -1
- package/dist/cli/backend/src/services/wiki/wiki-vault-walk.js.map +1 -1
- package/dist/cli/config/constants.d.ts +11 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +11 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/package.json +1 -1
|
@@ -55,6 +55,8 @@ import { resolveSlackMentions, extractNativeMentionIds, leadingNameMention } fro
|
|
|
55
55
|
import { toSlackMrkdwn } from './slack-mrkdwn.js';
|
|
56
56
|
import { renderSlackThreadContext } from './slack-thread-context.service.js';
|
|
57
57
|
import { trackSlackDelivery, SLACK_DELIVERY_METADATA_KEY } from './slack-outbound-delivery.js';
|
|
58
|
+
import { classifyAgentAddressing, stripTicketIds, CoordinationDedupe } from './agent-coordination.js';
|
|
59
|
+
import { AGENT_COORDINATION_CONSTANTS } from '../../constants.js';
|
|
58
60
|
const EMPTY_STORE = {
|
|
59
61
|
version: 1,
|
|
60
62
|
autoCreate: true,
|
|
@@ -458,6 +460,17 @@ export class SlackTeamChannelService {
|
|
|
458
460
|
* guessing.
|
|
459
461
|
*/
|
|
460
462
|
rootPosts = new Map();
|
|
463
|
+
/** Near-identical agent-to-agent repeats (crewly#1340). */
|
|
464
|
+
coordinationDedupe = new CoordinationDedupe();
|
|
465
|
+
/**
|
|
466
|
+
* `chatChannelId:helper` → the agent that last asked it something here, and
|
|
467
|
+
* when — so a reply the helper cannot post in Slack goes back to the asker.
|
|
468
|
+
*/
|
|
469
|
+
askedBy = new Map();
|
|
470
|
+
/** `slackChannelId:YYYY-MM-DD` → Slack ts of that day's cross-machine coordination thread. */
|
|
471
|
+
coordinationThreads = new Map();
|
|
472
|
+
/** Ticket prefixes, cached. */
|
|
473
|
+
ticketPrefixCache = null;
|
|
461
474
|
constructor(deps) {
|
|
462
475
|
this.deps = deps;
|
|
463
476
|
this.storePath =
|
|
@@ -485,6 +498,7 @@ export class SlackTeamChannelService {
|
|
|
485
498
|
void this.inviteInstalledEverywhere(record.agentSession);
|
|
486
499
|
}) ?? null;
|
|
487
500
|
this.started = true;
|
|
501
|
+
await this.healMissingChatChannels();
|
|
488
502
|
await this.restoreUnanswered();
|
|
489
503
|
this.logger.info('Slack team channels started', {
|
|
490
504
|
mappings: this.store?.mappings.length ?? 0,
|
|
@@ -512,6 +526,57 @@ export class SlackTeamChannelService {
|
|
|
512
526
|
this.pruneInterval.unref?.();
|
|
513
527
|
}
|
|
514
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Give every mapping a chat channel that exists. A mapping whose
|
|
531
|
+
* `chatChannelId` is not in the chat store (a team moved here without its
|
|
532
|
+
* room, a wiped chat.db) made every Slack turn reach its agents with no
|
|
533
|
+
* local thread history and `GET …/messages` answer 404 (crewly#1344,
|
|
534
|
+
* #air-steamfun). A fresh huddle with the known roster takes its place.
|
|
535
|
+
*
|
|
536
|
+
* @returns Mappings re-pointed
|
|
537
|
+
*/
|
|
538
|
+
async healMissingChatChannels() {
|
|
539
|
+
const store = await this.load();
|
|
540
|
+
let healed = 0;
|
|
541
|
+
for (const mapping of store.mappings) {
|
|
542
|
+
let exists = false;
|
|
543
|
+
try {
|
|
544
|
+
exists = this.deps.chat.getChannelForBridge(mapping.chatChannelId) !== null;
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
exists = true; // the store cannot say: leave the mapping alone
|
|
548
|
+
}
|
|
549
|
+
if (exists)
|
|
550
|
+
continue;
|
|
551
|
+
try {
|
|
552
|
+
const team = (await this.deps.storage.getTeams()).find((t) => t.id === mapping.teamId);
|
|
553
|
+
const roster = mapping.members && mapping.members.length > 0 ? mapping.members : team ? teamChannelMembers(team).map((m) => m.sessionName) : [];
|
|
554
|
+
const huddle = this.deps.chat.createHuddle({
|
|
555
|
+
name: `#${mapping.slackChannelName}`,
|
|
556
|
+
purpose: team ? `Slack team channel for ${team.name}` : `Slack channel #${mapping.slackChannelName}`,
|
|
557
|
+
memberSessions: roster.length > 0 ? roster : [`team:${mapping.teamId}`],
|
|
558
|
+
principal: { userId: 'system', source: 'oss' },
|
|
559
|
+
});
|
|
560
|
+
this.logger.warn('Slack channel mapping pointed at a chat channel that does not exist here — created a new one', {
|
|
561
|
+
slackChannel: `#${mapping.slackChannelName}`,
|
|
562
|
+
teamId: mapping.teamId,
|
|
563
|
+
missing: mapping.chatChannelId,
|
|
564
|
+
chatChannelId: huddle.id,
|
|
565
|
+
});
|
|
566
|
+
mapping.chatChannelId = huddle.id;
|
|
567
|
+
healed += 1;
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
this.logger.warn('Could not recreate the chat channel behind a Slack mapping', {
|
|
571
|
+
slackChannel: `#${mapping.slackChannelName}`,
|
|
572
|
+
error: err instanceof Error ? err.message : String(err),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (healed > 0)
|
|
577
|
+
await this.save();
|
|
578
|
+
return healed;
|
|
579
|
+
}
|
|
515
580
|
/**
|
|
516
581
|
* Drop ad-hoc room members whose bot is not in the Slack channel.
|
|
517
582
|
*
|
|
@@ -1878,10 +1943,19 @@ export class SlackTeamChannelService {
|
|
|
1878
1943
|
...(bareMentionNote ? { bareMentionNote } : {}),
|
|
1879
1944
|
...(slackContext
|
|
1880
1945
|
? {
|
|
1881
|
-
slackContextFor: (session) =>
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1946
|
+
slackContextFor: (session) => {
|
|
1947
|
+
const block = renderSlackThreadContext(slackContext, {
|
|
1948
|
+
botUserId: this.deps.identities?.get(session)?.botUserId,
|
|
1949
|
+
name: members.find((m) => m.sessionName === session)?.name,
|
|
1950
|
+
});
|
|
1951
|
+
// Where the whole thread lives on this machine, the way the
|
|
1952
|
+
// orchestrator path names its thread file (crewly#1344): the
|
|
1953
|
+
// block is a clipped window, the chat thread is the record.
|
|
1954
|
+
const localThread = threadId ?? persisted.threadId;
|
|
1955
|
+
return block && localThread
|
|
1956
|
+
? `${block}\n[Full thread: chat channel ${mapping.chatChannelId}, thread ${localThread} — read it with search-chat when the lines above are not enough]`
|
|
1957
|
+
: block;
|
|
1958
|
+
},
|
|
1885
1959
|
}
|
|
1886
1960
|
: {}),
|
|
1887
1961
|
});
|
|
@@ -3397,6 +3471,7 @@ export class SlackTeamChannelService {
|
|
|
3397
3471
|
*/
|
|
3398
3472
|
async mirrorOutboundDetailed(dto) {
|
|
3399
3473
|
const skipped = { attempted: false, delivery: null };
|
|
3474
|
+
let mappedForFailure = null;
|
|
3400
3475
|
try {
|
|
3401
3476
|
// Every reason a reply does not reach Slack is logged. Until now all
|
|
3402
3477
|
// four were silent `return false`, so an agent could answer, be told
|
|
@@ -3440,6 +3515,15 @@ export class SlackTeamChannelService {
|
|
|
3440
3515
|
const mapping = this.findByChatChannelId(dto.channelId);
|
|
3441
3516
|
if (!mapping)
|
|
3442
3517
|
return skip('channel-not-mapped-to-slack');
|
|
3518
|
+
mappedForFailure = mapping;
|
|
3519
|
+
// Agent-to-agent coordination (crewly#1340). Agents on this machine
|
|
3520
|
+
// talking to each other stay inside Crewly: the owner had 20+ unread
|
|
3521
|
+
// top-level posts in 90 minutes from three agents on his own Mac
|
|
3522
|
+
// (2026-10-16, #content-team). Decided before the Slack check, since
|
|
3523
|
+
// inside Crewly needs no Slack.
|
|
3524
|
+
const coordination = await this.coordinate(mapping, dto);
|
|
3525
|
+
if (coordination.handled)
|
|
3526
|
+
return skip(coordination.reason);
|
|
3443
3527
|
if (!this.deps.slack.isConnected())
|
|
3444
3528
|
return skip('slack-not-connected');
|
|
3445
3529
|
// A reply under a top-level post that is still being sent waits for
|
|
@@ -3450,15 +3534,28 @@ export class SlackTeamChannelService {
|
|
|
3450
3534
|
const resolvedTs = this.resolveOutboundThreadTs(mapping, dto);
|
|
3451
3535
|
if (resolvedTs === null)
|
|
3452
3536
|
return skip('thread-key-names-another-channel');
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3537
|
+
// Agents on other machines can only be reached through Slack: in the
|
|
3538
|
+
// room's coordination thread of the day, not as a new top-level post.
|
|
3539
|
+
const threadTs = resolvedTs || (coordination.addressing === 'cross-machine' ? this.coordinationThreadTs(mapping) : undefined);
|
|
3540
|
+
if (!threadTs) {
|
|
3541
|
+
const posted = await this.postTopLevel(mapping, dto);
|
|
3542
|
+
if (coordination.addressing === 'cross-machine')
|
|
3543
|
+
this.rememberCoordinationThread(mapping, dto);
|
|
3544
|
+
return posted;
|
|
3545
|
+
}
|
|
3546
|
+
if (!resolvedTs) {
|
|
3547
|
+
this.logger.info('Agent-to-agent message across machines posted in the room\'s coordination thread', {
|
|
3548
|
+
slackChannel: mapping.slackChannelName,
|
|
3549
|
+
sender: dto.senderId,
|
|
3550
|
+
threadTs,
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3456
3553
|
const member = await this.findPostingMember(mapping, dto.senderId);
|
|
3457
3554
|
// A real bot user (Cloud-provisioned identity) beats the cosmetic
|
|
3458
3555
|
// username/icon override.
|
|
3459
3556
|
const installed = this.deps.identities?.getInstalled(dto.senderId) ?? null;
|
|
3460
3557
|
const identity = installed ? { botToken: installed.botToken } : slackIdentityFor(member, dto.senderId);
|
|
3461
|
-
const text = await this.linkAgentMentions(toSlackMrkdwn(dto.content), mapping.slackChannelId);
|
|
3558
|
+
const text = await this.linkAgentMentions(toSlackMrkdwn(await this.withoutTicketIds(dto.content)), mapping.slackChannelId);
|
|
3462
3559
|
if (this.deps.typing) {
|
|
3463
3560
|
const typingKey = { agentSession: dto.senderId, slackChannelId: mapping.slackChannelId, ...(threadTs ? { threadTs } : {}) };
|
|
3464
3561
|
const typingIdentity = installed
|
|
@@ -3506,6 +3603,14 @@ export class SlackTeamChannelService {
|
|
|
3506
3603
|
}
|
|
3507
3604
|
catch (err) {
|
|
3508
3605
|
const error = err instanceof Error ? err.message : String(err);
|
|
3606
|
+
// A helper whose bot is not in this Slack channel (Sam in #content-team,
|
|
3607
|
+
// `channel_not_found` on every post, 2026-10-16) does not fail and is
|
|
3608
|
+
// not retried: its reply goes to the agent that asked it, who is in the
|
|
3609
|
+
// room and posts the update (crewly#1340).
|
|
3610
|
+
if (mappedForFailure && dto.senderType === 'agent' && isNotInChannelError(err)) {
|
|
3611
|
+
if (await this.handBackUnpostable(mappedForFailure, dto, describeSlackError(err).code))
|
|
3612
|
+
return skipped;
|
|
3613
|
+
}
|
|
3509
3614
|
this.logger.warn('Outbound mirror to Slack failed', {
|
|
3510
3615
|
channelId: dto.channelId,
|
|
3511
3616
|
sender: dto.senderId,
|
|
@@ -3520,6 +3625,476 @@ export class SlackTeamChannelService {
|
|
|
3520
3625
|
};
|
|
3521
3626
|
}
|
|
3522
3627
|
}
|
|
3628
|
+
// -------------------------------------------------------------------------
|
|
3629
|
+
// Agent-to-agent coordination (crewly#1340)
|
|
3630
|
+
// -------------------------------------------------------------------------
|
|
3631
|
+
/**
|
|
3632
|
+
* Decide what happens to an agent's room post that addresses other agents.
|
|
3633
|
+
*
|
|
3634
|
+
* The model is a company group chat: whoever the room asked is its one
|
|
3635
|
+
* voice; it asks its helpers privately and posts one update when their
|
|
3636
|
+
* result is in. So a post that @'s only agents on this machine — no
|
|
3637
|
+
* person, no name nobody knows, not inside a thread a person is in — is
|
|
3638
|
+
* delivered inside Crewly (the same dispatcher prompt the Slack round-trip
|
|
3639
|
+
* produced) and never posted to Slack. A reply in such an internal thread
|
|
3640
|
+
* stays internal too, and goes to the thread's other agents when it @'s
|
|
3641
|
+
* nobody. Everything else is left to the Slack path; posts that reach an
|
|
3642
|
+
* agent on another machine are threaded there.
|
|
3643
|
+
*
|
|
3644
|
+
* Until now a local colleague heard such a post only through the Slack
|
|
3645
|
+
* round-trip (its bot copy came back in and was dispatched from a
|
|
3646
|
+
* transient `slack-echo-…` turn), so every one of them was a Slack post —
|
|
3647
|
+
* and a colleague whose bot is not in the channel could not answer: its
|
|
3648
|
+
* reply named that transient thread and was refused.
|
|
3649
|
+
*
|
|
3650
|
+
* @param mapping - The room
|
|
3651
|
+
* @param dto - The agent's post
|
|
3652
|
+
* @returns Handled here (with the reason it is not posted), or the addressing for the Slack path
|
|
3653
|
+
*/
|
|
3654
|
+
async coordinate(mapping, dto) {
|
|
3655
|
+
const unchanged = { handled: false, addressing: 'other' };
|
|
3656
|
+
if (!this.deps.isLocalAgent)
|
|
3657
|
+
return unchanged;
|
|
3658
|
+
try {
|
|
3659
|
+
const root = dto.threadId ? this.deps.chat.getMessageForBridge(dto.threadId) : null;
|
|
3660
|
+
const internalThread = typeof root?.metadata?.[AGENT_COORDINATION_CONSTANTS.INTERNAL_METADATA_KEY] === 'string';
|
|
3661
|
+
// A thread a person started or spoke in is theirs to read: agents
|
|
3662
|
+
// answering there stay visible.
|
|
3663
|
+
if (dto.threadId && !internalThread && this.personInThread(dto.channelId, dto.threadId, root))
|
|
3664
|
+
return unchanged;
|
|
3665
|
+
// Nothing @'d and no internal thread to continue: nothing to decide.
|
|
3666
|
+
if (!internalThread && !(dto.content ?? '').includes('@'))
|
|
3667
|
+
return unchanged;
|
|
3668
|
+
const addressing = await this.agentAddressing(dto);
|
|
3669
|
+
let kind = addressing.kind;
|
|
3670
|
+
let recipients = addressing.local;
|
|
3671
|
+
const addressesNobody = addressing.local.length === 0 && addressing.remote.length === 0 && addressing.others.length === 0 && !addressing.addressesPeople;
|
|
3672
|
+
if (internalThread && root && addressesNobody) {
|
|
3673
|
+
recipients = this.internalThreadAgents(root, dto.senderId);
|
|
3674
|
+
kind = recipients.length > 0 ? 'local-only' : 'other';
|
|
3675
|
+
}
|
|
3676
|
+
if (kind !== 'local-only')
|
|
3677
|
+
return { handled: false, addressing: kind };
|
|
3678
|
+
if (!(await this.roomHears(mapping, recipients)))
|
|
3679
|
+
return unchanged;
|
|
3680
|
+
// "Working on it" from a colleague to a colleague: nobody needs it.
|
|
3681
|
+
if (isInterim(dto))
|
|
3682
|
+
return { handled: true, reason: 'agent-to-agent-local-interim' };
|
|
3683
|
+
const now = (this.deps.now?.() ?? new Date()).getTime();
|
|
3684
|
+
if (this.coordinationDedupe.isRepeat(mapping.chatChannelId, dto.senderId, recipients, dto.content, now)) {
|
|
3685
|
+
this.markInternal(dto, 'agent-to-agent-duplicate');
|
|
3686
|
+
this.logger.info('Agent-to-agent message dropped — the same agent sent these colleagues the same message recently', {
|
|
3687
|
+
slackChannel: mapping.slackChannelName,
|
|
3688
|
+
sender: dto.senderId,
|
|
3689
|
+
recipients,
|
|
3690
|
+
messageId: dto.id,
|
|
3691
|
+
});
|
|
3692
|
+
return { handled: true, reason: 'agent-to-agent-duplicate' };
|
|
3693
|
+
}
|
|
3694
|
+
const delivered = await this.deliverInternally(mapping, dto, recipients, 'agent-to-agent-local');
|
|
3695
|
+
// No dispatcher (start-up): Slack is the only way left — never lost.
|
|
3696
|
+
if (!delivered)
|
|
3697
|
+
return unchanged;
|
|
3698
|
+
return { handled: true, reason: 'agent-to-agent-local' };
|
|
3699
|
+
}
|
|
3700
|
+
catch (err) {
|
|
3701
|
+
this.logger.warn('Agent-to-agent routing failed — posting to Slack as before', {
|
|
3702
|
+
messageId: dto.id,
|
|
3703
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3704
|
+
});
|
|
3705
|
+
return unchanged;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
/**
|
|
3709
|
+
* Whom an agent post @'s: agents on this machine, agents on other machines
|
|
3710
|
+
* (Cloud's room roster and the directory), and anything else — a person,
|
|
3711
|
+
* a broadcast, a name nobody knows (each of which keeps the post in Slack).
|
|
3712
|
+
*
|
|
3713
|
+
* @param dto - The agent's post
|
|
3714
|
+
* @returns The resolved names and their class
|
|
3715
|
+
*/
|
|
3716
|
+
async agentAddressing(dto) {
|
|
3717
|
+
const text = dto.content ?? '';
|
|
3718
|
+
const teams = await this.deps.storage.getTeams();
|
|
3719
|
+
if (this.deps.identities)
|
|
3720
|
+
await this.deps.identities.load();
|
|
3721
|
+
// The room's own members first: with two local agents of one name
|
|
3722
|
+
// ("Ella" of two teams), the one in this room is meant.
|
|
3723
|
+
const roster = new Set((this.deps.chat.getChannelForBridge(dto.channelId)?.members ?? []).map((m) => m.sessionName));
|
|
3724
|
+
const candidates = teams
|
|
3725
|
+
.flatMap((t) => teamChannelMembers(t))
|
|
3726
|
+
.sort((a, b) => Number(roster.has(b.sessionName)) - Number(roster.has(a.sessionName)))
|
|
3727
|
+
.map((m) => ({ name: m.name, sessionName: m.sessionName, botUserId: this.deps.identities?.get(m.sessionName)?.botUserId }));
|
|
3728
|
+
const resolved = resolveSlackMentions(text, candidates);
|
|
3729
|
+
const isLocal = (s) => this.deps.isLocalAgent?.(s) ?? false;
|
|
3730
|
+
const local = resolved.mentions.filter((s) => s !== dto.senderId && isLocal(s));
|
|
3731
|
+
const others = resolved.mentions.filter((s) => s !== dto.senderId && !isLocal(s));
|
|
3732
|
+
const remote = [];
|
|
3733
|
+
const localBots = new Set(candidates.map((c) => c.botUserId).filter((id) => !!id));
|
|
3734
|
+
const nativeOthers = extractNativeMentionIds(text).filter((id) => !localBots.has(id));
|
|
3735
|
+
if (resolved.unknown.length > 0 || nativeOthers.length > 0) {
|
|
3736
|
+
const elsewhere = await this.agentsElsewhere(dto.channelId);
|
|
3737
|
+
for (const u of resolved.unknown) {
|
|
3738
|
+
const key = u.token.toLowerCase();
|
|
3739
|
+
const hit = this.humanNames.has(key) ? undefined : elsewhere.byName.get(key);
|
|
3740
|
+
if (hit)
|
|
3741
|
+
remote.push(hit);
|
|
3742
|
+
else
|
|
3743
|
+
others.push(u.token);
|
|
3744
|
+
}
|
|
3745
|
+
for (const id of nativeOthers) {
|
|
3746
|
+
const hit = elsewhere.byBot.get(id);
|
|
3747
|
+
if (hit)
|
|
3748
|
+
remote.push(hit);
|
|
3749
|
+
else
|
|
3750
|
+
others.push(id);
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
// "Steve, …" with no @ is still addressed to Steve.
|
|
3754
|
+
const ownerName = this.ownerName ?? (await this.ownerDisplayName());
|
|
3755
|
+
const people = [ownerName, ownerName.split(/\s+/u)[0], ...this.humanNames.keys()].filter((n) => n && n !== 'Owner').map((name) => ({ name }));
|
|
3756
|
+
const addressesPeople = SLACK_BROADCAST_MENTION_RE.test(text) || (people.length > 0 && !!leadingNameMention(text, people));
|
|
3757
|
+
return {
|
|
3758
|
+
kind: classifyAgentAddressing({ localAgents: local, remoteAgents: remote, others, addressesPeople }),
|
|
3759
|
+
local: [...new Set(local)],
|
|
3760
|
+
remote: [...new Set(remote)],
|
|
3761
|
+
others,
|
|
3762
|
+
addressesPeople,
|
|
3763
|
+
};
|
|
3764
|
+
}
|
|
3765
|
+
/**
|
|
3766
|
+
* Agents of this account on other machines that may be in a room, by
|
|
3767
|
+
* (lower-case) name and bot id: Cloud's last room roster for the channel,
|
|
3768
|
+
* then the directory (cached). Never throws.
|
|
3769
|
+
*
|
|
3770
|
+
* @param chatChannelId - The room's chat channel
|
|
3771
|
+
* @returns Name → session, bot id → session
|
|
3772
|
+
*/
|
|
3773
|
+
async agentsElsewhere(chatChannelId) {
|
|
3774
|
+
const byName = new Map();
|
|
3775
|
+
const byBot = new Map();
|
|
3776
|
+
const isLocal = (s) => this.deps.isLocalAgent?.(localAgentSession(s)) ?? false;
|
|
3777
|
+
const addName = (name, session) => {
|
|
3778
|
+
const full = (name ?? '').trim().toLowerCase();
|
|
3779
|
+
if (!full)
|
|
3780
|
+
return;
|
|
3781
|
+
if (!byName.has(full))
|
|
3782
|
+
byName.set(full, session);
|
|
3783
|
+
const bare = full.replace(/\s*\([^)]*\)\s*$/u, '').trim();
|
|
3784
|
+
if (bare && !byName.has(bare))
|
|
3785
|
+
byName.set(bare, session);
|
|
3786
|
+
};
|
|
3787
|
+
const mapping = this.findByChatChannelId(chatChannelId);
|
|
3788
|
+
for (const m of (mapping ? this.lastRooms.get(mapping.slackChannelId)?.members : undefined) ?? []) {
|
|
3789
|
+
if (!isLocal(m.agentSession))
|
|
3790
|
+
addName(m.displayName, m.agentSession);
|
|
3791
|
+
}
|
|
3792
|
+
try {
|
|
3793
|
+
const directory = getSlackDirectoryService();
|
|
3794
|
+
const entries = directory
|
|
3795
|
+
? await withinMs(directory.list(mapping?.slackChannelId), AGENT_COORDINATION_CONSTANTS.DIRECTORY_LOOKUP_MS, [])
|
|
3796
|
+
: [];
|
|
3797
|
+
for (const e of entries) {
|
|
3798
|
+
if (e.kind !== 'agent' || e.source === 'this-machine' || !e.agentSession || isLocal(e.agentSession))
|
|
3799
|
+
continue;
|
|
3800
|
+
addName(e.name, e.agentSession);
|
|
3801
|
+
if (e.botUserId)
|
|
3802
|
+
byBot.set(e.botUserId, e.agentSession);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
catch {
|
|
3806
|
+
/* the room roster alone */
|
|
3807
|
+
}
|
|
3808
|
+
return { byName, byBot };
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Whether a person started a chat thread or spoke in it.
|
|
3812
|
+
*
|
|
3813
|
+
* @param chatChannelId - The room's chat channel
|
|
3814
|
+
* @param threadId - Thread root id
|
|
3815
|
+
* @param root - The root row, when known
|
|
3816
|
+
* @returns True when a person is part of the thread
|
|
3817
|
+
*/
|
|
3818
|
+
personInThread(chatChannelId, threadId, root) {
|
|
3819
|
+
const isPerson = (m) => m.senderType === 'user' && !isAgentAuthoredRow(m);
|
|
3820
|
+
if (!root)
|
|
3821
|
+
return true;
|
|
3822
|
+
if (isPerson(root))
|
|
3823
|
+
return true;
|
|
3824
|
+
const rows = this.deps.chat.listThreadForBridge?.(chatChannelId, threadId) ?? [];
|
|
3825
|
+
return rows.some(isPerson);
|
|
3826
|
+
}
|
|
3827
|
+
/**
|
|
3828
|
+
* The agents of an internal coordination thread other than the sender:
|
|
3829
|
+
* whoever wrote in it and whoever it @'d.
|
|
3830
|
+
*
|
|
3831
|
+
* @param root - The thread root
|
|
3832
|
+
* @param sender - The agent posting now
|
|
3833
|
+
* @returns Local agent sessions
|
|
3834
|
+
*/
|
|
3835
|
+
internalThreadAgents(root, sender) {
|
|
3836
|
+
const rows = [root, ...(this.deps.chat.listThreadForBridge?.(root.channelId, root.id) ?? []).filter((m) => m.id !== root.id)];
|
|
3837
|
+
const out = new Set();
|
|
3838
|
+
for (const m of rows) {
|
|
3839
|
+
const author = m.senderType === 'agent' ? m.senderId : agentAuthorSession(m);
|
|
3840
|
+
if (author)
|
|
3841
|
+
out.add(author);
|
|
3842
|
+
for (const s of m.mentions ?? [])
|
|
3843
|
+
out.add(s);
|
|
3844
|
+
}
|
|
3845
|
+
out.delete(sender);
|
|
3846
|
+
return [...out].filter((s) => this.deps.isLocalAgent?.(s) ?? false);
|
|
3847
|
+
}
|
|
3848
|
+
/**
|
|
3849
|
+
* Make sure every recipient is on the room's roster, so the dispatcher
|
|
3850
|
+
* delivers to it: an ad-hoc room grows by an explicit @ (as it does for a
|
|
3851
|
+
* Slack message); a team channel does not, so a colleague of another team
|
|
3852
|
+
* keeps today's path (its own bot hears the Slack post).
|
|
3853
|
+
*
|
|
3854
|
+
* @param mapping - The room
|
|
3855
|
+
* @param recipients - Agents to deliver to
|
|
3856
|
+
* @returns True when all of them are on the roster
|
|
3857
|
+
*/
|
|
3858
|
+
async roomHears(mapping, recipients) {
|
|
3859
|
+
if (recipients.length === 0)
|
|
3860
|
+
return false;
|
|
3861
|
+
const room = this.deps.chat.getChannelForBridge(mapping.chatChannelId);
|
|
3862
|
+
if (!room || room.archivedAt)
|
|
3863
|
+
return false;
|
|
3864
|
+
const roster = new Set((room.members ?? []).map((m) => m.sessionName));
|
|
3865
|
+
const missing = recipients.filter((s) => !roster.has(s));
|
|
3866
|
+
if (missing.length === 0)
|
|
3867
|
+
return true;
|
|
3868
|
+
if (!isAdhocMapping(mapping))
|
|
3869
|
+
return false;
|
|
3870
|
+
const next = [...new Set([...(mapping.members ?? []), ...roster, ...missing])];
|
|
3871
|
+
mapping.members = next;
|
|
3872
|
+
if (mapping.excluded) {
|
|
3873
|
+
mapping.excluded = mapping.excluded.filter((s) => !next.includes(s));
|
|
3874
|
+
if (mapping.excluded.length === 0)
|
|
3875
|
+
delete mapping.excluded;
|
|
3876
|
+
}
|
|
3877
|
+
this.deps.chat.setHuddleMembers(mapping.chatChannelId, next);
|
|
3878
|
+
await this.save();
|
|
3879
|
+
this.deps.onRoomsChanged?.();
|
|
3880
|
+
return true;
|
|
3881
|
+
}
|
|
3882
|
+
/**
|
|
3883
|
+
* Remember on the chat row that it stayed inside Crewly, so replies in its
|
|
3884
|
+
* thread stay inside too (and a later reader knows why Slack never had it).
|
|
3885
|
+
*
|
|
3886
|
+
* @param dto - The agent's post
|
|
3887
|
+
* @param reason - Why
|
|
3888
|
+
*/
|
|
3889
|
+
markInternal(dto, reason) {
|
|
3890
|
+
const patch = { [AGENT_COORDINATION_CONSTANTS.INTERNAL_METADATA_KEY]: reason };
|
|
3891
|
+
try {
|
|
3892
|
+
this.deps.chat.updateMessageMetadata?.(dto.id, patch);
|
|
3893
|
+
}
|
|
3894
|
+
catch {
|
|
3895
|
+
/* routing hint only */
|
|
3896
|
+
}
|
|
3897
|
+
dto.metadata = { ...(dto.metadata ?? {}), ...patch };
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* Deliver an agent's post to colleagues on this machine without Slack:
|
|
3901
|
+
* the dispatcher gets it as a colleague's turn — exactly what the Slack
|
|
3902
|
+
* round-trip produced — but threaded on the real chat row, so their
|
|
3903
|
+
* replies find it (the transient `slack-echo-…` id of the round-trip did
|
|
3904
|
+
* not exist, and those replies were refused).
|
|
3905
|
+
*
|
|
3906
|
+
* @param mapping - The room
|
|
3907
|
+
* @param dto - The agent's post
|
|
3908
|
+
* @param recipients - Local agents it is for
|
|
3909
|
+
* @param reason - Logged and stored on the row
|
|
3910
|
+
* @param note - A line put before the text (a hand-back)
|
|
3911
|
+
* @returns False when there is no dispatcher to deliver with
|
|
3912
|
+
*/
|
|
3913
|
+
async deliverInternally(mapping, dto, recipients, reason, note) {
|
|
3914
|
+
const dispatcher = this.deps.getDispatcher();
|
|
3915
|
+
const channel = this.deps.chat.getChannelForBridge(mapping.chatChannelId);
|
|
3916
|
+
if (!dispatcher || !channel)
|
|
3917
|
+
return false;
|
|
3918
|
+
this.markInternal(dto, reason);
|
|
3919
|
+
const name = (await this.findPostingMember(mapping, dto.senderId))?.name ?? dto.senderId;
|
|
3920
|
+
const turn = {
|
|
3921
|
+
...dto,
|
|
3922
|
+
senderType: 'user',
|
|
3923
|
+
senderId: `${name} (agent)`,
|
|
3924
|
+
content: note ? `${note}\n\n${dto.content}` : dto.content,
|
|
3925
|
+
mentions: [...recipients],
|
|
3926
|
+
metadata: {
|
|
3927
|
+
...(dto.metadata ?? {}),
|
|
3928
|
+
[OWNER_EVIDENCE_METADATA.REMOTE_AGENT_SESSION]: dto.senderId,
|
|
3929
|
+
[AGENT_COORDINATION_CONSTANTS.INTERNAL_METADATA_KEY]: reason,
|
|
3930
|
+
},
|
|
3931
|
+
};
|
|
3932
|
+
const now = (this.deps.now?.() ?? new Date()).getTime();
|
|
3933
|
+
for (const r of recipients)
|
|
3934
|
+
this.noteAskedBy(mapping.chatChannelId, r, dto.senderId, now);
|
|
3935
|
+
const result = await dispatcher.dispatchMessage(channel, turn, {
|
|
3936
|
+
threadId: dto.threadId ?? dto.id,
|
|
3937
|
+
replyVia: 'reply-channel',
|
|
3938
|
+
excludeSessions: [dto.senderId],
|
|
3939
|
+
});
|
|
3940
|
+
const log = result.dispatched ? this.logger.info.bind(this.logger) : this.logger.warn.bind(this.logger);
|
|
3941
|
+
log('Agent-to-agent message delivered inside Crewly — not posted to Slack', {
|
|
3942
|
+
reason,
|
|
3943
|
+
slackChannel: mapping.slackChannelName,
|
|
3944
|
+
sender: dto.senderId,
|
|
3945
|
+
recipients,
|
|
3946
|
+
dispatched: result.dispatched,
|
|
3947
|
+
messageId: dto.id,
|
|
3948
|
+
});
|
|
3949
|
+
return true;
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Remember who asked a helper something in a room.
|
|
3953
|
+
*
|
|
3954
|
+
* @param chatChannelId - The room
|
|
3955
|
+
* @param helper - The agent asked
|
|
3956
|
+
* @param asker - The agent that asked
|
|
3957
|
+
* @param now - Time (ms)
|
|
3958
|
+
*/
|
|
3959
|
+
noteAskedBy(chatChannelId, helper, asker, now) {
|
|
3960
|
+
if (helper === asker)
|
|
3961
|
+
return;
|
|
3962
|
+
const key = `${chatChannelId}:${helper}`;
|
|
3963
|
+
this.askedBy.delete(key);
|
|
3964
|
+
this.askedBy.set(key, { asker, at: now });
|
|
3965
|
+
if (this.askedBy.size > AGENT_COORDINATION_CONSTANTS.ASKED_BY_MAX) {
|
|
3966
|
+
const oldest = this.askedBy.keys().next().value;
|
|
3967
|
+
if (oldest !== undefined)
|
|
3968
|
+
this.askedBy.delete(oldest);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
/**
|
|
3972
|
+
* A helper's reply its bot could not post in the room (not a member of the
|
|
3973
|
+
* Slack channel): handed to the agent that asked it — the thread root's
|
|
3974
|
+
* author, else the agent that last @'d it here, else the room's lead —
|
|
3975
|
+
* who is in the room and posts the update. Logged either way.
|
|
3976
|
+
*
|
|
3977
|
+
* @param mapping - The room
|
|
3978
|
+
* @param dto - The helper's reply
|
|
3979
|
+
* @param code - Slack's error code
|
|
3980
|
+
* @returns True when someone here got it
|
|
3981
|
+
*/
|
|
3982
|
+
async handBackUnpostable(mapping, dto, code) {
|
|
3983
|
+
try {
|
|
3984
|
+
const isLocal = (s) => !!s && s !== dto.senderId && (this.deps.isLocalAgent?.(s) ?? false);
|
|
3985
|
+
const root = dto.threadId ? this.deps.chat.getMessageForBridge(dto.threadId) : null;
|
|
3986
|
+
const rootAuthor = root ? (root.senderType === 'agent' ? root.senderId : agentAuthorSession(root)) : null;
|
|
3987
|
+
const now = (this.deps.now?.() ?? new Date()).getTime();
|
|
3988
|
+
const asked = this.askedBy.get(`${mapping.chatChannelId}:${dto.senderId}`);
|
|
3989
|
+
const recentAsker = asked && now - asked.at <= AGENT_COORDINATION_CONSTANTS.ASKED_BY_TTL_MS ? asked.asker : null;
|
|
3990
|
+
let requester = null;
|
|
3991
|
+
let source = '';
|
|
3992
|
+
if (isLocal(rootAuthor)) {
|
|
3993
|
+
requester = rootAuthor;
|
|
3994
|
+
source = 'thread-root';
|
|
3995
|
+
}
|
|
3996
|
+
else if (isLocal(recentAsker)) {
|
|
3997
|
+
requester = recentAsker;
|
|
3998
|
+
source = 'last-asker';
|
|
3999
|
+
}
|
|
4000
|
+
else {
|
|
4001
|
+
const lead = await this.localRoomLead(mapping, dto.threadId);
|
|
4002
|
+
if (isLocal(lead)) {
|
|
4003
|
+
requester = lead;
|
|
4004
|
+
source = 'room-lead';
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
if (!requester) {
|
|
4008
|
+
this.logger.warn('Helper cannot post in this Slack room and nobody here asked it — reply not handed on', {
|
|
4009
|
+
slackChannel: mapping.slackChannelName,
|
|
4010
|
+
sender: dto.senderId,
|
|
4011
|
+
code,
|
|
4012
|
+
});
|
|
4013
|
+
return false;
|
|
4014
|
+
}
|
|
4015
|
+
if (source === 'room-lead') {
|
|
4016
|
+
this.logger.warn('Helper cannot post in this Slack room and who asked it is unknown — handing its reply to the room lead', {
|
|
4017
|
+
slackChannel: mapping.slackChannelName,
|
|
4018
|
+
sender: dto.senderId,
|
|
4019
|
+
lead: requester,
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
if (!(await this.roomHears(mapping, [requester])))
|
|
4023
|
+
return false;
|
|
4024
|
+
const name = (await this.findPostingMember(mapping, dto.senderId))?.name ?? dto.senderId;
|
|
4025
|
+
const note = `[Sent to you only: ${name} cannot post in #${mapping.slackChannelName}. You are the voice of this room — put what matters into your own update there.]`;
|
|
4026
|
+
const ok = await this.deliverInternally(mapping, dto, [requester], 'helper-not-in-slack-channel', note);
|
|
4027
|
+
if (ok) {
|
|
4028
|
+
this.logger.info('Helper reply handed to the agent that asked it (its bot is not in the Slack channel)', {
|
|
4029
|
+
slackChannel: mapping.slackChannelName,
|
|
4030
|
+
sender: dto.senderId,
|
|
4031
|
+
requester,
|
|
4032
|
+
source,
|
|
4033
|
+
code,
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
return ok;
|
|
4037
|
+
}
|
|
4038
|
+
catch (err) {
|
|
4039
|
+
this.logger.warn('Could not hand a helper reply back to the agent that asked it', {
|
|
4040
|
+
sender: dto.senderId,
|
|
4041
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4042
|
+
});
|
|
4043
|
+
return false;
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
/**
|
|
4047
|
+
* The Slack ts of the room's cross-machine coordination thread today
|
|
4048
|
+
* (UTC), if one was started.
|
|
4049
|
+
*
|
|
4050
|
+
* @param mapping - The room
|
|
4051
|
+
* @returns Thread ts, or undefined
|
|
4052
|
+
*/
|
|
4053
|
+
coordinationThreadTs(mapping) {
|
|
4054
|
+
return this.coordinationThreads.get(this.coordinationKey(mapping));
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Make a top-level cross-machine post the room's coordination thread for
|
|
4058
|
+
* today: later ones go under it.
|
|
4059
|
+
*
|
|
4060
|
+
* @param mapping - The room
|
|
4061
|
+
* @param dto - The post (its Slack ts was recorded by recordSlackRoot)
|
|
4062
|
+
*/
|
|
4063
|
+
rememberCoordinationThread(mapping, dto) {
|
|
4064
|
+
const ts = dto.metadata?.slackThreadTs;
|
|
4065
|
+
if (typeof ts !== 'string' || !ts)
|
|
4066
|
+
return;
|
|
4067
|
+
this.coordinationThreads.set(this.coordinationKey(mapping), ts);
|
|
4068
|
+
if (this.coordinationThreads.size > AGENT_COORDINATION_CONSTANTS.COORDINATION_THREADS_MAX) {
|
|
4069
|
+
const oldest = this.coordinationThreads.keys().next().value;
|
|
4070
|
+
if (oldest !== undefined)
|
|
4071
|
+
this.coordinationThreads.delete(oldest);
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
/** `slackChannelId:YYYY-MM-DD` (UTC). */
|
|
4075
|
+
coordinationKey(mapping) {
|
|
4076
|
+
return `${mapping.slackChannelId}:${(this.deps.now?.() ?? new Date()).toISOString().slice(0, 10)}`;
|
|
4077
|
+
}
|
|
4078
|
+
/**
|
|
4079
|
+
* An agent's text without internal ticket ids (tickets are the harness's
|
|
4080
|
+
* bookkeeping; the owner never sees their ids). Never throws.
|
|
4081
|
+
*
|
|
4082
|
+
* @param text - The agent's text
|
|
4083
|
+
* @returns The text for Slack
|
|
4084
|
+
*/
|
|
4085
|
+
async withoutTicketIds(text) {
|
|
4086
|
+
try {
|
|
4087
|
+
const now = Date.now();
|
|
4088
|
+
if (!this.ticketPrefixCache || now - this.ticketPrefixCache.at > AGENT_COORDINATION_CONSTANTS.TICKET_PREFIX_CACHE_MS) {
|
|
4089
|
+
const prefixes = this.deps.ticketPrefixes ? await this.deps.ticketPrefixes() : [];
|
|
4090
|
+
this.ticketPrefixCache = { at: now, prefixes: [AGENT_COORDINATION_CONSTANTS.REQUEST_TICKET_PREFIX, ...prefixes] };
|
|
4091
|
+
}
|
|
4092
|
+
return stripTicketIds(text, this.ticketPrefixCache.prefixes);
|
|
4093
|
+
}
|
|
4094
|
+
catch {
|
|
4095
|
+
return text;
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
3523
4098
|
/**
|
|
3524
4099
|
* Store where a mirrored reply landed on its chat message, in the shape
|
|
3525
4100
|
* inbound messages use, so the Slack side can be audited from our store.
|
|
@@ -3661,7 +4236,7 @@ export class SlackTeamChannelService {
|
|
|
3661
4236
|
const member = await this.findPostingMember(mapping, dto.senderId);
|
|
3662
4237
|
const installed = this.deps.identities?.getInstalled(dto.senderId) ?? null;
|
|
3663
4238
|
const identity = installed ? { botToken: installed.botToken } : slackIdentityFor(member, dto.senderId);
|
|
3664
|
-
const text = await this.linkAgentMentions(toSlackMrkdwn(dto.content), mapping.slackChannelId);
|
|
4239
|
+
const text = await this.linkAgentMentions(toSlackMrkdwn(await this.withoutTicketIds(dto.content)), mapping.slackChannelId);
|
|
3665
4240
|
const ts = await this.deps.slack.sendMessage({
|
|
3666
4241
|
channelId: mapping.slackChannelId,
|
|
3667
4242
|
text,
|
|
@@ -4798,6 +5373,41 @@ export function isOwnerTypedInCrewly(dto) {
|
|
|
4798
5373
|
return false;
|
|
4799
5374
|
return typeof md.slackTs !== 'string' && typeof md.slackChannelId !== 'string';
|
|
4800
5375
|
}
|
|
5376
|
+
/**
|
|
5377
|
+
* Whether a chat row was written by an agent (as a colleague's user turn).
|
|
5378
|
+
*
|
|
5379
|
+
* @param dto - A chat row
|
|
5380
|
+
* @returns True for an agent-authored row
|
|
5381
|
+
*/
|
|
5382
|
+
function isAgentAuthoredRow(dto) {
|
|
5383
|
+
return agentAuthorSession(dto) !== null;
|
|
5384
|
+
}
|
|
5385
|
+
/**
|
|
5386
|
+
* The agent that wrote a user-turn row (a colleague on this or another
|
|
5387
|
+
* machine), or null for a person's row.
|
|
5388
|
+
*
|
|
5389
|
+
* @param dto - A chat row
|
|
5390
|
+
* @returns Agent session, or null
|
|
5391
|
+
*/
|
|
5392
|
+
function agentAuthorSession(dto) {
|
|
5393
|
+
const md = dto.metadata ?? {};
|
|
5394
|
+
const author = md[OWNER_EVIDENCE_METADATA.AUTHOR_AGENT_SESSION] ?? md[OWNER_EVIDENCE_METADATA.REMOTE_AGENT_SESSION];
|
|
5395
|
+
return typeof author === 'string' && author ? author : null;
|
|
5396
|
+
}
|
|
5397
|
+
/**
|
|
5398
|
+
* Whether Slack refused a post because the posting bot is not in the
|
|
5399
|
+
* channel (`not_in_channel`, or `channel_not_found` for a private channel
|
|
5400
|
+
* it cannot see).
|
|
5401
|
+
*
|
|
5402
|
+
* @param err - The thrown error
|
|
5403
|
+
* @returns True for "this bot cannot post here"
|
|
5404
|
+
*/
|
|
5405
|
+
function isNotInChannelError(err) {
|
|
5406
|
+
const code = describeSlackError(err).code;
|
|
5407
|
+
if (code === 'not_in_channel' || code === 'channel_not_found')
|
|
5408
|
+
return true;
|
|
5409
|
+
return /\b(?:not_in_channel|channel_not_found)\b/.test(err instanceof Error ? err.message : String(err));
|
|
5410
|
+
}
|
|
4801
5411
|
/** Type guard for persisted mapping rows. */
|
|
4802
5412
|
/**
|
|
4803
5413
|
* A promise's value, or `fallback` when it takes longer than `ms`.
|