cli-jaw 2.2.8 → 2.2.10
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/README.md +4 -4
- package/dist/bin/agent-driven.js +33 -0
- package/dist/bin/agent-driven.js.map +1 -0
- package/dist/bin/commands/doctor.js +66 -1
- package/dist/bin/commands/doctor.js.map +1 -1
- package/dist/bin/commands/init.js +107 -8
- package/dist/bin/commands/init.js.map +1 -1
- package/dist/bin/interactive-confirm.js +110 -0
- package/dist/bin/interactive-confirm.js.map +1 -0
- package/dist/bin/star-prompt.js +46 -15
- package/dist/bin/star-prompt.js.map +1 -1
- package/dist/lib/upload.js +5 -1
- package/dist/lib/upload.js.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/src/agent/alert-escalation.js +1 -1
- package/dist/src/agent/alert-escalation.js.map +1 -1
- package/dist/src/agent/codex-app-catalog.js +57 -0
- package/dist/src/agent/codex-app-catalog.js.map +1 -0
- package/dist/src/agent/codex-app-client.js +89 -7
- package/dist/src/agent/codex-app-client.js.map +1 -1
- package/dist/src/agent/pi-rpc-verdict.js +22 -0
- package/dist/src/agent/pi-rpc-verdict.js.map +1 -0
- package/dist/src/agent/pi-runtime.js +240 -0
- package/dist/src/agent/pi-runtime.js.map +1 -1
- package/dist/src/agent/runtime-pool.js +445 -0
- package/dist/src/agent/runtime-pool.js.map +1 -0
- package/dist/src/agent/spawn.js +395 -240
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/command-context.js +5 -2
- package/dist/src/cli/command-context.js.map +1 -1
- package/dist/src/cli/commands.js +43 -39
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/handlers-runtime.js +15 -7
- package/dist/src/cli/handlers-runtime.js.map +1 -1
- package/dist/src/cli/handlers-search.js +1 -1
- package/dist/src/cli/handlers-search.js.map +1 -1
- package/dist/src/cli/handlers-skill-invoke.js +1 -1
- package/dist/src/cli/handlers-skill-invoke.js.map +1 -1
- package/dist/src/cli/handlers-workflows.js +1 -1
- package/dist/src/cli/handlers-workflows.js.map +1 -1
- package/dist/src/cli/handlers.js +1 -1
- package/dist/src/cli/handlers.js.map +1 -1
- package/dist/src/cli/opencodex-models.js +40 -7
- package/dist/src/cli/opencodex-models.js.map +1 -1
- package/dist/src/cli/registry-live.js +6 -5
- package/dist/src/cli/registry-live.js.map +1 -1
- package/dist/src/command-contract/catalog.js +4 -0
- package/dist/src/command-contract/catalog.js.map +1 -1
- package/dist/src/core/config.js +69 -4
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/runtime-settings.js +17 -0
- package/dist/src/core/runtime-settings.js.map +1 -1
- package/dist/src/core/settings-merge.js +1 -1
- package/dist/src/core/settings-merge.js.map +1 -1
- package/dist/src/discord/bot.js +29 -14
- package/dist/src/discord/bot.js.map +1 -1
- package/dist/src/discord/commands.js +5 -4
- package/dist/src/discord/commands.js.map +1 -1
- package/dist/src/discord/discord-file.js +8 -2
- package/dist/src/discord/discord-file.js.map +1 -1
- package/dist/src/discord/forwarder.js +17 -19
- package/dist/src/discord/forwarder.js.map +1 -1
- package/dist/src/discord/send-only-client.js +9 -1
- package/dist/src/discord/send-only-client.js.map +1 -1
- package/dist/src/manager/dashboard-service.js +38 -1
- package/dist/src/manager/dashboard-service.js.map +1 -1
- package/dist/src/manager/telegram-hub/hub-bot.js +47 -29
- package/dist/src/manager/telegram-hub/hub-bot.js.map +1 -1
- package/dist/src/messaging/channel-health.js +32 -0
- package/dist/src/messaging/channel-health.js.map +1 -1
- package/dist/src/messaging/chunk.js +376 -0
- package/dist/src/messaging/chunk.js.map +1 -0
- package/dist/src/messaging/dedupe.js +104 -0
- package/dist/src/messaging/dedupe.js.map +1 -0
- package/dist/src/messaging/fold.js +210 -0
- package/dist/src/messaging/fold.js.map +1 -0
- package/dist/src/messaging/redact.js +577 -0
- package/dist/src/messaging/redact.js.map +1 -0
- package/dist/src/messaging/retry.js +76 -0
- package/dist/src/messaging/retry.js.map +1 -0
- package/dist/src/messaging/runtime.js +6 -3
- package/dist/src/messaging/runtime.js.map +1 -1
- package/dist/src/messaging/send.js +45 -15
- package/dist/src/messaging/send.js.map +1 -1
- package/dist/src/messaging/slack-target.js +50 -0
- package/dist/src/messaging/slack-target.js.map +1 -0
- package/dist/src/messaging/types.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +4 -4
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/routes/command.js +11 -7
- package/dist/src/routes/command.js.map +1 -1
- package/dist/src/routes/messaging.js +28 -12
- package/dist/src/routes/messaging.js.map +1 -1
- package/dist/src/routes/settings.js +22 -6
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/slack/api.js +116 -0
- package/dist/src/slack/api.js.map +1 -0
- package/dist/src/slack/bot.js +262 -0
- package/dist/src/slack/bot.js.map +1 -0
- package/dist/src/slack/commands.js +98 -0
- package/dist/src/slack/commands.js.map +1 -0
- package/dist/src/slack/events.js +116 -0
- package/dist/src/slack/events.js.map +1 -0
- package/dist/src/slack/format.js +58 -0
- package/dist/src/slack/format.js.map +1 -0
- package/dist/src/slack/forwarder.js +57 -0
- package/dist/src/slack/forwarder.js.map +1 -0
- package/dist/src/slack/register.js +13 -0
- package/dist/src/slack/register.js.map +1 -0
- package/dist/src/slack/send-handler.js +41 -0
- package/dist/src/slack/send-handler.js.map +1 -0
- package/dist/src/slack/send-only-client.js +46 -0
- package/dist/src/slack/send-only-client.js.map +1 -0
- package/dist/src/slack/slack-file.js +72 -0
- package/dist/src/slack/slack-file.js.map +1 -0
- package/dist/src/slack/socket.js +341 -0
- package/dist/src/slack/socket.js.map +1 -0
- package/dist/src/telegram/bot.js +117 -37
- package/dist/src/telegram/bot.js.map +1 -1
- package/dist/src/telegram/elicitation-buttons.js +7 -2
- package/dist/src/telegram/elicitation-buttons.js.map +1 -1
- package/dist/src/telegram/forwarder.js +15 -7
- package/dist/src/telegram/forwarder.js.map +1 -1
- package/dist/src/telegram/rich-message.js +209 -24
- package/dist/src/telegram/rich-message.js.map +1 -1
- package/dist/src/telegram/telegram-file.js +35 -4
- package/dist/src/telegram/telegram-file.js.map +1 -1
- package/dist/src/telegram/voice.js +4 -3
- package/dist/src/telegram/voice.js.map +1 -1
- package/package.json +5 -1
- package/public/assets/providers/slack.svg +1 -0
- package/public/dist/assets/ChannelsDiscord-D9FLnZso.js +1 -0
- package/public/dist/assets/ChannelsSlack-ybvupgdK.js +1 -0
- package/public/dist/assets/ChannelsTelegram-BOCOPrBc.js +1 -0
- package/public/dist/assets/HealthBadge-megTlF4n.js +1 -0
- package/public/dist/assets/TransportStatusChips-CjEySMis.js +1 -0
- package/public/dist/assets/app-BGIAy4he.js +55 -0
- package/public/dist/assets/{bgtask-badge-CdBqBy1L.js → bgtask-badge-DXca8E7c.js} +1 -1
- package/public/dist/assets/bgtask-badge-GRSiNILQ.js +1 -0
- package/public/dist/assets/{employees-C8qufeJl.js → employees-BL4qMxJj.js} +1 -1
- package/public/dist/assets/iframe-renderer-BUSk0Rw9.js +1 -0
- package/public/dist/assets/{iframe-renderer-CZ67aeTO.js → iframe-renderer-DUI0LYBy.js} +2 -2
- package/public/dist/assets/{manager-CLXT60M5.js → manager-B-gpf-3j.js} +3 -3
- package/public/dist/assets/{memory-lWramLVw.js → memory-B_Y0L0KD.js} +1 -1
- package/public/dist/assets/memory-CX6YahTc.js +1 -0
- package/public/dist/assets/message-history-Det7u76S.js +1 -0
- package/public/dist/assets/{message-history-2NUN3UxM.js → message-history-Dl8dJzez.js} +1 -1
- package/public/dist/assets/{provider-icons-stsNmQKx.js → provider-icons-VDfahiUp.js} +3 -2
- package/public/dist/assets/{render-CsF-ojLe.js → render-BI1aqKoc.js} +1 -1
- package/public/dist/assets/{settings-BMLowRCh.js → settings-B0Sj4sUH.js} +1 -1
- package/public/dist/assets/settings-C7NutP6X.js +1 -0
- package/public/dist/assets/settings-core-CyX-px6_.js +100 -0
- package/public/dist/assets/settings-core-DVpFmrhY.js +1 -0
- package/public/dist/assets/{sidebar-gKjYLv49.js → sidebar-DOR9fTgN.js} +3 -3
- package/public/dist/assets/skills-BWhKreQ2.js +1 -0
- package/public/dist/assets/{skills-CKTbomdm.js → skills-wt0fkV9Y.js} +1 -1
- package/public/dist/assets/slash-commands-BZbqTg3v.js +20 -0
- package/public/dist/assets/slash-commands-Di4NDA4w.js +1 -0
- package/public/dist/assets/{trace-drawer-C3G4mmB1.js → trace-drawer-kMYzaVFW.js} +1 -1
- package/public/dist/assets/ui-BghexWrB.js +1 -0
- package/public/dist/assets/{ui-CVVC_flX.js → ui-D4D5HUfo.js} +1 -1
- package/public/dist/index.html +62 -1
- package/public/dist/manager/index.html +1 -1
- package/public/index.html +61 -0
- package/public/js/features/command-info.ts +1 -0
- package/public/js/features/help-content.ts +8 -0
- package/public/js/features/settings-channel.ts +5 -1
- package/public/js/features/settings-core.ts +2 -0
- package/public/js/features/settings-slack.ts +74 -0
- package/public/js/features/settings-types.ts +5 -1
- package/public/js/features/settings.ts +1 -0
- package/public/js/features/transport-status-row.ts +15 -3
- package/public/js/main.ts +19 -0
- package/public/js/provider-icons.ts +4 -1
- package/public/js/ws.ts +1 -1
- package/public/locales/en.json +21 -0
- package/public/locales/ja.json +21 -0
- package/public/locales/ko.json +21 -0
- package/public/locales/zh.json +21 -0
- package/public/manager/src/settings/SettingsShell.tsx +1 -0
- package/public/manager/src/settings/SettingsSidebar.tsx +1 -0
- package/public/manager/src/settings/pages/ChannelsSlack.tsx +323 -0
- package/public/manager/src/settings/pages/components/ActiveChannelToggle.tsx +2 -1
- package/public/manager/src/settings/pages/components/TransportStatusChips.tsx +13 -4
- package/public/manager/src/settings/types.ts +1 -0
- package/scripts/check-redaction-sinks.mjs +276 -0
- package/scripts/pi-rpc-probe.mts +361 -0
- package/scripts/release-gates.mjs +83 -0
- package/scripts/release-preview.sh +8 -1
- package/scripts/release.sh +13 -2
- package/scripts/sync-electron-version.cjs +93 -0
- package/public/dist/assets/ChannelsDiscord-Bt1_rtNC.js +0 -1
- package/public/dist/assets/ChannelsTelegram-BLV0m1oq.js +0 -1
- package/public/dist/assets/TransportStatusChips-Bld0yXi7.js +0 -1
- package/public/dist/assets/app-iSJhsP20.js +0 -55
- package/public/dist/assets/bgtask-badge-CSkSsoB6.js +0 -1
- package/public/dist/assets/iframe-renderer-DXdo0YXN.js +0 -1
- package/public/dist/assets/memory-DeKUlD2M.js +0 -1
- package/public/dist/assets/message-history-CWTQ2woA.js +0 -1
- package/public/dist/assets/settings-CDjs7hTT.js +0 -1
- package/public/dist/assets/settings-core-Cs_oq-b3.js +0 -99
- package/public/dist/assets/settings-core-D-TOh1a6.js +0 -1
- package/public/dist/assets/skills-GuIfNASs.js +0 -1
- package/public/dist/assets/slash-commands-BtZa093n.js +0 -20
- package/public/dist/assets/slash-commands-DGJKfpMT.js +0 -1
- package/public/dist/assets/ui-DJg5Q_NJ.js +0 -1
- /package/public/dist/assets/{DashboardMeta-B7CVqXcU.js → DashboardMeta-DennaaA_.js} +0 -0
- /package/public/dist/assets/{Display-BF-rACK6.js → Display-hGuSn_W5.js} +0 -0
- /package/public/dist/assets/{Employees-D3hnciP0.js → Employees-B5YXk5ma.js} +0 -0
- /package/public/dist/assets/{Heartbeat-D4Pn5O3O.js → Heartbeat-vDGhPfct.js} +0 -0
- /package/public/dist/assets/{Mcp-SfytKWGX.js → Mcp-BHWnJ6SP.js} +0 -0
- /package/public/dist/assets/{Memory-DNxBOQ7N.js → Memory-B_UZ-iCA.js} +0 -0
- /package/public/dist/assets/{ModelProvider-CXwtlG7l.js → ModelProvider-CgUvs6pE.js} +0 -0
- /package/public/dist/assets/{Network-DISUhige.js → Network-AWpSkALD.js} +0 -0
- /package/public/dist/assets/{Profile-B8yBO3Ju.js → Profile-DnIE2ios.js} +0 -0
- /package/public/dist/assets/{Prompts-5lNQ5qOm.js → Prompts-B9aKLall.js} +0 -0
- /package/public/dist/assets/{SpeechKeys-BepaIHBa.js → SpeechKeys-K4Be_o7z.js} +0 -0
- /package/public/dist/assets/{TelegramHub--DNjRAwW.js → TelegramHub-CFwSkzHP.js} +0 -0
- /package/public/dist/assets/{TerminalPanel-DQovYfwz.js → TerminalPanel-CdlFZHAK.js} +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ─── Slack transport registrar (lazy) ────────────────
|
|
2
|
+
// Slack has no heavy SDK dependency (see 000_plan.md D-1), but the lazy shape
|
|
3
|
+
// mirrors src/discord/register.ts so every transport registers the same way and
|
|
4
|
+
// a disabled Slack never pulls its modules in.
|
|
5
|
+
//
|
|
6
|
+
import { registerTransport } from '../messaging/runtime.js';
|
|
7
|
+
import { registerSendTransport } from '../messaging/send.js';
|
|
8
|
+
registerTransport('slack', {
|
|
9
|
+
init: async () => (await import('./bot.js')).initSlack(),
|
|
10
|
+
shutdown: async () => (await import('./bot.js')).shutdownSlack(),
|
|
11
|
+
});
|
|
12
|
+
registerSendTransport('slack', async (req) => (await import('./send-handler.js')).slackSendHandler(req));
|
|
13
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../../src/slack/register.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,8EAA8E;AAC9E,gFAAgF;AAChF,+CAA+C;AAC/C,EAAE;AACF,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,iBAAiB,CAAC,OAAO,EAAE;IACvB,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE;IACxD,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,EAAE;CACnE,CAAC,CAAC;AAEH,qBAAqB,CAAC,OAAO,EAAE,KAAK,EAAE,GAAuB,EAAE,EAAE,CAC7D,CAAC,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ─── Slack Send Handler ──────────────────────────────
|
|
2
|
+
// Adapts the channel-agnostic ChannelSendRequest to Slack's Web API.
|
|
3
|
+
import { slackTargetFromId } from '../messaging/slack-target.js';
|
|
4
|
+
import { getSlackSendClient, resolveSlackDmChannel, sendSlackText } from './send-only-client.js';
|
|
5
|
+
import { sendSlackFile } from './slack-file.js';
|
|
6
|
+
export async function slackSendHandler(req) {
|
|
7
|
+
const client = getSlackSendClient();
|
|
8
|
+
if (!client.token) {
|
|
9
|
+
return { ok: false, error: client.reason ?? 'slack_unavailable', status: client.status ?? 503 };
|
|
10
|
+
}
|
|
11
|
+
let target = req.target;
|
|
12
|
+
if (!target)
|
|
13
|
+
return { ok: false, error: 'slack_target_missing', status: 400 };
|
|
14
|
+
// A U... id is a user, not a conversation: open the DM first.
|
|
15
|
+
if (target.targetId.toUpperCase().startsWith('U')) {
|
|
16
|
+
const dm = await resolveSlackDmChannel(client.token, target.targetId);
|
|
17
|
+
if (!dm.ok || !dm.channelId) {
|
|
18
|
+
return { ok: false, error: dm.error || 'dm_open_failed', status: 502 };
|
|
19
|
+
}
|
|
20
|
+
target = slackTargetFromId(dm.channelId, target.threadId ? { threadTs: target.threadId } : {});
|
|
21
|
+
}
|
|
22
|
+
switch (req.type) {
|
|
23
|
+
case 'text':
|
|
24
|
+
// Slack's inline-keyboard analogue is Block Kit, whose callbacks need
|
|
25
|
+
// the interactive-envelope routing that v1 puts out of scope. Sending
|
|
26
|
+
// the text is better than silently dropping the message.
|
|
27
|
+
case 'keyboard':
|
|
28
|
+
if (!req.text)
|
|
29
|
+
return { ok: false, error: 'empty_text', status: 400 };
|
|
30
|
+
return sendSlackText(client.token, target, req.text);
|
|
31
|
+
case 'photo':
|
|
32
|
+
case 'document':
|
|
33
|
+
case 'voice':
|
|
34
|
+
if (!req.filePath)
|
|
35
|
+
return { ok: false, error: 'missing_file_path', status: 400 };
|
|
36
|
+
return sendSlackFile(client.token, target, req.filePath, req.caption ? { caption: req.caption } : {});
|
|
37
|
+
default:
|
|
38
|
+
return { ok: false, error: `unsupported_outbound_type_${String(req.type)}`, status: 400 };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=send-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send-handler.js","sourceRoot":"","sources":["../../../src/slack/send-handler.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,qEAAqE;AAGrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAClC,GAAuB;IAEvB,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAC;IACpC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,IAAI,mBAAmB,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;IACpG,CAAC;IACD,IAAI,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IAE9E,8DAA8D;IAC9D,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAChD,MAAM,EAAE,GAAG,MAAM,qBAAqB,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QACtE,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC;YAC1B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,gBAAgB,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAC3E,CAAC;QACD,MAAM,GAAG,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACnG,CAAC;IAED,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,MAAM,CAAC;QACZ,sEAAsE;QACtE,sEAAsE;QACtE,yDAAyD;QACzD,KAAK,UAAU;YACX,IAAI,CAAC,GAAG,CAAC,IAAI;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;YACtE,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QACzD,KAAK,OAAO,CAAC;QACb,KAAK,UAAU,CAAC;QAChB,KAAK,OAAO;YACR,IAAI,CAAC,GAAG,CAAC,QAAQ;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;YACjF,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1G;YACI,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,6BAA6B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IAClG,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ─── Slack Send-Only Client ──────────────────────────
|
|
2
|
+
// Outbound path that works with just the bot token, independent of whether the
|
|
3
|
+
// Socket Mode inbound connection is up. Mirrors src/discord/send-only-client.ts.
|
|
4
|
+
import { settings } from '../core/config.js';
|
|
5
|
+
import { slackApi, describeSlackError, slackFailure } from './api.js';
|
|
6
|
+
import { chunkSlackMessage, toMrkdwn } from './format.js';
|
|
7
|
+
export function invalidateSlackSendClient() {
|
|
8
|
+
// no-op: the token is read fresh from settings on every call.
|
|
9
|
+
// Present so runtime-settings.ts can invalidate symmetrically with Discord.
|
|
10
|
+
}
|
|
11
|
+
export function getSlackSendClient() {
|
|
12
|
+
const sc = settings["slack"];
|
|
13
|
+
if (!sc?.enabled)
|
|
14
|
+
return { token: null, reason: 'slack_disabled', status: 503 };
|
|
15
|
+
const token = typeof sc.botToken === 'string' ? sc.botToken.trim() : '';
|
|
16
|
+
if (!token)
|
|
17
|
+
return { token: null, reason: 'slack_bot_token_missing', status: 503 };
|
|
18
|
+
return { token };
|
|
19
|
+
}
|
|
20
|
+
/** Resolve a DM conversation id for a user id, opening the DM if needed. */
|
|
21
|
+
export async function resolveSlackDmChannel(token, userId, fetchImpl) {
|
|
22
|
+
if (!userId.toUpperCase().startsWith('U'))
|
|
23
|
+
return { ok: true, channelId: userId };
|
|
24
|
+
const result = await slackApi(token, 'conversations.open', { users: userId }, fetchImpl ? { fetchImpl } : {});
|
|
25
|
+
const channelId = result.data?.channel?.id;
|
|
26
|
+
if (!result.ok || !channelId) {
|
|
27
|
+
return { ok: false, error: describeSlackError(result.error || 'conversations_open_failed') };
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, channelId };
|
|
30
|
+
}
|
|
31
|
+
export async function sendSlackText(token, target, text, options = {}) {
|
|
32
|
+
const chunks = chunkSlackMessage(toMrkdwn(text));
|
|
33
|
+
for (const chunk of chunks) {
|
|
34
|
+
const result = await slackApi(token, 'chat.postMessage', {
|
|
35
|
+
channel: target.targetId,
|
|
36
|
+
text: chunk,
|
|
37
|
+
// thread_ts is the PARENT ts (see slack-target.resolveSlackThreadTs)
|
|
38
|
+
...(target.threadId ? { thread_ts: target.threadId } : {}),
|
|
39
|
+
}, options.fetchImpl ? { fetchImpl: options.fetchImpl } : {});
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
return slackFailure(describeSlackError(result.error), result.status);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { ok: true };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=send-only-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send-only-client.js","sourceRoot":"","sources":["../../../src/slack/send-only-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// ─── Slack File Upload ───────────────────────────────
|
|
2
|
+
// files.upload was SUNSET on 2025-11-12. The supported flow is:
|
|
3
|
+
// 1. files.getUploadURLExternal -> { upload_url, file_id }
|
|
4
|
+
// 2. POST the bytes to upload_url (multipart, NOT a Slack API method:
|
|
5
|
+
// no Authorization header, no ok:false envelope)
|
|
6
|
+
// 3. files.completeUploadExternal -> attaches file to a conversation
|
|
7
|
+
// Source: docs.slack.dev/changelog/2024-04-a-better-way-to-upload-files-is-here-to-stay
|
|
8
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
9
|
+
import { basename } from 'node:path';
|
|
10
|
+
import { slackApi, describeSlackError, redactSlackTokens, slackFailure } from './api.js';
|
|
11
|
+
import { redactOutboundText } from '../messaging/redact.js';
|
|
12
|
+
// Slack's per-file ceiling is 1 GB, but a chat transport has no business
|
|
13
|
+
// streaming that. 50 MiB matches the inbound attachment cap the Discord
|
|
14
|
+
// transport already enforces.
|
|
15
|
+
export const SLACK_FILE_LIMIT = 50 * 1024 * 1024;
|
|
16
|
+
export function validateSlackFileSize(size) {
|
|
17
|
+
if (size > SLACK_FILE_LIMIT) {
|
|
18
|
+
throw Object.assign(new Error(`File exceeds Slack transport limit: ${(size / 1024 / 1024).toFixed(1)} MiB (max 50 MiB)`), { statusCode: 413 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function sendSlackFile(token, target, filePath, options = {}) {
|
|
22
|
+
const doFetch = options.fetchImpl || fetch;
|
|
23
|
+
let fileStat;
|
|
24
|
+
try {
|
|
25
|
+
fileStat = await stat(filePath);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { ok: false, error: `File not found: ${filePath}`, status: 400 };
|
|
29
|
+
}
|
|
30
|
+
validateSlackFileSize(fileStat.size);
|
|
31
|
+
if (fileStat.size === 0) {
|
|
32
|
+
// Slack rejects a zero-length reservation with `missing_argument`,
|
|
33
|
+
// which reads as a client bug. Fail locally with something actionable.
|
|
34
|
+
return slackFailure('Cannot upload an empty file to Slack', 400);
|
|
35
|
+
}
|
|
36
|
+
const filename = basename(filePath);
|
|
37
|
+
// Step 1 — reserve an upload URL (POST, form-encoded per Slack docs).
|
|
38
|
+
const reserve = await slackApi(token, 'files.getUploadURLExternal', { filename, length: fileStat.size }, { fetchImpl: doFetch, form: true });
|
|
39
|
+
const uploadUrl = reserve.data?.upload_url;
|
|
40
|
+
const fileId = reserve.data?.file_id;
|
|
41
|
+
if (!reserve.ok || !uploadUrl || !fileId) {
|
|
42
|
+
return slackFailure(describeSlackError(reserve.error || 'upload_url_missing'), reserve.status);
|
|
43
|
+
}
|
|
44
|
+
// Step 2 — POST the bytes to the returned URL.
|
|
45
|
+
try {
|
|
46
|
+
const buffer = await readFile(filePath);
|
|
47
|
+
const form = new FormData();
|
|
48
|
+
form.append('file', new Blob([new Uint8Array(buffer)]), filename);
|
|
49
|
+
const upload = await doFetch(uploadUrl, { method: 'POST', body: form });
|
|
50
|
+
if (!upload.ok) {
|
|
51
|
+
return { ok: false, error: `Slack upload failed (${upload.status})`, status: upload.status };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// The presigned upload URL is a temporary capability: a thrown fetch
|
|
56
|
+
// error routinely embeds it, and this string reaches both API responses
|
|
57
|
+
// and the image-relay log.
|
|
58
|
+
return slackFailure(redactSlackTokens(error.message), 502);
|
|
59
|
+
}
|
|
60
|
+
// Step 3 — attach it to the conversation (thread-aware).
|
|
61
|
+
const complete = await slackApi(token, 'files.completeUploadExternal', {
|
|
62
|
+
files: [{ id: fileId, title: filename }],
|
|
63
|
+
channel_id: target.targetId,
|
|
64
|
+
...(target.threadId ? { thread_ts: target.threadId } : {}),
|
|
65
|
+
...(options.caption?.trim() ? { initial_comment: redactOutboundText(options.caption.trim()) } : {}),
|
|
66
|
+
}, { fetchImpl: doFetch });
|
|
67
|
+
if (!complete.ok) {
|
|
68
|
+
return slackFailure(describeSlackError(complete.error), complete.status);
|
|
69
|
+
}
|
|
70
|
+
return { ok: true };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=slack-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-file.js","sourceRoot":"","sources":["../../../src/slack/slack-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// ─── Slack Socket Mode Client ────────────────────────
|
|
2
|
+
// Protocol (docs.slack.dev/apis/events-api/using-socket-mode):
|
|
3
|
+
// 1. POST apps.connections.open with the app-level token -> wss:// URL
|
|
4
|
+
// 2. Connect; Slack sends {"type":"hello"} when it is ready
|
|
5
|
+
// 3. Each delivery is an envelope; reply {"envelope_id": "<id>"} within 3s
|
|
6
|
+
// 4. Slack recycles sockets every few hours and warns ~10s before closing
|
|
7
|
+
//
|
|
8
|
+
// Design rules this file enforces:
|
|
9
|
+
// - ACK BEFORE WORK. Slack retries un-acked envelopes, so acking after the
|
|
10
|
+
// agent run is how one message becomes three agent runs.
|
|
11
|
+
// - Envelope-id dedupe. Retries carry the same envelope_id.
|
|
12
|
+
// - Frames arriving while NOT connected are left UN-acked on purpose: that
|
|
13
|
+
// is what makes Slack redeliver them on the new socket. Acking then
|
|
14
|
+
// dropping would be permanent message loss.
|
|
15
|
+
// - link_disabled is terminal, not transient: do not reconnect into a wall.
|
|
16
|
+
import { log } from '../core/logger.js';
|
|
17
|
+
import { slackApi, redactSlackTokens } from './api.js';
|
|
18
|
+
const HANDLED_ENVELOPE_TYPES = new Set(['events_api', 'slash_commands', 'interactive']);
|
|
19
|
+
const CONTROL_FRAME_TYPES = new Set(['hello', 'disconnect']);
|
|
20
|
+
/**
|
|
21
|
+
* Retry dedupe memory. Slack retries an un-acked delivery for a bounded
|
|
22
|
+
* period, so entries expire by TIME rather than by count: a count-only window
|
|
23
|
+
* lets a busy channel evict an id before its retry arrives, which recreates
|
|
24
|
+
* the duplicate-agent-run failure this dedupe exists to prevent.
|
|
25
|
+
*/
|
|
26
|
+
const DEDUPE_TTL_MS = 10 * 60 * 1000;
|
|
27
|
+
/**
|
|
28
|
+
* Soft ceiling that triggers expiry sweeps. It deliberately does NOT evict
|
|
29
|
+
* unexpired ids: dropping one before its retry horizon reopens the duplicate-
|
|
30
|
+
* agent-run hole this map exists to close. Memory stays bounded by the TTL
|
|
31
|
+
* instead — a workspace would have to exceed 30k distinct deliveries within
|
|
32
|
+
* the window to grow past this, and each entry is a short id plus a number.
|
|
33
|
+
*/
|
|
34
|
+
const DEDUPE_SWEEP_AT = 5000;
|
|
35
|
+
/** Slack sends `hello` promptly; without it the socket is not usable. */
|
|
36
|
+
const HELLO_DEADLINE_MS = 15000;
|
|
37
|
+
export class SlackSocketClient {
|
|
38
|
+
options;
|
|
39
|
+
ws = null;
|
|
40
|
+
state = 'disconnected';
|
|
41
|
+
reconnectAttempts = 0;
|
|
42
|
+
reconnectTimer = null;
|
|
43
|
+
helloTimer = null;
|
|
44
|
+
/** Set when a reconnect is requested while a connect is already running. */
|
|
45
|
+
reconnectPending = false;
|
|
46
|
+
seenEnvelopes = new Map();
|
|
47
|
+
stopped = false;
|
|
48
|
+
connecting = false;
|
|
49
|
+
maxReconnectAttempts;
|
|
50
|
+
baseReconnectDelayMs;
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this.options = options;
|
|
53
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
|
|
54
|
+
this.baseReconnectDelayMs = options.baseReconnectDelayMs ?? 1000;
|
|
55
|
+
}
|
|
56
|
+
getState() { return this.state; }
|
|
57
|
+
getReconnectAttempts() { return this.reconnectAttempts; }
|
|
58
|
+
async start() {
|
|
59
|
+
this.stopped = false;
|
|
60
|
+
await this.connect();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Stop the client. `terminalState` preserves WHY it stopped so health and
|
|
64
|
+
* diagnostics can distinguish "Socket Mode is off in app settings" from a
|
|
65
|
+
* generic disconnect.
|
|
66
|
+
*/
|
|
67
|
+
stop(terminalState = 'disconnected') {
|
|
68
|
+
this.stopped = true;
|
|
69
|
+
this.clearReconnectTimer();
|
|
70
|
+
this.clearHelloTimer();
|
|
71
|
+
this.state = terminalState;
|
|
72
|
+
try {
|
|
73
|
+
this.ws?.close();
|
|
74
|
+
}
|
|
75
|
+
catch { /* already closing */ }
|
|
76
|
+
this.ws = null;
|
|
77
|
+
}
|
|
78
|
+
clearReconnectTimer() {
|
|
79
|
+
// Clearing before scheduling prevents timer leaks on rapid disconnects.
|
|
80
|
+
if (this.reconnectTimer) {
|
|
81
|
+
clearTimeout(this.reconnectTimer);
|
|
82
|
+
this.reconnectTimer = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
clearHelloTimer() {
|
|
86
|
+
if (this.helloTimer) {
|
|
87
|
+
clearTimeout(this.helloTimer);
|
|
88
|
+
this.helloTimer = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
defaultSocketFactory(url) {
|
|
92
|
+
// Node 22+ ships a global WebSocket, which is why this transport needs
|
|
93
|
+
// no SDK. See devlog/_plan/260802_slack_channel/000_plan.md D-1.
|
|
94
|
+
const socket = new WebSocket(url);
|
|
95
|
+
return {
|
|
96
|
+
send: (data) => socket.send(data),
|
|
97
|
+
close: () => socket.close(),
|
|
98
|
+
addEventListener: (type, listener) => socket.addEventListener(type, listener),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async connect() {
|
|
102
|
+
if (this.stopped)
|
|
103
|
+
return;
|
|
104
|
+
// Single-flight: a second concurrent connect would overwrite this.ws
|
|
105
|
+
// and orphan a live socket against Slack's connection cap.
|
|
106
|
+
if (this.connecting)
|
|
107
|
+
return;
|
|
108
|
+
this.connecting = true;
|
|
109
|
+
try {
|
|
110
|
+
await this.connectOnce();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this.connecting = false;
|
|
114
|
+
// A reconnect requested DURING this attempt was deferred so it
|
|
115
|
+
// could not stack; drain it now, or a failed
|
|
116
|
+
// apps.connections.open would stall the transport forever.
|
|
117
|
+
if (this.reconnectPending) {
|
|
118
|
+
this.reconnectPending = false;
|
|
119
|
+
this.scheduleReconnect();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async connectOnce() {
|
|
124
|
+
this.state = this.reconnectAttempts > 0 ? 'reconnecting' : 'connecting';
|
|
125
|
+
// The socket being replaced must stop influencing lifecycle decisions
|
|
126
|
+
// the moment a replacement is under way: its close event would
|
|
127
|
+
// otherwise schedule a second reconnect on top of this one.
|
|
128
|
+
const superseded = this.ws;
|
|
129
|
+
this.ws = null;
|
|
130
|
+
if (superseded) {
|
|
131
|
+
try {
|
|
132
|
+
superseded.close();
|
|
133
|
+
}
|
|
134
|
+
catch { /* already closing */ }
|
|
135
|
+
}
|
|
136
|
+
const opened = await slackApi(this.options.appToken, 'apps.connections.open', undefined, this.options.fetchImpl ? { fetchImpl: this.options.fetchImpl } : {});
|
|
137
|
+
const url = opened.data?.url;
|
|
138
|
+
if (!opened.ok || !url) {
|
|
139
|
+
log.warn('[slack:socket] apps.connections.open failed:', opened.error || 'no url');
|
|
140
|
+
this.scheduleReconnect();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const factory = this.options.socketFactory ?? ((u) => this.defaultSocketFactory(u));
|
|
144
|
+
const ws = factory(url);
|
|
145
|
+
// A stopped client must not adopt a socket that opened while the
|
|
146
|
+
// handshake was in flight.
|
|
147
|
+
if (this.stopped) {
|
|
148
|
+
try {
|
|
149
|
+
ws.close();
|
|
150
|
+
}
|
|
151
|
+
catch { /* already closing */ }
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.ws = ws;
|
|
155
|
+
// Slack signals readiness with `hello`. If it never arrives the socket
|
|
156
|
+
// is useless and inbound would stall forever, because frames are
|
|
157
|
+
// deliberately left un-acked while not connected.
|
|
158
|
+
this.clearHelloTimer();
|
|
159
|
+
this.helloTimer = setTimeout(() => {
|
|
160
|
+
this.helloTimer = null;
|
|
161
|
+
if (this.stopped || this.ws !== ws || this.state === 'connected')
|
|
162
|
+
return;
|
|
163
|
+
log.warn(`[slack:socket] no hello within ${HELLO_DEADLINE_MS}ms — recycling socket`);
|
|
164
|
+
try {
|
|
165
|
+
ws.close();
|
|
166
|
+
}
|
|
167
|
+
catch { /* already closing */ }
|
|
168
|
+
this.ws = null;
|
|
169
|
+
this.scheduleReconnect();
|
|
170
|
+
}, HELLO_DEADLINE_MS);
|
|
171
|
+
// Every listener below checks that IT still owns the live socket.
|
|
172
|
+
// Slack recycles sockets, and a superseded socket's late `close` would
|
|
173
|
+
// otherwise schedule an extra reconnect, compounding into a storm of
|
|
174
|
+
// parallel connections.
|
|
175
|
+
const isCurrent = () => this.ws === ws;
|
|
176
|
+
ws.addEventListener('open', () => {
|
|
177
|
+
if (!isCurrent())
|
|
178
|
+
return;
|
|
179
|
+
// 'open' means the WS handshake finished, not that Slack accepted
|
|
180
|
+
// us. Slack's readiness signal is the `hello` frame.
|
|
181
|
+
log.info('[slack:socket] websocket open, awaiting hello');
|
|
182
|
+
});
|
|
183
|
+
ws.addEventListener('message', (event) => {
|
|
184
|
+
if (!isCurrent())
|
|
185
|
+
return;
|
|
186
|
+
const data = event?.data;
|
|
187
|
+
void this.handleFrame(typeof data === 'string' ? data : String(data));
|
|
188
|
+
});
|
|
189
|
+
ws.addEventListener('error', (event) => {
|
|
190
|
+
if (!isCurrent())
|
|
191
|
+
return;
|
|
192
|
+
const message = event?.message;
|
|
193
|
+
log.warn('[slack:socket] error', redactSlackTokens(String(message ?? 'socket error')));
|
|
194
|
+
});
|
|
195
|
+
ws.addEventListener('close', () => {
|
|
196
|
+
if (!isCurrent() || this.stopped || this.state === 'disabled')
|
|
197
|
+
return;
|
|
198
|
+
// Detach so a repeated close from this same dead socket cannot
|
|
199
|
+
// schedule a second reconnect.
|
|
200
|
+
this.ws = null;
|
|
201
|
+
this.clearHelloTimer();
|
|
202
|
+
log.info('[slack:socket] closed, scheduling reconnect');
|
|
203
|
+
this.scheduleReconnect();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async handleFrame(raw) {
|
|
207
|
+
let envelope;
|
|
208
|
+
try {
|
|
209
|
+
envelope = JSON.parse(raw);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
log.warn('[slack:socket] unparseable frame dropped');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!envelope || typeof envelope.type !== 'string')
|
|
216
|
+
return;
|
|
217
|
+
if (CONTROL_FRAME_TYPES.has(envelope.type)) {
|
|
218
|
+
if (envelope.type === 'hello') {
|
|
219
|
+
this.state = 'connected';
|
|
220
|
+
this.reconnectAttempts = 0;
|
|
221
|
+
this.clearHelloTimer();
|
|
222
|
+
log.info('[slack:socket] hello received, connected');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// 'link_disabled' means Socket Mode was turned off in app settings.
|
|
226
|
+
// Reconnecting would burn attempts against a closed door.
|
|
227
|
+
if (envelope.reason === 'link_disabled') {
|
|
228
|
+
log.warn('[slack:socket] link_disabled — Socket Mode is off in app settings');
|
|
229
|
+
this.stop('disabled');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
log.info(`[slack:socket] disconnect (${envelope.reason || 'unspecified'}), reconnecting`);
|
|
233
|
+
this.scheduleReconnect();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Reconnect-window guard — BEFORE the ack. Leaving the envelope
|
|
237
|
+
// un-acked is what makes Slack redeliver it on the new connection.
|
|
238
|
+
if (this.state !== 'connected') {
|
|
239
|
+
log.info(`[slack:socket] frame left un-acked while state=${this.state} (Slack will redeliver)`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// ACK FIRST — before any work, always within the 3s deadline. Any
|
|
243
|
+
// valid envelope is acked, including types we do not handle, so Slack
|
|
244
|
+
// stops retrying payloads we will never act on.
|
|
245
|
+
if (envelope.envelope_id) {
|
|
246
|
+
if (!this.ack(envelope.envelope_id)) {
|
|
247
|
+
// The ack did not reach Slack, so this delivery WILL be
|
|
248
|
+
// retried. Running the agent now would duplicate that work.
|
|
249
|
+
log.warn('[slack:socket] ack failed — skipping dispatch, awaiting Slack retry');
|
|
250
|
+
this.recycleSocket();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (this.isDuplicate(envelope.envelope_id)) {
|
|
254
|
+
log.info(`[slack:socket] duplicate envelope ignored (retry_attempt=${envelope.retry_attempt ?? 0})`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!HANDLED_ENVELOPE_TYPES.has(envelope.type))
|
|
259
|
+
return;
|
|
260
|
+
try {
|
|
261
|
+
await this.options.onEnvelope(envelope);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
log.error('[slack:socket] handler error', redactSlackTokens(error.message));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** @returns true when the ack was handed to the socket successfully. */
|
|
268
|
+
ack(envelopeId) {
|
|
269
|
+
const socket = this.ws;
|
|
270
|
+
if (!socket)
|
|
271
|
+
return false;
|
|
272
|
+
try {
|
|
273
|
+
socket.send(JSON.stringify({ envelope_id: envelopeId }));
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
log.warn('[slack:socket] ack failed', error.message);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Drop the current socket and reconnect — used when an ack cannot be sent. */
|
|
282
|
+
recycleSocket() {
|
|
283
|
+
if (this.stopped || this.state === 'disabled')
|
|
284
|
+
return;
|
|
285
|
+
const socket = this.ws;
|
|
286
|
+
this.ws = null;
|
|
287
|
+
this.clearHelloTimer();
|
|
288
|
+
try {
|
|
289
|
+
socket?.close();
|
|
290
|
+
}
|
|
291
|
+
catch { /* already closing */ }
|
|
292
|
+
this.scheduleReconnect();
|
|
293
|
+
}
|
|
294
|
+
isDuplicate(envelopeId) {
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
const seenAt = this.seenEnvelopes.get(envelopeId);
|
|
297
|
+
if (seenAt !== undefined && now - seenAt < DEDUPE_TTL_MS)
|
|
298
|
+
return true;
|
|
299
|
+
this.seenEnvelopes.set(envelopeId, now);
|
|
300
|
+
// Lazy sweep of EXPIRED entries only. Evicting an unexpired id to hit
|
|
301
|
+
// a size target would let a busy workspace reprocess a delayed retry,
|
|
302
|
+
// which is precisely the failure this dedupe prevents.
|
|
303
|
+
if (this.seenEnvelopes.size > DEDUPE_SWEEP_AT) {
|
|
304
|
+
for (const [id, at] of this.seenEnvelopes) {
|
|
305
|
+
if (now - at >= DEDUPE_TTL_MS)
|
|
306
|
+
this.seenEnvelopes.delete(id);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
scheduleReconnect() {
|
|
312
|
+
if (this.stopped || this.state === 'disabled')
|
|
313
|
+
return;
|
|
314
|
+
// An already-pending reconnect wins; a second trigger (e.g. a
|
|
315
|
+
// `disconnect` frame immediately followed by `close`) must not stack.
|
|
316
|
+
if (this.reconnectTimer)
|
|
317
|
+
return;
|
|
318
|
+
// A connect already in flight cannot be stacked on, but the demand
|
|
319
|
+
// must not be dropped either: record it and let connect()'s finally
|
|
320
|
+
// drain it once the attempt settles.
|
|
321
|
+
if (this.connecting) {
|
|
322
|
+
this.reconnectPending = true;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
326
|
+
log.error(`[slack:socket] max reconnect attempts (${this.maxReconnectAttempts}) reached — giving up`);
|
|
327
|
+
this.state = 'disconnected';
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.clearReconnectTimer();
|
|
331
|
+
const delay = Math.min(this.baseReconnectDelayMs * 2 ** this.reconnectAttempts, 60000);
|
|
332
|
+
this.reconnectAttempts++;
|
|
333
|
+
this.state = 'reconnecting';
|
|
334
|
+
log.info(`[slack:socket] reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
335
|
+
this.reconnectTimer = setTimeout(() => {
|
|
336
|
+
this.reconnectTimer = null;
|
|
337
|
+
void this.connect();
|
|
338
|
+
}, delay);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../../../src/slack/socket.ts"],"names":[],"mappings":"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"}
|