crewly 1.20.201 → 1.20.203
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/dist/backend/backend/src/constants.d.ts +98 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +98 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +29 -7
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +28 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/session/session.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/session/session.controller.js +7 -0
- package/dist/backend/backend/src/controllers/session/session.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +4 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts +28 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +132 -34
- package/dist/backend/backend/src/index.js.map +1 -1
- 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 +7 -0
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +11 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.d.ts +22 -0
- 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 +39 -3
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/in-process-turn-failure.service.d.ts +140 -0
- package/dist/backend/backend/src/services/agent/in-process-turn-failure.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/in-process-turn-failure.service.js +267 -0
- package/dist/backend/backend/src/services/agent/in-process-turn-failure.service.js.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts +17 -0
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.service.js +27 -2
- package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts +2 -0
- package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.js +1 -0
- package/dist/backend/backend/src/services/decisions/decision.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/drain-queued-delivery.d.ts +59 -0
- package/dist/backend/backend/src/services/messaging/drain-queued-delivery.d.ts.map +1 -0
- package/dist/backend/backend/src/services/messaging/drain-queued-delivery.js +70 -0
- package/dist/backend/backend/src/services/messaging/drain-queued-delivery.js.map +1 -0
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts +46 -1
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js +109 -0
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.d.ts +2 -0
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.js +6 -0
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +38 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +63 -8
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/liveness-monitor.service.d.ts +142 -0
- package/dist/backend/backend/src/services/monitoring/liveness-monitor.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/monitoring/liveness-monitor.service.js +231 -0
- package/dist/backend/backend/src/services/monitoring/liveness-monitor.service.js.map +1 -0
- package/dist/backend/backend/src/services/open-items/open-items.service.d.ts +21 -0
- package/dist/backend/backend/src/services/open-items/open-items.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/open-items/open-items.service.js +72 -3
- package/dist/backend/backend/src/services/open-items/open-items.service.js.map +1 -1
- package/dist/backend/backend/src/services/orc/orc-chat-owner-mirror.d.ts +106 -0
- package/dist/backend/backend/src/services/orc/orc-chat-owner-mirror.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orc/orc-chat-owner-mirror.js +199 -0
- package/dist/backend/backend/src/services/orc/orc-chat-owner-mirror.js.map +1 -0
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +9 -3
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js +21 -6
- package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.d.ts +11 -2
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js +22 -1
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.js +30 -2
- package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts +8 -2
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js +7 -4
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +41 -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 +394 -231
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts +21 -5
- 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 +21 -8
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +1 -0
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js +8 -3
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +98 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +98 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- 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 +7 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +11 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.d.ts +22 -0
- 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 +39 -3
- package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/in-process-turn-failure.service.d.ts +140 -0
- package/dist/cli/backend/src/services/agent/in-process-turn-failure.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/in-process-turn-failure.service.js +267 -0
- package/dist/cli/backend/src/services/agent/in-process-turn-failure.service.js.map +1 -0
- package/dist/cli/backend/src/services/decisions/decision.service.d.ts +17 -0
- package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision.service.js +27 -2
- package/dist/cli/backend/src/services/decisions/decision.service.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts +46 -1
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js +109 -0
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +38 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +63 -8
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/cli/backend/src/services/open-items/open-items.service.d.ts +21 -0
- package/dist/cli/backend/src/services/open-items/open-items.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/open-items/open-items.service.js +72 -3
- package/dist/cli/backend/src/services/open-items/open-items.service.js.map +1 -1
- package/dist/cli/backend/src/services/runtime-fallback/reset-time.d.ts +66 -0
- package/dist/cli/backend/src/services/runtime-fallback/reset-time.d.ts.map +1 -0
- package/dist/cli/backend/src/services/runtime-fallback/reset-time.js +211 -0
- package/dist/cli/backend/src/services/runtime-fallback/reset-time.js.map +1 -0
- package/dist/cli/backend/src/services/runtime-fallback/usage-limit-rules.d.ts +113 -0
- package/dist/cli/backend/src/services/runtime-fallback/usage-limit-rules.d.ts.map +1 -0
- package/dist/cli/backend/src/services/runtime-fallback/usage-limit-rules.js +257 -0
- package/dist/cli/backend/src/services/runtime-fallback/usage-limit-rules.js.map +1 -0
- package/dist/cli/backend/src/services/session/pty/pty-session.d.ts +9 -3
- package/dist/cli/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/pty/pty-session.js +21 -6
- package/dist/cli/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.d.ts +11 -2
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js +22 -1
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +41 -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 +394 -231
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts +21 -5
- 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 +21 -8
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +1 -0
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js +8 -3
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -1282,255 +1282,287 @@ export class SlackTeamChannelService {
|
|
|
1282
1282
|
if (oldest !== undefined)
|
|
1283
1283
|
this.seenInbound.delete(oldest);
|
|
1284
1284
|
}
|
|
1285
|
-
//
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1285
|
+
// Route guard (crewly#1015 §7): from here on an owner's message is
|
|
1286
|
+
// recorded but not yet delivered. "旧模板是什么" (10-02, #C0C46TTBNNP)
|
|
1287
|
+
// was recorded and then nothing — no "routed", no "reached nobody", no
|
|
1288
|
+
// watchdog entry: one of the awaits below never settled, and every
|
|
1289
|
+
// safeguard sits after them. Each await before dispatch is bounded
|
|
1290
|
+
// (ROUTE_STEP_TIMEOUT_MS); a routing that throws, or has not reached
|
|
1291
|
+
// dispatch within ROUTE_STALL_MS, is rescued by the unanswered-message
|
|
1292
|
+
// fallback. Dispatch itself is never timed (sequential cold starts can
|
|
1293
|
+
// take minutes), and a routing that reaches dispatch after a rescue ran
|
|
1294
|
+
// stops there — no double delivery.
|
|
1295
|
+
const routeGuard = !remoteAgent && isOwnerAuthored(message, this.deps.getOwnerUserId?.())
|
|
1296
|
+
? this.armRouteGuard(message, mapping, persisted, threadId ?? persisted.id)
|
|
1297
|
+
: null;
|
|
1298
|
+
try {
|
|
1299
|
+
// Ad-hoc channels grow their huddle as new agents get @'d there.
|
|
1300
|
+
if (isAdhocMapping(mapping) && resolved.mentions.length > 0) {
|
|
1301
|
+
const next = [...new Set([...(mapping.members ?? []), ...resolved.mentions])];
|
|
1302
|
+
if (next.length !== (mapping.members ?? []).length) {
|
|
1303
|
+
mapping.members = next;
|
|
1304
|
+
this.deps.chat.setHuddleMembers(mapping.chatChannelId, next);
|
|
1305
|
+
await this.save();
|
|
1306
|
+
this.deps.onRoomsChanged?.();
|
|
1307
|
+
}
|
|
1293
1308
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
return { mapping, message: persisted, mentions: [], dispatch: null };
|
|
1312
|
-
}
|
|
1313
|
-
if (addressedElsewhereOnly) {
|
|
1314
|
-
// Kept for context (the next question in the thread may be ours), but
|
|
1315
|
-
// no eyes, no placeholder, and nobody here is told.
|
|
1316
|
-
this.logger.info('Slack team message addressed to an agent on another machine — recorded, not dispatched', {
|
|
1317
|
-
teamId: mapping.teamId,
|
|
1318
|
-
slackChannel: `#${mapping.slackChannelName}`,
|
|
1319
|
-
mentionedElsewhere,
|
|
1320
|
-
});
|
|
1321
|
-
return { mapping, message: persisted, mentions: [], dispatch: null };
|
|
1322
|
-
}
|
|
1323
|
-
if (addressedPeopleOnly) {
|
|
1324
|
-
// Context for the agents' next turn in this thread, nothing more. No
|
|
1325
|
-
// suggestion hint either: the names were people, not typos.
|
|
1326
|
-
if (inherited) {
|
|
1327
|
-
this.logger.info('Slack team message continues a person-to-person exchange — recorded, not dispatched', {
|
|
1328
|
-
teamId: mapping.teamId,
|
|
1329
|
-
slackChannel: `#${mapping.slackChannelName}`,
|
|
1330
|
-
addressedTo: inherited.people,
|
|
1331
|
-
reason: inherited.reason,
|
|
1332
|
-
threaded: !!threadId,
|
|
1333
|
-
});
|
|
1309
|
+
// Who will receive this, and who owes a reply — decided by the same rules
|
|
1310
|
+
// delivery uses, but *before* delivery, which can take a minute or two
|
|
1311
|
+
// when an agent has to be cold-started. The owner should not look at an
|
|
1312
|
+
// unacknowledged message for that long.
|
|
1313
|
+
const dispatcherForPlan = this.deps.getDispatcher();
|
|
1314
|
+
const presence = await this.bounded(this.roomStateFor(message, mapping, team ?? null), null, 'room presence', message);
|
|
1315
|
+
const dispatchOptions = {
|
|
1316
|
+
threadId: threadId ?? persisted.id,
|
|
1317
|
+
replyVia: 'reply-channel',
|
|
1318
|
+
// A local agent's own message (fanned out to the colleagues it @'d)
|
|
1319
|
+
// must not come back to its author.
|
|
1320
|
+
...(remoteAgent ? { excludeSessions: [remoteAgent] } : {}),
|
|
1321
|
+
...(presence ? { room: presence.state, ...(presence.line ? { roomPresence: presence.line } : {}) } : {}),
|
|
1322
|
+
};
|
|
1323
|
+
if (declinedOnly) {
|
|
1324
|
+
// Every agent it addressed is dedicated to someone else and declined.
|
|
1325
|
+
return { mapping, message: persisted, mentions: [], dispatch: null };
|
|
1334
1326
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1327
|
+
if (addressedElsewhereOnly) {
|
|
1328
|
+
// Kept for context (the next question in the thread may be ours), but
|
|
1329
|
+
// no eyes, no placeholder, and nobody here is told.
|
|
1330
|
+
this.logger.info('Slack team message addressed to an agent on another machine — recorded, not dispatched', {
|
|
1337
1331
|
teamId: mapping.teamId,
|
|
1338
1332
|
slackChannel: `#${mapping.slackChannelName}`,
|
|
1339
|
-
|
|
1340
|
-
mentionedNames: peopleMentions.names,
|
|
1341
|
-
threaded: !!threadId,
|
|
1333
|
+
mentionedElsewhere,
|
|
1342
1334
|
});
|
|
1335
|
+
return { mapping, message: persisted, mentions: [], dispatch: null };
|
|
1343
1336
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1337
|
+
if (addressedPeopleOnly) {
|
|
1338
|
+
// Context for the agents' next turn in this thread, nothing more. No
|
|
1339
|
+
// suggestion hint either: the names were people, not typos.
|
|
1340
|
+
if (inherited) {
|
|
1341
|
+
this.logger.info('Slack team message continues a person-to-person exchange — recorded, not dispatched', {
|
|
1342
|
+
teamId: mapping.teamId,
|
|
1343
|
+
slackChannel: `#${mapping.slackChannelName}`,
|
|
1344
|
+
addressedTo: inherited.people,
|
|
1345
|
+
reason: inherited.reason,
|
|
1346
|
+
threaded: !!threadId,
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
else {
|
|
1350
|
+
this.logger.info('Slack team message addressed to people, not agents — recorded, not dispatched', {
|
|
1351
|
+
teamId: mapping.teamId,
|
|
1352
|
+
slackChannel: `#${mapping.slackChannelName}`,
|
|
1353
|
+
mentionedUsers: peopleMentions.userIds,
|
|
1354
|
+
mentionedNames: peopleMentions.names,
|
|
1355
|
+
threaded: !!threadId,
|
|
1356
|
+
});
|
|
1359
1357
|
}
|
|
1358
|
+
return { mapping, message: persisted, mentions: [], dispatch: null };
|
|
1359
|
+
}
|
|
1360
|
+
const planned = dispatcherForPlan?.planHuddleTargets
|
|
1361
|
+
? await this.bounded(dispatcherForPlan.planHuddleTargets(channel, persisted, dispatchOptions).catch(() => null), null, 'dispatch plan', message)
|
|
1360
1362
|
: null;
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
//
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1385
|
-
//
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
//
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1363
|
+
// Prompt backstop: who the message (or the exchange it may continue)
|
|
1364
|
+
// was meant for, when people are in the picture. A direct request inside
|
|
1365
|
+
// the exchange gets a neutral note, and none at all in an assistant or
|
|
1366
|
+
// support room, where requests are what the team is there for.
|
|
1367
|
+
const peopleAddressing = namedPeople.length > 0
|
|
1368
|
+
? { kind: 'named-in-message', people: await this.personLabels(namedPeople, message.channelId) }
|
|
1369
|
+
: exchange.recent.length > 0 && !(exchange.request && isAssistantRoom(team ?? null, mapping.slackChannelName))
|
|
1370
|
+
? {
|
|
1371
|
+
kind: exchange.request ? 'recent-exchange-request' : 'recent-exchange',
|
|
1372
|
+
people: await this.personLabels(exchange.recent, message.channelId),
|
|
1373
|
+
}
|
|
1374
|
+
: null;
|
|
1375
|
+
// A recipient told to stay silent by default gets no placeholder and no
|
|
1376
|
+
// auto "working on it": both promise a reply the prompt does not ask for.
|
|
1377
|
+
const addressedHere = new Set(persisted.mentions ?? []);
|
|
1378
|
+
const silentByDefault = (session) => isSilentByDefault(peopleAddressing, planned?.get(session) ?? 'optional', addressedHere.has(session));
|
|
1379
|
+
// Ticket loop (specs/ticket-loop.md §2): the owner's message goes through
|
|
1380
|
+
// the single intake. Started now, awaited just before dispatch, so the
|
|
1381
|
+
// receipt and the agent's `[TICKET:…]` marker do not hold up the eyes /
|
|
1382
|
+
// placeholders below.
|
|
1383
|
+
const ticketPromise = this.intakeTicket(message, mapping, resolved.mentions, planned, handoffTo, remoteAgent);
|
|
1384
|
+
await this.bounded(this.acknowledgeSeen(message, mapping, resolved.mentions, planned), undefined, 'seen reaction', message);
|
|
1385
|
+
// Agents that must reply get a placeholder straight away — "waking up…"
|
|
1386
|
+
// for an idle agent (a cold start is 1–2 minutes), "is working on it…"
|
|
1387
|
+
// once it holds the message. Agents that were only told, and may or may
|
|
1388
|
+
// not decide to answer, get none: a placeholder is a promise of a reply,
|
|
1389
|
+
// and they announce their own with `reply-channel --working` if they
|
|
1390
|
+
// take it on. With a plan this covers the case the old heuristic missed —
|
|
1391
|
+
// a bare follow-up in a thread, which the last speaker must answer.
|
|
1392
|
+
let owing;
|
|
1393
|
+
if (planned) {
|
|
1394
|
+
// An agent that is asleep and still gets the message is being woken
|
|
1395
|
+
// for it — the room's leader when nobody was awake — so it owns the
|
|
1396
|
+
// message and the owner should see that at once, not after the one or
|
|
1397
|
+
// two minutes a cold start takes (#pro-crewly-marketing, 2026-09-23).
|
|
1398
|
+
// Awake agents that were only told announce themselves with --working.
|
|
1399
|
+
// The orchestrator is left out: its bot is usually not in the room.
|
|
1400
|
+
const isAwake = this.deps.isAgentAwake;
|
|
1401
|
+
owing = [...planned]
|
|
1402
|
+
.filter(([session, mode]) => mode === 'required' ||
|
|
1403
|
+
(isAwake !== undefined && !isAwake(session) && session !== CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME))
|
|
1404
|
+
.map(([session]) => session);
|
|
1399
1405
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
// name/icon — the person should see *something* during a cold start.
|
|
1409
|
-
const identity = installed
|
|
1410
|
-
? { botToken: installed.botToken, displayName: member?.name ?? session }
|
|
1411
|
-
: { displayName: member?.name ?? session, ...slackIdentityFor(member, session) };
|
|
1412
|
-
const key = { agentSession: session, slackChannelId: message.channelId, threadTs: slackThreadTs };
|
|
1413
|
-
const awake = this.deps.isAgentAwake ? this.deps.isAgentAwake(session) : true;
|
|
1414
|
-
await this.deps.typing.begin(key, identity, awake ? 'typing' : 'waking', message.ts);
|
|
1415
|
-
typingTargets.push({ session, key });
|
|
1406
|
+
else {
|
|
1407
|
+
owing = resolved.mentions;
|
|
1408
|
+
if (owing.length === 0 && !message.threadTs && team) {
|
|
1409
|
+
// Same rule as the dispatcher's huddleLeaderFor: the team lead, else the first member.
|
|
1410
|
+
const leader = teamChannelLeader(team, members);
|
|
1411
|
+
if (leader)
|
|
1412
|
+
owing = [leader.sessionName];
|
|
1413
|
+
}
|
|
1416
1414
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
const autoWatchCandidates = [
|
|
1422
|
-
...new Set([...(planned ? [...planned.keys()] : []), ...members.map((m) => m.sessionName)]),
|
|
1423
|
-
].filter((session) => !silentByDefault(session));
|
|
1424
|
-
const autoWatch = this.deps.autoWorking && this.deps.typing && autoWatchCandidates.length > 0 && isOwnerAuthored(message, this.deps.getOwnerUserId?.())
|
|
1425
|
-
? this.deps.autoWorking.watch({
|
|
1426
|
-
slackChannelId: message.channelId,
|
|
1427
|
-
threadTs: slackThreadTs,
|
|
1428
|
-
sourceTs: message.ts,
|
|
1429
|
-
candidates: autoWatchCandidates,
|
|
1430
|
-
identityFor: (session) => {
|
|
1415
|
+
owing = owing.filter((session) => !silentByDefault(session));
|
|
1416
|
+
const typingTargets = [];
|
|
1417
|
+
if (this.deps.typing) {
|
|
1418
|
+
for (const session of owing) {
|
|
1431
1419
|
const member = members.find((m) => m.sessionName === session);
|
|
1432
1420
|
const installed = this.deps.identities?.getInstalled(session);
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
let roster = await getSlackDirectoryService()?.rosterLine(message.channelId).catch(() => '');
|
|
1444
|
-
// The directory needs the master bot to list a channel's members; in a
|
|
1445
|
-
// private ad-hoc channel it is not a member, so fall back to the local
|
|
1446
|
-
// huddle roster (with roles) — enough to answer "who leads this?".
|
|
1447
|
-
if (!roster && isAdhocMapping(mapping)) {
|
|
1448
|
-
roster = members
|
|
1449
|
-
.filter((m) => (mapping.members ?? []).includes(m.sessionName))
|
|
1450
|
-
.map((m) => `${m.name} (${teams.find((t) => (t.members ?? []).some((x) => x.id === m.id))?.name ?? '?'}, ${String(m.role)}, this machine)`)
|
|
1451
|
-
.join(' · ');
|
|
1421
|
+
// Own bot when installed; otherwise the master bot wearing the agent's
|
|
1422
|
+
// name/icon — the person should see *something* during a cold start.
|
|
1423
|
+
const identity = installed
|
|
1424
|
+
? { botToken: installed.botToken, displayName: member?.name ?? session }
|
|
1425
|
+
: { displayName: member?.name ?? session, ...slackIdentityFor(member, session) };
|
|
1426
|
+
const key = { agentSession: session, slackChannelId: message.channelId, threadTs: slackThreadTs };
|
|
1427
|
+
const awake = this.deps.isAgentAwake ? this.deps.isAgentAwake(session) : true;
|
|
1428
|
+
await this.bounded(this.deps.typing.begin(key, identity, awake ? 'typing' : 'waking', message.ts), null, 'placeholder', message);
|
|
1429
|
+
typingTargets.push({ session, key });
|
|
1430
|
+
}
|
|
1452
1431
|
}
|
|
1453
|
-
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1432
|
+
// Recipients who were only told get no placeholder above; the first of
|
|
1433
|
+
// them that starts working on an owner's message gets one posted for it
|
|
1434
|
+
// by the harness (2026-09-30: Owen, 3.5 min of nothing in #pro-ce).
|
|
1435
|
+
const autoWatchCandidates = [
|
|
1436
|
+
...new Set([...(planned ? [...planned.keys()] : []), ...members.map((m) => m.sessionName)]),
|
|
1437
|
+
].filter((session) => !silentByDefault(session));
|
|
1438
|
+
const autoWatch = this.deps.autoWorking && this.deps.typing && autoWatchCandidates.length > 0 && isOwnerAuthored(message, this.deps.getOwnerUserId?.())
|
|
1439
|
+
? this.deps.autoWorking.watch({
|
|
1440
|
+
slackChannelId: message.channelId,
|
|
1441
|
+
threadTs: slackThreadTs,
|
|
1442
|
+
sourceTs: message.ts,
|
|
1443
|
+
candidates: autoWatchCandidates,
|
|
1444
|
+
identityFor: (session) => {
|
|
1445
|
+
const member = members.find((m) => m.sessionName === session);
|
|
1446
|
+
const installed = this.deps.identities?.getInstalled(session);
|
|
1447
|
+
const displayName = member?.name ?? session;
|
|
1448
|
+
return installed
|
|
1449
|
+
? { botToken: installed.botToken, displayName }
|
|
1450
|
+
: { displayName, ...slackIdentityFor(member, session) };
|
|
1451
|
+
},
|
|
1452
|
+
})
|
|
1453
|
+
: null;
|
|
1454
|
+
const dispatcher = this.deps.getDispatcher();
|
|
1455
|
+
let dispatch = null;
|
|
1456
|
+
if (dispatcher) {
|
|
1457
|
+
let roster = await this.bounded(getSlackDirectoryService()?.rosterLine(message.channelId).catch(() => '') ?? Promise.resolve(''), '', 'room roster', message);
|
|
1458
|
+
// The directory needs the master bot to list a channel's members; in a
|
|
1459
|
+
// private ad-hoc channel it is not a member, so fall back to the local
|
|
1460
|
+
// huddle roster (with roles) — enough to answer "who leads this?".
|
|
1461
|
+
if (!roster && isAdhocMapping(mapping)) {
|
|
1462
|
+
roster = members
|
|
1463
|
+
.filter((m) => (mapping.members ?? []).includes(m.sessionName))
|
|
1464
|
+
.map((m) => `${m.name} (${teams.find((t) => (t.members ?? []).some((x) => x.id === m.id))?.name ?? '?'}, ${String(m.role)}, this machine)`)
|
|
1465
|
+
.join(' · ');
|
|
1466
|
+
}
|
|
1467
|
+
const ticket = await this.bounded(ticketPromise, null, 'ticket intake', message);
|
|
1468
|
+
// The thread as Slack has it — posts by agents on other machines
|
|
1469
|
+
// included — rendered per recipient so its own lines are marked.
|
|
1470
|
+
const slackContext = await this.bounded(Promise.resolve(message.threadContext), undefined, 'thread context', message);
|
|
1471
|
+
// A rescue already ran (routing stalled before this point): it has
|
|
1472
|
+
// handed the message over, so this late routing must not deliver it
|
|
1473
|
+
// again, nor arm another unanswered watch.
|
|
1474
|
+
if (routeGuard && !routeGuard.dispatchStarting()) {
|
|
1475
|
+
this.logger.warn('Owner room message reached dispatch after it was rescued — not dispatching it again', {
|
|
1476
|
+
slackChannel: `#${mapping.slackChannelName}`,
|
|
1477
|
+
ts: message.ts,
|
|
1478
|
+
});
|
|
1479
|
+
return { mapping, message: persisted, mentions: resolved.mentions, dispatch: null };
|
|
1480
|
+
}
|
|
1481
|
+
dispatch = await dispatcher.dispatchMessage(channel, markAndLinkTicket(persisted, ticket), {
|
|
1482
|
+
...dispatchOptions,
|
|
1483
|
+
...(roster ? { channelRoster: roster } : {}),
|
|
1484
|
+
...(peopleAddressing ? { peopleAddressing } : {}),
|
|
1485
|
+
...(slackContext
|
|
1486
|
+
? {
|
|
1487
|
+
slackContextFor: (session) => renderSlackThreadContext(slackContext, {
|
|
1488
|
+
botUserId: this.deps.identities?.get(session)?.botUserId,
|
|
1489
|
+
name: members.find((m) => m.sessionName === session)?.name,
|
|
1490
|
+
}),
|
|
1491
|
+
}
|
|
1492
|
+
: {}),
|
|
1493
|
+
});
|
|
1490
1494
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
mentions: resolved.mentions,
|
|
1496
|
-
...(mentionedElsewhere.length > 0 ? { mentionedElsewhere } : {}),
|
|
1497
|
-
unknown: resolved.unknown.map((u) => u.token),
|
|
1498
|
-
strategy: dispatch?.strategy ?? 'none',
|
|
1499
|
-
threaded: !!threadId,
|
|
1500
|
-
});
|
|
1501
|
-
const recipients = deliveredSessions(dispatch);
|
|
1502
|
-
if (dispatch?.dispatched) {
|
|
1503
|
-
// Someone here holds it — including a follow-up in a thread we were
|
|
1504
|
-
// still waiting on.
|
|
1505
|
-
this.settleUnanswered(`${message.channelId}:${slackThreadTs}`);
|
|
1506
|
-
// The owner machine of a shared room (others deferred to it) runs the
|
|
1507
|
-
// one 90 s fallback: if nobody here answers, its lead is woken.
|
|
1508
|
-
const allOptional = (dispatch.huddleOutcomes ?? []).every((o) => o.responseMode !== 'required');
|
|
1509
|
-
if (presence?.ownsSharedMessage && allOptional && !handoffTo && isOwnerAuthored(message, this.deps.getOwnerUserId?.())) {
|
|
1510
|
-
this.watchUnanswered(message, mapping, persisted, dispatchOptions.threadId, recipients);
|
|
1495
|
+
else {
|
|
1496
|
+
this.logger.warn('No chat dispatcher wired — message persisted but not delivered', {
|
|
1497
|
+
huddle: mapping.chatChannelId,
|
|
1498
|
+
});
|
|
1511
1499
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1500
|
+
if (resolved.unknown.length > 0) {
|
|
1501
|
+
await this.postUnknownMentionHint(message, resolved.unknown, candidates);
|
|
1502
|
+
}
|
|
1503
|
+
autoWatch?.delivered(deliveredSessions(dispatch));
|
|
1504
|
+
// Dispatch is done: each placeholder now reflects whether its agent
|
|
1505
|
+
// actually holds the message.
|
|
1506
|
+
if (this.deps.typing && typingTargets.length > 0) {
|
|
1507
|
+
const outcomes = new Map((dispatch?.huddleOutcomes ?? []).map((o) => [o.sessionName, o.dispatched]));
|
|
1508
|
+
for (const { session, key } of typingTargets) {
|
|
1509
|
+
const delivered = outcomes.get(session) ?? dispatch?.dispatched ?? false;
|
|
1510
|
+
if (delivered)
|
|
1511
|
+
await this.deps.typing.setPhase(key, 'typing');
|
|
1512
|
+
else
|
|
1513
|
+
await this.deps.typing.fail(key);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
this.logger.info('Slack team message routed', {
|
|
1516
1517
|
teamId: mapping.teamId,
|
|
1517
1518
|
slackChannel: `#${mapping.slackChannelName}`,
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
wakeWhenAllAsleep: presence?.state.wakeWhenAllAsleep ?? null,
|
|
1524
|
-
handoffTo,
|
|
1525
|
-
recipients,
|
|
1519
|
+
mentions: resolved.mentions,
|
|
1520
|
+
...(mentionedElsewhere.length > 0 ? { mentionedElsewhere } : {}),
|
|
1521
|
+
unknown: resolved.unknown.map((u) => u.token),
|
|
1522
|
+
strategy: dispatch?.strategy ?? 'none',
|
|
1523
|
+
threaded: !!threadId,
|
|
1526
1524
|
});
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1525
|
+
const recipients = deliveredSessions(dispatch);
|
|
1526
|
+
if (dispatch?.dispatched) {
|
|
1527
|
+
// Someone here holds it — including a follow-up in a thread we were
|
|
1528
|
+
// still waiting on.
|
|
1529
|
+
this.settleUnanswered(`${message.channelId}:${slackThreadTs}`);
|
|
1530
|
+
// The owner machine of a shared room (others deferred to it) runs the
|
|
1531
|
+
// one 90 s fallback: if nobody here answers, its lead is woken.
|
|
1532
|
+
const allOptional = (dispatch.huddleOutcomes ?? []).every((o) => o.responseMode !== 'required');
|
|
1533
|
+
if (presence?.ownsSharedMessage && allOptional && !handoffTo && isOwnerAuthored(message, this.deps.getOwnerUserId?.())) {
|
|
1534
|
+
this.watchUnanswered(message, mapping, persisted, dispatchOptions.threadId, recipients);
|
|
1535
|
+
}
|
|
1531
1536
|
}
|
|
1537
|
+
else if (!remoteAgent) {
|
|
1538
|
+
// Never a silent no-op: say why nobody got it.
|
|
1539
|
+
this.logger.warn('Slack room message reached nobody on this machine', {
|
|
1540
|
+
teamId: mapping.teamId,
|
|
1541
|
+
slackChannel: `#${mapping.slackChannelName}`,
|
|
1542
|
+
ts: message.ts,
|
|
1543
|
+
planned: planned ? [...planned.keys()] : null,
|
|
1544
|
+
failed: (dispatch?.huddleOutcomes ?? []).filter((o) => !o.dispatched).map((o) => o.sessionName),
|
|
1545
|
+
awakeHere: presence?.state.awakeHere ?? null,
|
|
1546
|
+
awakeElsewhere: presence?.state.awakeElsewhere ?? null,
|
|
1547
|
+
wakeWhenAllAsleep: presence?.state.wakeWhenAllAsleep ?? null,
|
|
1548
|
+
handoffTo,
|
|
1549
|
+
recipients,
|
|
1550
|
+
});
|
|
1551
|
+
// A message deferred to another machine is watched there, by its
|
|
1552
|
+
// owner, only — two machines running the fallback would answer twice.
|
|
1553
|
+
if (!handoffTo && !presence?.deferredElsewhere && !presence?.fallbackElsewhere && isOwnerAuthored(message, this.deps.getOwnerUserId?.())) {
|
|
1554
|
+
this.watchUnanswered(message, mapping, persisted, dispatchOptions.threadId, recipients);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return { mapping, message: persisted, mentions: resolved.mentions, dispatch };
|
|
1558
|
+
}
|
|
1559
|
+
catch (err) {
|
|
1560
|
+
routeGuard?.fail(err);
|
|
1561
|
+
throw err;
|
|
1562
|
+
}
|
|
1563
|
+
finally {
|
|
1564
|
+
routeGuard?.disarm();
|
|
1532
1565
|
}
|
|
1533
|
-
return { mapping, message: persisted, mentions: resolved.mentions, dispatch };
|
|
1534
1566
|
}
|
|
1535
1567
|
/**
|
|
1536
1568
|
* Hold an owner's room message that reached nobody here until an agent
|
|
@@ -1565,9 +1597,140 @@ export class SlackTeamChannelService {
|
|
|
1565
1597
|
const pending = this.unanswered.get(key);
|
|
1566
1598
|
if (!pending)
|
|
1567
1599
|
return;
|
|
1568
|
-
|
|
1600
|
+
if (pending.timer)
|
|
1601
|
+
clearTimeout(pending.timer);
|
|
1569
1602
|
this.unanswered.delete(key);
|
|
1570
1603
|
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Arm the route guard for an owner's room message that was just recorded
|
|
1606
|
+
* (crewly#1015 §7). `dispatchStarting` right before dispatch: it stops the
|
|
1607
|
+
* timer and answers false when a rescue already ran (the caller must not
|
|
1608
|
+
* dispatch then). `fail` when routing threw — a rescue only when dispatch
|
|
1609
|
+
* had not started. `disarm` when routing finished. Not at dispatch within
|
|
1610
|
+
* ROUTE_STALL_MS → the message is rescued.
|
|
1611
|
+
*
|
|
1612
|
+
* @param message - The inbound Slack message
|
|
1613
|
+
* @param mapping - Its room
|
|
1614
|
+
* @param persisted - The huddle row it was recorded as
|
|
1615
|
+
* @param threadId - Huddle thread the replies land in
|
|
1616
|
+
* @returns The guard
|
|
1617
|
+
*/
|
|
1618
|
+
armRouteGuard(message, mapping, persisted, threadId) {
|
|
1619
|
+
let done = false;
|
|
1620
|
+
let rescued = false;
|
|
1621
|
+
const rescue = (why, err) => {
|
|
1622
|
+
if (done)
|
|
1623
|
+
return;
|
|
1624
|
+
done = true;
|
|
1625
|
+
rescued = true;
|
|
1626
|
+
clearTimeout(timer);
|
|
1627
|
+
this.logger.error(why === 'stalled'
|
|
1628
|
+
? 'Owner room message routing has not finished — rescuing it with the unanswered-message fallback'
|
|
1629
|
+
: 'Owner room message routing failed — rescuing it with the unanswered-message fallback', {
|
|
1630
|
+
slackChannel: `#${mapping.slackChannelName}`,
|
|
1631
|
+
ts: message.ts,
|
|
1632
|
+
threaded: !!message.threadTs,
|
|
1633
|
+
handoff: message.handoffTo ?? null,
|
|
1634
|
+
...(err !== undefined ? { error: err instanceof Error ? err.message : String(err) } : {}),
|
|
1635
|
+
});
|
|
1636
|
+
void this.rescueUnroutedMessage(message, mapping, persisted, threadId).catch((rescueErr) => this.logger.error('Could not rescue an unrouted owner room message', {
|
|
1637
|
+
ts: message.ts,
|
|
1638
|
+
error: rescueErr instanceof Error ? rescueErr.message : String(rescueErr),
|
|
1639
|
+
}));
|
|
1640
|
+
};
|
|
1641
|
+
const timer = setTimeout(() => rescue('stalled'), this.routeStallMs());
|
|
1642
|
+
timer.unref?.();
|
|
1643
|
+
return {
|
|
1644
|
+
disarm: () => {
|
|
1645
|
+
if (done)
|
|
1646
|
+
return;
|
|
1647
|
+
done = true;
|
|
1648
|
+
clearTimeout(timer);
|
|
1649
|
+
},
|
|
1650
|
+
// After dispatch started, a throw is not an undelivered message:
|
|
1651
|
+
// rescuing then could deliver it twice.
|
|
1652
|
+
fail: (err) => rescue('failed', err),
|
|
1653
|
+
dispatchStarting: () => {
|
|
1654
|
+
if (rescued)
|
|
1655
|
+
return false;
|
|
1656
|
+
done = true;
|
|
1657
|
+
clearTimeout(timer);
|
|
1658
|
+
return true;
|
|
1659
|
+
},
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Wait for a routing step at most ROUTE_STEP_TIMEOUT_MS; on timeout (or a
|
|
1664
|
+
* rejection) go on with `fallback` and say so. Only for steps before
|
|
1665
|
+
* dispatch that routing can do without (crewly#1015 §7).
|
|
1666
|
+
*
|
|
1667
|
+
* @param step - The step
|
|
1668
|
+
* @param fallback - Value used when it does not settle in time
|
|
1669
|
+
* @param label - For the log
|
|
1670
|
+
* @param message - The message being routed (for the log)
|
|
1671
|
+
* @returns The step's value, or the fallback
|
|
1672
|
+
*/
|
|
1673
|
+
async bounded(step, fallback, label, message) {
|
|
1674
|
+
let timer;
|
|
1675
|
+
const timeout = new Promise((resolve) => {
|
|
1676
|
+
timer = setTimeout(() => resolve({ timedOut: true }), this.windowMs(SLACK_TEAM_CHANNEL_CONSTANTS.ROUTE_STEP_TIMEOUT_ENV, SLACK_TEAM_CHANNEL_CONSTANTS.ROUTE_STEP_TIMEOUT_MS));
|
|
1677
|
+
timer.unref?.();
|
|
1678
|
+
});
|
|
1679
|
+
try {
|
|
1680
|
+
const out = await Promise.race([step.then((value) => ({ value })), timeout]);
|
|
1681
|
+
if ('timedOut' in out) {
|
|
1682
|
+
this.logger.warn('A routing step did not finish in time — going on without it', { step: label, ts: message.ts });
|
|
1683
|
+
return fallback;
|
|
1684
|
+
}
|
|
1685
|
+
return out.value;
|
|
1686
|
+
}
|
|
1687
|
+
catch (err) {
|
|
1688
|
+
this.logger.warn('A routing step failed — going on without it', { step: label, ts: message.ts, error: err instanceof Error ? err.message : String(err) });
|
|
1689
|
+
return fallback;
|
|
1690
|
+
}
|
|
1691
|
+
finally {
|
|
1692
|
+
if (timer)
|
|
1693
|
+
clearTimeout(timer);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
/** @returns How long routing may take before the guard rescues the message (env override for tests). */
|
|
1697
|
+
routeStallMs() {
|
|
1698
|
+
return this.windowMs(SLACK_TEAM_CHANNEL_CONSTANTS.ROUTE_STALL_ENV, SLACK_TEAM_CHANNEL_CONSTANTS.ROUTE_STALL_MS);
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Rescue an owner's room message whose routing stalled or failed: the
|
|
1702
|
+
* unanswered-message fallback runs at once (the room lead here is handed
|
|
1703
|
+
* the message). A message that was itself such a hand-off is not handed
|
|
1704
|
+
* over again: the owner is told in the thread.
|
|
1705
|
+
*
|
|
1706
|
+
* @param message - The inbound Slack message
|
|
1707
|
+
* @param mapping - Its room
|
|
1708
|
+
* @param persisted - The huddle row
|
|
1709
|
+
* @param threadId - Huddle thread the replies land in
|
|
1710
|
+
*/
|
|
1711
|
+
async rescueUnroutedMessage(message, mapping, persisted, threadId) {
|
|
1712
|
+
const threadTs = message.threadTs || message.ts;
|
|
1713
|
+
const key = `${message.channelId}:${threadTs}`;
|
|
1714
|
+
const pending = {
|
|
1715
|
+
// The hand-off does without the Slack thread context: reading it may
|
|
1716
|
+
// be what got stuck.
|
|
1717
|
+
message: { ...message, threadContext: undefined },
|
|
1718
|
+
chatChannelId: mapping.chatChannelId,
|
|
1719
|
+
slackChannelId: message.channelId,
|
|
1720
|
+
threadTs,
|
|
1721
|
+
threadId,
|
|
1722
|
+
messageId: persisted.id,
|
|
1723
|
+
recipients: [],
|
|
1724
|
+
note: SLACK_TEAM_CHANNEL_CONSTANTS.ROOM_ROUTE_STALLED_NOTE,
|
|
1725
|
+
};
|
|
1726
|
+
if (message.handoffTo) {
|
|
1727
|
+
await this.postUnansweredNote(pending, mapping);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
this.settleUnanswered(key);
|
|
1731
|
+
this.unanswered.set(key, pending);
|
|
1732
|
+
await this.runUnansweredFallback(key);
|
|
1733
|
+
}
|
|
1571
1734
|
/**
|
|
1572
1735
|
* An agent spoke in a room — any message we were waiting on in that
|
|
1573
1736
|
* thread (or the room, for a top-level post) has been taken.
|
|
@@ -1684,7 +1847,7 @@ export class SlackTeamChannelService {
|
|
|
1684
1847
|
await this.deps.slack.sendMessage({
|
|
1685
1848
|
channelId: pending.slackChannelId,
|
|
1686
1849
|
threadTs: pending.threadTs,
|
|
1687
|
-
text: SLACK_TEAM_CHANNEL_CONSTANTS.ROOM_UNANSWERED_NOTE,
|
|
1850
|
+
text: pending.note ?? SLACK_TEAM_CHANNEL_CONSTANTS.ROOM_UNANSWERED_NOTE,
|
|
1688
1851
|
skipChatV2Mirror: true,
|
|
1689
1852
|
notAnAnswer: true,
|
|
1690
1853
|
...(botToken ? { botToken } : {}),
|