@unblocklabs/slack-subagent-card 1.0.2 → 1.0.3

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/index.ts CHANGED
@@ -156,8 +156,11 @@ export default definePluginEntry({
156
156
 
157
157
  const web = createSlackWebClient(token);
158
158
 
159
+ log.info(`slack-subagent-card: registering hooks (registrationMode=${pluginApi.registrationMode}, typeof api.on=${typeof pluginApi.on})`);
160
+
159
161
  pluginApi.on("subagent_spawned", async (event, ctx) => {
160
162
  try {
163
+ log.info(`slack-subagent-card: subagent_spawned fired — runId=${(event as SubagentSpawnedEvent).runId ?? ctx.runId} requesterSessionKey=${ctx.requesterSessionKey} threadRequested=${(event as SubagentSpawnedEvent).threadRequested} requester=${JSON.stringify((event as SubagentSpawnedEvent).requester)}`);
161
164
  await handleSpawned(pluginApi, web, shared, event as SubagentSpawnedEvent, ctx);
162
165
  } catch (error) {
163
166
  log.warn(`slack-subagent-card: subagent_spawned failed: ${stringifyError(error)}`);
@@ -166,6 +169,7 @@ export default definePluginEntry({
166
169
 
167
170
  pluginApi.on("subagent_ended", async (event, ctx) => {
168
171
  try {
172
+ log.info(`slack-subagent-card: subagent_ended fired — runId=${(event as SubagentEndedEvent).runId ?? ctx.runId} outcome=${(event as SubagentEndedEvent).outcome}`);
169
173
  await handleEnded(pluginApi, web, shared, event as SubagentEndedEvent, ctx);
170
174
  } catch (error) {
171
175
  log.warn(`slack-subagent-card: subagent_ended failed: ${stringifyError(error)}`);
@@ -174,6 +178,7 @@ export default definePluginEntry({
174
178
 
175
179
  pluginApi.on("subagent_delivery_target", async (event, ctx) => {
176
180
  try {
181
+ log.info(`slack-subagent-card: subagent_delivery_target fired — runId=${(event as SubagentDeliveryTargetEvent).childRunId ?? ctx.runId}, updating card early`);
177
182
  await handleDeliveryTarget(pluginApi, web, shared, event as SubagentDeliveryTargetEvent, ctx);
178
183
  } catch (error) {
179
184
  log.warn(`slack-subagent-card: subagent_delivery_target failed: ${stringifyError(error)}`);
@@ -244,7 +249,7 @@ async function handleSpawned(
244
249
 
245
250
  cleanupStaleRuns(shared, api.logger);
246
251
 
247
- const target = resolveSlackThreadTarget(requesterSessionKey, event.requester);
252
+ const target = await resolveSlackThreadTarget(requesterSessionKey, event.requester, web, api.logger);
248
253
  if (!target) {
249
254
  api.logger.debug?.(
250
255
  `slack-subagent-card: no Slack thread target for runId=${runId} requesterSessionKey=${requesterSessionKey}`,
@@ -263,10 +268,10 @@ async function handleSpawned(
263
268
  web.chat.postMessage({
264
269
  channel: target.channelId,
265
270
  thread_ts: target.threadTs,
266
- text: `${CARD_TEXT_PREFIX}${cardTitle}: Running`,
271
+ text: `${CARD_TEXT_PREFIX}${cardTitle}: SubAgent Running`,
267
272
  blocks: buildBlocks({
268
273
  label: cardTitle,
269
- statusText: "⏳ Running",
274
+ statusText: "⏳ SubAgent Running",
270
275
  taskId: runId,
271
276
  }) as any,
272
277
  }),
@@ -574,21 +579,36 @@ function normalizeOutcome(value: unknown): Outcome {
574
579
  return value === "error" || value === "timeout" || value === "killed" ? value : "ok";
575
580
  }
576
581
 
577
- function resolveSlackThreadTarget(
582
+ async function resolveSlackThreadTarget(
578
583
  requesterSessionKey: string,
579
- requester?: SlackRequester,
580
- ): SlackThreadTarget | null {
584
+ requester: SlackRequester | undefined,
585
+ web: WebClient,
586
+ log: Logger,
587
+ ): Promise<SlackThreadTarget | null> {
581
588
  const fromSession = parseSlackThreadSessionKey(requesterSessionKey);
582
- if (fromSession) return fromSession;
589
+ if (fromSession) {
590
+ // For DM sessions the captured ID is a user ID (U...), not a DM channel (D...).
591
+ // Slack's chat.postMessage needs the D-prefixed channel ID for thread replies.
592
+ if (/^U/i.test(fromSession.channelId)) {
593
+ const resolved = await resolveUserToDmChannel(web, fromSession.channelId, log);
594
+ if (!resolved) return null;
595
+ return { channelId: resolved, threadTs: fromSession.threadTs };
596
+ }
597
+ return fromSession;
598
+ }
583
599
 
584
600
  const rawTarget = asNonEmptyString(requester?.to);
585
601
  const threadTs = asNonEmptyString(requester?.threadId);
586
602
  if (!rawTarget || !threadTs) return null;
587
603
 
588
- return {
589
- channelId: normalizeSlackChannelId(stripSlackTargetPrefix(rawTarget)),
590
- threadTs,
591
- };
604
+ let channelId = normalizeSlackChannelId(stripSlackTargetPrefix(rawTarget));
605
+ if (/^U/i.test(channelId)) {
606
+ const resolved = await resolveUserToDmChannel(web, channelId, log);
607
+ if (!resolved) return null;
608
+ channelId = resolved;
609
+ }
610
+
611
+ return { channelId, threadTs };
592
612
  }
593
613
 
594
614
  function parseSlackThreadSessionKey(sessionKey: string): SlackThreadTarget | null {
@@ -737,6 +757,37 @@ function normalizeSlackChannelId(channelId: string): string {
737
757
  return /^[cgdu]/i.test(channelId) ? channelId.toUpperCase() : channelId;
738
758
  }
739
759
 
760
+ /** Cache of userId → DM channelId to avoid repeated conversations.open calls */
761
+ const dmChannelCache = new Map<string, string>();
762
+
763
+ async function resolveUserToDmChannel(
764
+ web: WebClient,
765
+ userId: string,
766
+ log: Logger,
767
+ ): Promise<string | undefined> {
768
+ const normalized = userId.toUpperCase();
769
+ const cached = dmChannelCache.get(normalized);
770
+ if (cached) return cached;
771
+
772
+ try {
773
+ const result = await withSlackRetry(
774
+ () => web.conversations.open({ users: normalized, return_im: true }),
775
+ log,
776
+ );
777
+ const channelId = asNonEmptyString((result as any)?.channel?.id);
778
+ if (channelId) {
779
+ dmChannelCache.set(normalized, channelId);
780
+ log.info(`slack-subagent-card: resolved DM channel for ${normalized} → ${channelId}`);
781
+ return channelId;
782
+ }
783
+ log.warn(`slack-subagent-card: conversations.open returned no channel for ${normalized}`);
784
+ return undefined;
785
+ } catch (error) {
786
+ log.warn(`slack-subagent-card: failed to resolve DM channel for ${normalized}: ${stringifyError(error)}`);
787
+ return undefined;
788
+ }
789
+ }
790
+
740
791
  function cleanupStaleRuns(shared: SharedState, log: Logger): void {
741
792
  const cutoff = Date.now() - STALE_RUN_TTL_MS;
742
793
  for (const [runId, tracked] of shared.runs.entries()) {
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "id": "slack-subagent-card",
3
3
  "name": "Slack Subagent Card",
4
- "version": "1.0.2",
4
+ "version": "1.0.3",
5
5
  "description": "Posts and updates a Slack Block Kit status card for thread-bound sub-agents, nudges quiet parent threads after completion, wakes parent sessions, and writes completion markers.",
6
- "entry": "index.ts",
7
6
  "configSchema": {
8
7
  "type": "object",
9
8
  "properties": {
10
9
  "nudgeDelaySec": {
11
- "type": "number",
10
+ "type": "integer",
12
11
  "minimum": 10,
13
12
  "maximum": 300,
14
13
  "default": 30,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unblocklabs/slack-subagent-card",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "OpenClaw plugin: Slack Block Kit status cards for sub-agents, nudge timer, parent session wake, and completion markers.",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -20,7 +20,7 @@
20
20
  "author": "Unblock Labs",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/unblocklabs-ai/openclaw-slack-subagent-card"
23
+ "url": "https://github.com/bill492/slack-subagent-card"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=22"