@meetquinn/animactl 0.1.26 → 0.1.27

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.
@@ -8,12 +8,10 @@ export interface SlackIngestInput {
8
8
  envelope?: SlackMessageEnvelope;
9
9
  event: RoutableSlackMessage;
10
10
  profiles?: SlackProfileResolver;
11
+ previewClient?: (signal: AbortSignal) => Pick<WebClient, 'conversations'>;
12
+ previewTimeoutMs?: number;
13
+ previewRetryDelaysMs?: readonly number[];
11
14
  warn?: (message: string) => void;
12
15
  }
13
- export interface SlackInboxBuildResult {
14
- item: SlackInboxItem;
15
- latePreview?: (item: SlackInboxItem) => Promise<SlackInboxItem | undefined>;
16
- }
17
16
  export declare function buildSlackInboxItem(input: SlackIngestInput): Promise<SlackInboxItem>;
18
- export declare function buildSlackInboxItemWithLatePreview(input: SlackIngestInput): Promise<SlackInboxBuildResult>;
19
17
  //# sourceMappingURL=slack-ingest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slack-ingest.d.ts","sourceRoot":"","sources":["../../../server/inbox/slack-ingest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAShD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,gBAAgB;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,KAAK,EAAE,oBAAoB,CAAC;IAC5B,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,cAAc,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;CAC7E;AAQD,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAE1F;AAED,wBAAsB,kCAAkC,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA8ChH"}
1
+ {"version":3,"file":"slack-ingest.d.ts","sourceRoot":"","sources":["../../../server/inbox/slack-ingest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAQhD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,gBAAgB;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,KAAK,EAAE,oBAAoB,CAAC;IAC5B,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AASD,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CA0C1F"}
@@ -1,18 +1,16 @@
1
1
  import { errorMessage } from '../ids.js';
2
- import { attachmentsHaveSlackMessagePreviews, slackMessagePreviewsFromAttachments, slackMessageAttachments, waitForSlackMessagePreviewAttachments, } from '../slack/message-previews.js';
2
+ import { attachmentsHaveSlackMessagePreviews, slackPermalinkMentioned, waitForSlackMessagePreviewAttachments, } from '../slack/message-previews.js';
3
3
  import { SlackProfileResolver } from '../slack/profiles.js';
4
4
  import { slackVisibleMessageText } from '../slack/message-text.js';
5
5
  import { normalizeSlackMessage, slackEventTeamId, } from './slack-events.js';
6
6
  // Turns one routable Slack event into a fully enriched inbox item: sender and
7
7
  // channel profiles, readable mention text, permalink, unfurl previews, and file
8
- // metadata. Every Slack lookup is best-effort a failure degrades that field
9
- // and never blocks the wake. Privacy boundary: the only message-content read is
8
+ // metadata. Every Slack lookup is best-effort; a failure degrades that field
9
+ // without dropping the wake. Slack-link preview reads finish (or exhaust a
10
+ // bounded budget) BEFORE enqueue. Privacy boundary: the only message-content read is
10
11
  // the containing message itself (for late unfurls); linked channels and DMs are
11
12
  // never fetched, previews come only from what Slack attached to this event.
12
13
  export async function buildSlackInboxItem(input) {
13
- return (await buildSlackInboxItemWithLatePreview(input)).item;
14
- }
15
- export async function buildSlackInboxItemWithLatePreview(input) {
16
14
  const warn = input.warn ?? ((message) => console.warn(message));
17
15
  const profiles = input.profiles ?? new SlackProfileResolver();
18
16
  const client = input.client;
@@ -26,7 +24,18 @@ export async function buildSlackInboxItemWithLatePreview(input) {
26
24
  profiles.conversation({ channelId: event.channel, client, teamId }),
27
25
  profiles.displayText({ client, teamId, text: event.text }),
28
26
  slackPermalink(event, client, warn),
29
- slackFastUnfurlAttachments(event, client, warn),
27
+ attachmentsHaveSlackMessagePreviews(event.attachments)
28
+ ? Promise.resolve(event.attachments)
29
+ : waitForSlackMessagePreviewAttachments({
30
+ channelId: event.channel,
31
+ client,
32
+ createClient: input.previewClient,
33
+ messageTs: event.ts,
34
+ retryDelaysMs: input.previewRetryDelaysMs,
35
+ timeoutMs: input.previewTimeoutMs,
36
+ text: event.text,
37
+ warn,
38
+ }),
30
39
  ]);
31
40
  const item = normalizeSlackMessage({
32
41
  ...(attachments ? { attachments } : {}),
@@ -38,24 +47,9 @@ export async function buildSlackInboxItemWithLatePreview(input) {
38
47
  text,
39
48
  ...(userProfile ? { userProfile } : {}),
40
49
  });
41
- if (attachmentsHaveSlackMessagePreviews(attachments))
42
- return { item };
43
- return {
44
- item,
45
- latePreview: async (queuedItem) => {
46
- const delayedAttachments = await waitForSlackMessagePreviewAttachments({
47
- channelId: event.channel,
48
- client,
49
- messageTs: event.ts,
50
- text: event.text,
51
- warn,
52
- });
53
- const previews = slackMessagePreviewsFromAttachments(delayedAttachments);
54
- if (!previews.length)
55
- return undefined;
56
- return { ...queuedItem, previews };
57
- },
58
- };
50
+ if (slackPermalinkMentioned(event.text) && !item.previews?.length)
51
+ item.previewStatus = 'unavailable';
52
+ return item;
59
53
  }
60
54
  async function slackPermalink(event, client, warn) {
61
55
  try {
@@ -70,17 +64,3 @@ async function slackPermalink(event, client, warn) {
70
64
  return undefined;
71
65
  }
72
66
  }
73
- // Unfurl attachments for the containing message: prefer what the realtime event
74
- // carried; otherwise do one immediate re-read of the containing message. The
75
- // delayed retry ladder runs after enqueue.
76
- async function slackFastUnfurlAttachments(event, client, warn) {
77
- return event.attachments?.length
78
- ? event.attachments
79
- : slackMessageAttachments({
80
- channelId: event.channel,
81
- client,
82
- messageTs: event.ts,
83
- text: event.text,
84
- warn,
85
- });
86
- }
@@ -1,4 +1,3 @@
1
- import type { SlackInboxItem } from '../../shared/inbox.js';
2
1
  import { WakeQueueService } from './wake-queue.service.js';
3
2
  export interface SlackInboxSubscriberOptions {
4
3
  agentRuntimeKind: string;
@@ -25,9 +24,4 @@ export declare class SlackInboxSubscriber {
25
24
  private handleSlackEvent;
26
25
  private maybeSyncBotDisplayInfo;
27
26
  }
28
- export declare function applyLateSlackPreviewToQueuedItem(input: {
29
- item: SlackInboxItem;
30
- latePreview: (item: SlackInboxItem) => Promise<SlackInboxItem | undefined>;
31
- queue: Pick<WakeQueueService, 'replaceQueuedItem'>;
32
- }): Promise<void>;
33
27
  //# sourceMappingURL=slack-subscriber.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slack-subscriber.d.ts","sourceRoot":"","sources":["../../../server/inbox/slack-subscriber.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAoC5D,OAAO,EAAE,gBAAgB,EAA+B,MAAM,yBAAyB,CAAC;AAExF,MAAM,WAAW,2BAA2B;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED,qBAAa,oBAAoB;IAQnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAM;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuB;IACvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA8B;IAG5D,OAAO,CAAC,0BAA0B,CAAS;gBAEd,OAAO,EAAE,2BAA2B;IAO3D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B,OAAO,CAAC,SAAS;YAgFH,oBAAoB;YASpB,qBAAqB;YAOrB,0BAA0B;YA2C1B,kCAAkC;YAYlC,gBAAgB;IA2E9B,OAAO,CAAC,uBAAuB;CAYhC;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE;IACvD,IAAI,EAAE,cAAc,CAAC;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAC3E,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,CAAC;CACpD,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB"}
1
+ {"version":3,"file":"slack-subscriber.d.ts","sourceRoot":"","sources":["../../../server/inbox/slack-subscriber.ts"],"names":[],"mappings":"AAuCA,OAAO,EAAE,gBAAgB,EAA+B,MAAM,yBAAyB,CAAC;AAExF,MAAM,WAAW,2BAA2B;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED,qBAAa,oBAAoB;IAQnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAM;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuB;IACvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA8B;IAG5D,OAAO,CAAC,0BAA0B,CAAS;gBAEd,OAAO,EAAE,2BAA2B;IAO3D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B,OAAO,CAAC,SAAS;YAgFH,oBAAoB;YASpB,qBAAqB;YAOrB,0BAA0B;YA2C1B,kCAAkC;YAYlC,gBAAgB;IAgE9B,OAAO,CAAC,uBAAuB;CAYhC"}
@@ -3,7 +3,7 @@ import { activityServiceForAgent } from '../activities/activity.service.js';
3
3
  import { agentSlackServiceForAgent } from '../agents/agent-slack.service.js';
4
4
  import { interactiveAskServiceForAgent } from '../asks/interactive-ask.service.js';
5
5
  import { errorMessage, slackMessageEventId } from '../ids.js';
6
- import { createSlackWebClient } from '../slack/client.js';
6
+ import { createSlackWebClient, createSlackPreviewWebClient } from '../slack/client.js';
7
7
  import { ResilientSocketModeReceiver } from '../slack/resilient-socket-mode-receiver.js';
8
8
  import { SlackShortcutService, userIdFromShortcutBody, } from '../slack-interactions/shortcut.service.js';
9
9
  import { SLACK_STOP_CONFIRM_VIEW_CALLBACK_ID, SLACK_VIEW_REMINDER_DETAIL_ACTION_ID, SLACK_VIEW_REMINDERS_ACTION_ID, } from '../slack-interactions/shortcut-ids.js';
@@ -14,7 +14,7 @@ import { isSlackEvent, isRoutableSlackMessage, slackEventTeamId, slackSurfaceFor
14
14
  import { slackAttentionSuggestionPayload, } from './attention-suggestion-activity.js';
15
15
  import { runIngestPipeline } from './ingest-pipeline.js';
16
16
  import { observeSlackEventAtIngress } from './observed-conversation.js';
17
- import { buildSlackInboxItemWithLatePreview } from './slack-ingest.js';
17
+ import { buildSlackInboxItem } from './slack-ingest.js';
18
18
  import { slackShortcutHandoffServiceForAgent } from './slack-shortcut-handoff.service.js';
19
19
  import { slackRuntimeDecision } from './slack-subscription.service.js';
20
20
  export class SlackInboxSubscriber {
@@ -206,7 +206,6 @@ export class SlackInboxSubscriber {
206
206
  if (!isRoutableSlackMessage(rawEvent))
207
207
  return;
208
208
  const teamId = slackEventTeamId(envelope, rawEvent);
209
- let latePreview;
210
209
  await runIngestPipeline({
211
210
  agentId: this.options.queue.agentId,
212
211
  attentionSuggestionPayload: slackAttentionSuggestionPayload,
@@ -218,25 +217,15 @@ export class SlackInboxSubscriber {
218
217
  enrich: async () => {
219
218
  const webClient = client ?? createSlackWebClient(this.options.botToken);
220
219
  this.maybeSyncBotDisplayInfo(webClient);
221
- const buildResult = await buildSlackInboxItemWithLatePreview({
220
+ return buildSlackInboxItem({
222
221
  client: webClient,
222
+ previewClient: (signal) => createSlackPreviewWebClient(this.options.botToken, signal),
223
223
  envelope,
224
224
  event: rawEvent,
225
225
  profiles: this.slackProfiles,
226
226
  });
227
- latePreview = buildResult.latePreview;
228
- return buildResult.item;
229
227
  },
230
228
  itemId: slackMessageEventId(teamId, rawEvent.channel, rawEvent.ts),
231
- onAfterEnqueue: ({ item, result }) => {
232
- if (result.queued && latePreview) {
233
- applyLateSlackPreviewToQueuedItem({
234
- item,
235
- latePreview,
236
- queue: this.options.queue,
237
- });
238
- }
239
- },
240
229
  onAfterAttentionSuggestion: ({ decision, item, result }) => {
241
230
  if (decision.reason === 'mention' && decision.subscription && !result.duplicate) {
242
231
  activityServiceForAgent(this.options.queue.agentId).record({
@@ -273,15 +262,6 @@ export class SlackInboxSubscriber {
273
262
  });
274
263
  }
275
264
  }
276
- export function applyLateSlackPreviewToQueuedItem(input) {
277
- return input.latePreview(input.item).then(async (updatedItem) => {
278
- if (!updatedItem)
279
- return;
280
- await input.queue.replaceQueuedItem(updatedItem);
281
- }).catch((error) => {
282
- console.warn(`Slack late preview update failed for ${input.item.id}: ${errorMessage(error)}`);
283
- });
284
- }
285
265
  // Refresh the bot's own Slack display info at most once every 6h while handling
286
266
  // messages — frequent enough to pick up an avatar/name change soon after it
287
267
  // happens, rare enough to add no meaningful Slack API load.
@@ -1 +1 @@
1
- {"version":3,"file":"delivery-prompt.d.ts","sourceRoot":"","sources":["../../../server/runtime/delivery-prompt.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAKV,SAAS,EAKT,cAAc,EACf,MAAM,uBAAuB,CAAC;AAY/B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AASzD,MAAM,WAAW,sBAAsB;IACrC,eAAe,CAAC,EAAE;QAChB,2BAA2B,CAAC,EAAE,MAAM,CAAC;QACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAwC3G;AAED,0FAA0F;AAC1F,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAIrE;AA4ND,wBAAgB,6CAA6C,CAAC,KAAK,EAAE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAWT;AAED,wBAAgB,oCAAoC,CAAC,KAAK,EAAE;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAWT;AAED,wBAAgB,qCAAqC,CAAC,KAAK,EAAE;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAcT;AAED,wBAAgB,yCAAyC,CAAC,KAAK,EAAE;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAcT;AAED,wBAAgB,0CAA0C,CAAC,KAAK,EAAE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAWT"}
1
+ {"version":3,"file":"delivery-prompt.d.ts","sourceRoot":"","sources":["../../../server/runtime/delivery-prompt.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAKV,SAAS,EAKT,cAAc,EACf,MAAM,uBAAuB,CAAC;AAY/B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AASzD,MAAM,WAAW,sBAAsB;IACrC,eAAe,CAAC,EAAE;QAChB,2BAA2B,CAAC,EAAE,MAAM,CAAC;QACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAwC3G;AAED,0FAA0F;AAC1F,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAIrE;AAkOD,wBAAgB,6CAA6C,CAAC,KAAK,EAAE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAWT;AAED,wBAAgB,oCAAoC,CAAC,KAAK,EAAE;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAWT;AAED,wBAAgB,qCAAqC,CAAC,KAAK,EAAE;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAcT;AAED,wBAAgB,yCAAyC,CAAC,KAAK,EAAE;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAcT;AAED,wBAAgB,0CAA0C,CAAC,KAAK,EAAE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAWT"}
@@ -74,7 +74,7 @@ export function buildCodeAgentDeliveryPrompt(event, context = {}) {
74
74
  }
75
75
  /** Render Slack unfurl previews + attached_files for inclusion in the cursor envelope. */
76
76
  export function renderSlackCursorExtras(event) {
77
- return [formatSlackMessagePreviews(event.previews), formatAttachedFiles(event.files)]
77
+ return [formatSlackPreviewContent(event), formatAttachedFiles(event.files)]
78
78
  .filter(Boolean)
79
79
  .join('\n\n');
80
80
  }
@@ -82,12 +82,17 @@ function buildSlackMessageDeliveryPrompt(event) {
82
82
  const envelope = `${messageEnvelope(event)} ${actorLabel(event)}: ${event.text}`;
83
83
  return buildDeliveryEventPrompt({
84
84
  attentionSuggestion: event.attentionSuggestion,
85
- body: formatSlackMessagePreviews(event.previews),
85
+ body: formatSlackPreviewContent(event),
86
86
  envelope,
87
87
  files: event.files,
88
88
  title: 'New Slack message',
89
89
  });
90
90
  }
91
+ function formatSlackPreviewContent(event) {
92
+ return event.previewStatus === 'unavailable' && !event.previews?.length
93
+ ? '<slack_message_previews status="unavailable">\nPreview not yet obtained within the bounded lookup. This does not establish that the shared message has no preview or that the linked channel is accessible. If needed, re-read the containing message using this envelope\'s channel and message timestamp; do not infer permission to read the linked target.\n</slack_message_previews>'
94
+ : formatSlackMessagePreviews(event.previews);
95
+ }
91
96
  function buildFeishuMessageDeliveryPrompt(event) {
92
97
  const envelope = event.quotedMessage
93
98
  ? `${feishuMessageEnvelope(event)} ${feishuActorLabel(event)}:\n${formatFeishuQuotedMessage(event.quotedMessage)}\n${event.text}`
@@ -1,3 +1,4 @@
1
1
  import { WebClient } from '@slack/web-api';
2
2
  export declare function createSlackWebClient(token: string): WebClient;
3
+ export declare function createSlackPreviewWebClient(token: string, signal: AbortSignal): WebClient;
3
4
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../server/slack/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyB,MAAM,gBAAgB,CAAC;AAElE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAE7D"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../server/slack/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyB,MAAM,gBAAgB,CAAC;AAElE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAE7D;AAID,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,CAQzF"}
@@ -2,6 +2,17 @@ import { WebClient } from '@slack/web-api';
2
2
  export function createSlackWebClient(token) {
3
3
  return new WebClient(token, slackWebClientOptions());
4
4
  }
5
+ // Preview enrichment has its own deadline. Never leave SDK retries or a 429
6
+ // backoff running after the original message has been delivered without it.
7
+ export function createSlackPreviewWebClient(token, signal) {
8
+ return new WebClient(token, {
9
+ ...slackWebClientOptions(),
10
+ timeout: 1_000,
11
+ retryConfig: { retries: 0 },
12
+ rejectRateLimitedCalls: true,
13
+ requestInterceptor: (config) => ({ ...config, signal }),
14
+ });
15
+ }
5
16
  function slackWebClientOptions() {
6
17
  return process.env.ANIMA_SLACK_API_URL ? { slackApiUrl: process.env.ANIMA_SLACK_API_URL } : {};
7
18
  }
@@ -5,8 +5,10 @@ type SlackPreviewWebClient = Pick<WebClient, 'conversations'>;
5
5
  export interface SlackMessagePreviewRetryInput {
6
6
  channelId: string;
7
7
  client: SlackPreviewWebClient;
8
+ createClient?: (signal: AbortSignal) => SlackPreviewWebClient;
8
9
  messageTs: string;
9
10
  retryDelaysMs?: readonly number[];
11
+ timeoutMs?: number;
10
12
  text?: string;
11
13
  sleep?: (ms: number) => Promise<void>;
12
14
  warn?: (message: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"message-previews.d.ts","sourceRoot":"","sources":["../../../server/slack/message-previews.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,KAAK,EAAE,mBAAmB,EAA2B,MAAM,uBAAuB,CAAC;AAS1F,wBAAgB,mCAAmC,CAAC,cAAc,EAAE,OAAO,GAAG,mBAAmB,EAAE,CAwDlG;AAuBD,KAAK,qBAAqB,GAAG,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;AAE9D,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAKD,wBAAsB,qCAAqC,CACzD,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,CAiBhC;AAID,wBAAsB,uBAAuB,CAAC,KAAK,EAAE;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,CAkBjC;AAED,wBAAgB,mCAAmC,CAAC,WAAW,EAAE,OAAO,GAAG,OAAO,CAEjF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEzE"}
1
+ {"version":3,"file":"message-previews.d.ts","sourceRoot":"","sources":["../../../server/slack/message-previews.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,KAAK,EAAE,mBAAmB,EAA2B,MAAM,uBAAuB,CAAC;AAS1F,wBAAgB,mCAAmC,CAAC,cAAc,EAAE,OAAO,GAAG,mBAAmB,EAAE,CAwDlG;AAyBD,KAAK,qBAAqB,GAAG,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;AAE9D,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,qBAAqB,CAAC;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAKD,wBAAsB,qCAAqC,CACzD,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,CAyChC;AAID,wBAAsB,uBAAuB,CAAC,KAAK,EAAE;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,CAkBjC;AAED,wBAAgB,mCAAmC,CAAC,WAAW,EAAE,OAAO,GAAG,OAAO,CAEjF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEzE"}
@@ -81,7 +81,9 @@ function slackMessagePreviewFiles(rawFiles) {
81
81
  }
82
82
  return files;
83
83
  }
84
- const SLACK_MESSAGE_PREVIEW_RETRY_DELAYS_MS = [2_000, 5_000];
84
+ // Immediate read, then retries at ~2s and ~7s; network time shares the 8s budget.
85
+ const SLACK_MESSAGE_PREVIEW_RETRY_DELAYS_MS = [0, 2_000, 5_000];
86
+ const SLACK_MESSAGE_PREVIEW_TIMEOUT_MS = 8_000;
85
87
  // Unfurl attachments for shared Slack permalinks are added by Slack after the
86
88
  // realtime event fires, so an immediate read can miss them. Re-read the
87
89
  // containing message a couple of times before giving up.
@@ -89,22 +91,48 @@ export async function waitForSlackMessagePreviewAttachments(input) {
89
91
  if (!slackPermalinkMentioned(input.text))
90
92
  return undefined;
91
93
  const retryDelaysMs = input.retryDelaysMs ?? SLACK_MESSAGE_PREVIEW_RETRY_DELAYS_MS;
92
- const sleep = input.sleep ?? sleepMs;
93
- for (const delayMs of retryDelaysMs) {
94
- if (delayMs > 0)
95
- await sleep(delayMs);
96
- const attachments = await slackMessageAttachments({
97
- channelId: input.channelId,
98
- client: input.client,
99
- messageTs: input.messageTs,
100
- text: input.text,
101
- warn: input.warn,
102
- });
103
- if (!attachmentsHaveSlackMessagePreviews(attachments))
104
- continue;
105
- return attachments;
94
+ const controller = new AbortController();
95
+ const startedAt = performance.now();
96
+ const timeoutMs = input.timeoutMs ?? SLACK_MESSAGE_PREVIEW_TIMEOUT_MS;
97
+ let timer;
98
+ let sleepTimer;
99
+ const expired = new Promise((resolve) => {
100
+ timer = setTimeout(() => {
101
+ controller.abort();
102
+ resolve(undefined);
103
+ }, timeoutMs);
104
+ });
105
+ const client = input.createClient?.(controller.signal) ?? input.client;
106
+ const lookup = async () => {
107
+ let nextAttemptAt = startedAt;
108
+ for (const delayMs of retryDelaysMs) {
109
+ nextAttemptAt += delayMs;
110
+ const waitMs = Math.max(0, nextAttemptAt - performance.now());
111
+ if (waitMs > 0) {
112
+ await (input.sleep?.(waitMs) ?? new Promise((resolve) => {
113
+ sleepTimer = setTimeout(resolve, waitMs);
114
+ }));
115
+ }
116
+ if (controller.signal.aborted || performance.now() - startedAt >= timeoutMs)
117
+ return undefined;
118
+ const attachments = await slackMessageAttachments({ ...input, client });
119
+ if (controller.signal.aborted)
120
+ return undefined;
121
+ if (attachmentsHaveSlackMessagePreviews(attachments))
122
+ return attachments;
123
+ }
124
+ return undefined;
125
+ };
126
+ try {
127
+ // Also bounds injected/non-cooperative clients. No late result mutates a
128
+ // queued item; the production transport is aborted at this same boundary.
129
+ return await Promise.race([lookup(), expired]);
130
+ }
131
+ finally {
132
+ clearTimeout(timer);
133
+ clearTimeout(sleepTimer);
134
+ controller.abort();
106
135
  }
107
- return undefined;
108
136
  }
109
137
  // Re-reads ONLY the containing message (the message that just arrived) to pick
110
138
  // up its Slack-provided unfurl attachments. Never fetches the linked target.
@@ -133,9 +161,6 @@ export function attachmentsHaveSlackMessagePreviews(attachments) {
133
161
  export function slackPermalinkMentioned(text) {
134
162
  return Boolean(text && /https:\/\/[^\s|>]+\.slack\.com\/archives\/[A-Z0-9]+\/p\d{10,}/.test(text));
135
163
  }
136
- function sleepMs(ms) {
137
- return new Promise((resolve) => setTimeout(resolve, ms));
138
- }
139
164
  function stringField(record, key) {
140
165
  const value = record[key];
141
166
  if (typeof value !== 'string')
@@ -86,6 +86,7 @@ export declare const WakeQueueFileSchema: z.ZodPreprocess<z.ZodObject<{
86
86
  messageTs: z.ZodOptional<z.ZodString>;
87
87
  text: z.ZodString;
88
88
  }, z.core.$strip>>>;
89
+ previewStatus: z.ZodOptional<z.ZodLiteral<"unavailable">>;
89
90
  teamId: z.ZodString;
90
91
  text: z.ZodString;
91
92
  threadTs: z.ZodOptional<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"wake-queue.store.d.ts","sourceRoot":"","sources":["../../../../server/storage/schema/wake-queue.store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wake-queue.store.d.ts","sourceRoot":"","sources":["../../../../server/storage/schema/wake-queue.store.ts"],"names":[],"mappings":"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"}
@@ -3,11 +3,17 @@ import assert from 'node:assert/strict';
3
3
  import { mkdtemp, rm } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { buildSlackInboxItem, buildSlackInboxItemWithLatePreview } from '../inbox/slack-ingest.js';
7
- import { applyLateSlackPreviewToQueuedItem } from '../inbox/slack-subscriber.js';
6
+ import { createServer } from 'node:http';
7
+ import { buildSlackInboxItem } from '../inbox/slack-ingest.js';
8
+ import { runIngestPipeline } from '../inbox/ingest-pipeline.js';
9
+ import { SlackInboxItem } from '../../shared/inbox.js';
10
+ import { buildCodeAgentDeliveryPrompt, renderSlackCursorExtras } from '../runtime/delivery-prompt.js';
8
11
  import { WakeQueueService } from '../inbox/wake-queue.service.js';
9
12
  import { slackMessageContentForText } from '../tools/slack-message-format.js';
10
13
  import { withAnimaHome } from './anima-home.js';
14
+ import { withTempAnimaHome, writeAgentConfigs, withTimeout } from './helpers/harness.js';
15
+ import { SlackInboxSubscriber } from '../inbox/slack-subscriber.js';
16
+ import { SlackProfileResolver } from '../slack/profiles.js';
11
17
  function fakeIngestClient(input) {
12
18
  return {
13
19
  chat: {
@@ -390,109 +396,182 @@ test('buildSlackInboxItem re-reads only the containing message for late unfurls,
390
396
  }
391
397
  });
392
398
  });
393
- test('Slack ingest enqueue does not wait for the delayed unfurl retry ladder', async () => {
399
+ const sharedLink = 'https://demo.slack.com/archives/C0PRIVATE1/p1770000100000001';
400
+ const linkEvent = {
401
+ attachments: [{ fallback: 'ordinary attachment must not suppress lookup' }],
402
+ channel: 'D-owner',
403
+ channel_type: 'im',
404
+ text: `<${sharedLink}> can you see this?`,
405
+ ts: '1770000300.000001',
406
+ type: 'message',
407
+ user: 'UALICE1',
408
+ };
409
+ const previewAttachment = {
410
+ channel_id: 'C0PRIVATE1', from_url: sharedLink, is_msg_unfurl: true,
411
+ text: 'Late shared preview', ts: '1770000100.000001',
412
+ };
413
+ test('ingest holds enqueue until preview completes, then an immediate claim contains it; duplicates stay deduped', { timeout: 3_000 }, async () => {
394
414
  await withIngestHome(async () => {
395
- const link = 'https://demo.slack.com/archives/C0PRIVATE1/p1770000100000001';
396
415
  const calls = emptyCalls();
397
416
  const client = fakeIngestClient({ calls });
398
- client.conversations.history = async () => new Promise(() => { });
399
- const result = await buildSlackInboxItemWithLatePreview({
400
- client,
401
- envelope: { team_id: 'T-ingest' },
402
- event: {
403
- attachments: [{ fallback: 'ordinary attachment' }],
404
- channel: 'D-owner',
405
- channel_type: 'im',
406
- text: `<${link}> can you see this?`,
407
- ts: '1770000300.000001',
408
- type: 'message',
409
- user: 'UALICE1',
417
+ let finishLookup;
418
+ const held = new Promise((resolve) => { finishLookup = resolve; });
419
+ let lookupStarted;
420
+ const started = new Promise((resolve) => { lookupStarted = resolve; });
421
+ client.conversations.history = async (args) => {
422
+ calls.history.push(args);
423
+ if (calls.history.length === 1)
424
+ return { ok: true, messages: [] };
425
+ lookupStarted();
426
+ await held;
427
+ return { ok: true, messages: [{ ts: linkEvent.ts, attachments: [previewAttachment] }] };
428
+ };
429
+ const queue = new WakeQueueService('anima');
430
+ let enrichCount = 0;
431
+ const ingest = () => runIngestPipeline({
432
+ agentId: 'anima',
433
+ itemId: 'slack:T-ingest:D-owner:' + linkEvent.ts,
434
+ decide: async ({ duplicate }) => ({ shouldStartRuntime: !duplicate, reason: 'dm' }),
435
+ enrich: async () => {
436
+ enrichCount++;
437
+ return buildSlackInboxItem({
438
+ client, envelope: { team_id: 'T-ingest' }, event: linkEvent,
439
+ previewRetryDelaysMs: [0, 0], warn: () => { },
440
+ });
410
441
  },
411
- warn: () => { },
442
+ queue,
443
+ surfaceLog: () => ({}),
412
444
  });
413
- assert.ok(result.latePreview);
445
+ const pending = ingest();
446
+ await Promise.race([
447
+ started,
448
+ pending.then(() => { throw new Error('wake was published before preview retry'); }),
449
+ ]);
450
+ assert.equal(await queue.takeNextRunnable({ isWorkerAlive: () => true, workerId: 'worker-1' }), undefined);
451
+ finishLookup();
452
+ await pending;
453
+ const claimed = await queue.takeNextRunnable({ isWorkerAlive: () => true, workerId: 'worker-1' });
454
+ assert.equal(claimed?.kind, 'slack');
455
+ if (claimed?.kind !== 'slack')
456
+ throw new Error('missing Slack item');
457
+ assert.equal(claimed.previews?.[0]?.text, 'Late shared preview');
458
+ assert.equal(claimed.previewStatus, undefined);
459
+ assert.match(buildCodeAgentDeliveryPrompt(claimed), /Late shared preview/);
460
+ await ingest();
461
+ assert.equal(enrichCount, 1);
462
+ assert.equal(await queue.takeNextRunnable({ isWorkerAlive: () => true, workerId: 'worker-2' }), undefined);
463
+ assert.equal(calls.history.length, 2);
464
+ for (const call of calls.history) {
465
+ assert.equal(call.channel, 'D-owner');
466
+ assert.equal(call.latest, linkEvent.ts);
467
+ assert.equal(call.oldest, linkEvent.ts);
468
+ }
469
+ });
470
+ });
471
+ test('subscriber waits for the production preview client before publishing a claimable wake', async () => {
472
+ await withTempAnimaHome(async (home) => {
473
+ await writeAgentConfigs(home);
474
+ let finish;
475
+ let started;
476
+ const lookupStarted = new Promise((resolve) => { started = resolve; });
477
+ const requests = [];
478
+ const server = createServer((request, response) => {
479
+ let body = '';
480
+ request.on('data', (chunk) => { body += String(chunk); });
481
+ request.on('end', () => {
482
+ requests.push({ url: request.url, body });
483
+ finish = () => {
484
+ if (!response.writableEnded)
485
+ response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({
486
+ ok: true, messages: [{ ts: linkEvent.ts, attachments: [previewAttachment] }],
487
+ }));
488
+ };
489
+ started();
490
+ });
491
+ });
492
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
493
+ const previousUrl = process.env.ANIMA_SLACK_API_URL;
494
+ process.env.ANIMA_SLACK_API_URL = `http://127.0.0.1:${server.address().port}/api/`;
414
495
  const queue = new WakeQueueService('anima');
415
- const decision = await queue.enqueue(result.item);
416
- assert.equal(decision.queued, true);
417
- assert.equal((await queue.find(result.item.id))?.id, result.item.id);
496
+ const subscriber = Object.create(SlackInboxSubscriber.prototype);
497
+ subscriber['options'] = { agentRuntimeKind: 'codex-cli', botToken: 'test-only-token', botUserId: 'U-bot', queue };
498
+ subscriber['slackProfiles'] = new SlackProfileResolver();
499
+ subscriber['botDisplayInfoSyncInFlight'] = true;
500
+ const handle = () => subscriber.handleSlackEvent({ team_id: 'T-ingest' }, linkEvent, fakeIngestClient({ calls: emptyCalls() }));
501
+ let pending;
502
+ try {
503
+ pending = handle();
504
+ await withTimeout(lookupStarted, 1_000);
505
+ assert.equal(await queue.takeNextRunnable({ isWorkerAlive: () => true, workerId: 'fast-worker' }), undefined);
506
+ finish();
507
+ await pending;
508
+ const claimed = await queue.takeNextRunnable({ isWorkerAlive: () => true, workerId: 'fast-worker' });
509
+ assert.equal(claimed?.kind, 'slack');
510
+ if (claimed?.kind === 'slack')
511
+ assert.equal(claimed.previews?.[0]?.text, 'Late shared preview');
512
+ assert.equal(requests.length, 1);
513
+ assert.equal(requests[0]?.url, '/api/conversations.history');
514
+ const body = new URLSearchParams(requests[0]?.body);
515
+ assert.equal(body.get('channel'), 'D-owner');
516
+ assert.equal(body.get('oldest'), linkEvent.ts);
517
+ assert.equal(body.get('latest'), linkEvent.ts);
518
+ }
519
+ finally {
520
+ finish?.();
521
+ // Join ingress before deleting its scoped home, including on assertion failure.
522
+ await pending;
523
+ if (previousUrl === undefined)
524
+ delete process.env.ANIMA_SLACK_API_URL;
525
+ else
526
+ process.env.ANIMA_SLACK_API_URL = previousUrl;
527
+ server.closeAllConnections();
528
+ await new Promise((resolve) => server.close(() => resolve()));
529
+ }
418
530
  });
419
531
  });
420
- test('late Slack previews replace queued items and leave claimed items untouched', async () => {
532
+ test('hanging preview request times out before enqueue; late results cannot mutate the delivered item', async () => {
421
533
  await withIngestHome(async () => {
422
- const queuedQueue = new WakeQueueService('anima');
423
- const queuedItem = await buildSlackInboxItem({
424
- client: fakeIngestClient({ calls: emptyCalls() }),
425
- envelope: { team_id: 'T-ingest' },
426
- event: {
427
- channel: 'D-owner',
428
- channel_type: 'im',
429
- text: 'queued preview',
430
- ts: '1770000400.000001',
431
- type: 'message',
432
- user: 'UALICE1',
433
- },
434
- warn: () => { },
435
- });
436
- await queuedQueue.enqueue(queuedItem);
437
- await applyLateSlackPreviewToQueuedItem({
438
- item: queuedItem,
439
- latePreview: async (item) => ({
440
- ...item,
441
- previews: [{ text: 'late preview while queued' }],
442
- }),
443
- queue: queuedQueue,
444
- });
445
- const updatedQueuedItem = await queuedQueue.find(queuedItem.id);
446
- assert.equal(updatedQueuedItem?.kind, 'slack');
447
- assert.deepEqual(updatedQueuedItem.previews, [{ text: 'late preview while queued' }]);
448
- await queuedQueue.complete(queuedItem.id);
449
- const claimedQueue = new WakeQueueService('anima');
450
- let runningItemId;
451
- const claimedItem = await buildSlackInboxItem({
452
- client: fakeIngestClient({ calls: emptyCalls() }),
453
- envelope: { team_id: 'T-ingest' },
454
- event: {
455
- channel: 'D-owner',
456
- channel_type: 'im',
457
- text: 'claimed preview',
458
- ts: '1770000500.000001',
459
- type: 'message',
460
- user: 'UALICE1',
461
- },
462
- warn: () => { },
463
- });
464
- await claimedQueue.enqueue(claimedItem);
465
- await applyLateSlackPreviewToQueuedItem({
466
- item: claimedItem,
467
- latePreview: async (item) => ({
468
- ...item,
469
- previews: [{ text: 'late preview after claim' }],
470
- }),
471
- queue: {
472
- replaceQueuedItem: async (item) => {
473
- const running = await claimedQueue.takeNextRunnable({
474
- isWorkerAlive: () => true,
475
- workerId: 'worker-1',
476
- });
477
- runningItemId = running?.id;
478
- return claimedQueue.replaceQueuedItem(item);
479
- },
480
- },
481
- });
482
- assert.equal(runningItemId, claimedItem.id);
483
- const updatedClaimedItem = await claimedQueue.find(claimedItem.id);
484
- assert.equal(updatedClaimedItem?.kind, 'slack');
485
- assert.equal(updatedClaimedItem.handling.status, 'running');
486
- assert.equal(updatedClaimedItem.previews, undefined);
487
- await claimedQueue.complete(claimedItem.id);
488
- await applyLateSlackPreviewToQueuedItem({
489
- item: claimedItem,
490
- latePreview: async (item) => ({
491
- ...item,
492
- previews: [{ text: 'late preview after settle' }],
493
- }),
494
- queue: claimedQueue,
534
+ const client = fakeIngestClient({ calls: emptyCalls() });
535
+ let resolveLookup;
536
+ client.conversations.history = (() => new Promise((resolve) => { resolveLookup = resolve; }));
537
+ const startedAt = performance.now();
538
+ const item = await buildSlackInboxItem({
539
+ client, envelope: { team_id: 'T-ingest' }, event: linkEvent,
540
+ previewTimeoutMs: 30, previewRetryDelaysMs: [0, 0], warn: () => { },
495
541
  });
496
- assert.equal(await claimedQueue.find(claimedItem.id), undefined);
542
+ assert.ok(performance.now() - startedAt < 1_000);
543
+ assert.equal(item.previewStatus, 'unavailable');
544
+ assert.equal(item.text, linkEvent.text);
545
+ const parsed = SlackInboxItem.parse(item);
546
+ assert.equal(parsed.previewStatus, 'unavailable');
547
+ assert.match(buildCodeAgentDeliveryPrompt(parsed), /Preview not yet obtained/);
548
+ assert.match(renderSlackCursorExtras(parsed), /status="unavailable"/);
549
+ const queue = new WakeQueueService('anima');
550
+ assert.equal((await queue.enqueue(item)).queued, true);
551
+ resolveLookup({ ok: true, messages: [{ ts: linkEvent.ts, attachments: [previewAttachment] }] });
552
+ await new Promise((resolve) => setImmediate(resolve));
553
+ const claimed = await queue.takeNextRunnable({ isWorkerAlive: () => true, workerId: 'worker-1' });
554
+ assert.equal(claimed?.kind, 'slack');
555
+ if (claimed?.kind === 'slack') {
556
+ assert.equal(claimed.previewStatus, 'unavailable');
557
+ assert.equal(claimed.previews, undefined);
558
+ }
559
+ });
560
+ });
561
+ test('ordinary messages and already attached previews do not create a preview client or wait', async () => {
562
+ await withIngestHome(async () => {
563
+ for (const event of [
564
+ { ...linkEvent, text: 'plain message', attachments: undefined },
565
+ { ...linkEvent, attachments: [previewAttachment] },
566
+ ]) {
567
+ const calls = emptyCalls();
568
+ const item = await buildSlackInboxItem({
569
+ client: fakeIngestClient({ calls }), event,
570
+ previewClient: () => { throw new Error('unexpected preview client'); },
571
+ warn: () => { },
572
+ });
573
+ assert.deepEqual(calls.history, []);
574
+ assert.equal(item.previewStatus, undefined);
575
+ }
497
576
  });
498
577
  });
@@ -1,5 +1,7 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import { createServer } from 'node:http';
4
+ import { createSlackPreviewWebClient } from '../slack/client.js';
3
5
  import { waitForSlackMessagePreviewAttachments } from '../slack/message-previews.js';
4
6
  import { normalizeSlackMessage } from '../inbox/slack-events.js';
5
7
  test('delayed Slack preview wait returns containing-message unfurl attachments before enqueue', async () => {
@@ -78,3 +80,98 @@ test('delayed Slack preview wait skips messages without Slack permalinks', async
78
80
  assert.equal(attachments, undefined);
79
81
  assert.equal(calls, 0);
80
82
  });
83
+ test('preview budget bounds a hung initial read and aborts its client without another retry', async () => {
84
+ let calls = 0;
85
+ let signal;
86
+ const client = {
87
+ conversations: { history: async () => { calls++; return new Promise(() => { }); } },
88
+ };
89
+ const start = performance.now();
90
+ assert.equal(await waitForSlackMessagePreviewAttachments({
91
+ channelId: 'D-containing', messageTs: '1782810200.000001', client,
92
+ createClient: (value) => { signal = value; return client; },
93
+ text: 'https://demo.slack.com/archives/C0PRIVATE1/p1770000100000001',
94
+ timeoutMs: 30, retryDelaysMs: [0, 0, 0],
95
+ }), undefined);
96
+ assert.ok(performance.now() - start < 1_000);
97
+ assert.equal(signal?.aborted, true);
98
+ assert.equal(calls, 1);
99
+ });
100
+ test('preview budget includes retry sleeps and leaves no later lookup', async () => {
101
+ let calls = 0;
102
+ const client = {
103
+ conversations: { history: async () => { calls++; return { messages: [] }; } },
104
+ };
105
+ assert.equal(await waitForSlackMessagePreviewAttachments({
106
+ channelId: 'D-containing', messageTs: '1782810200.000001', client,
107
+ text: 'https://demo.slack.com/archives/C0PRIVATE1/p1770000100000001',
108
+ timeoutMs: 20, retryDelaysMs: [0, 40],
109
+ }), undefined);
110
+ await new Promise((resolve) => setTimeout(resolve, 60));
111
+ assert.equal(calls, 1);
112
+ });
113
+ test('lookup errors degrade without losing the message or reading a target channel', async () => {
114
+ const channels = [];
115
+ const client = {
116
+ conversations: { history: async (args) => {
117
+ channels.push(args.channel);
118
+ throw new Error('channel_not_found');
119
+ } },
120
+ };
121
+ assert.equal(await waitForSlackMessagePreviewAttachments({
122
+ channelId: 'D-containing', messageTs: '1782810200.000001', client,
123
+ text: 'https://demo.slack.com/archives/C0PRIVATE1/p1770000100000001',
124
+ retryDelaysMs: [0, 0, 0], warn: () => { },
125
+ }), undefined);
126
+ assert.deepEqual(channels, ['D-containing', 'D-containing', 'D-containing']);
127
+ });
128
+ test('production preview transport aborts hanging HTTP and rejects rate-limit backoff', async () => {
129
+ let requests = 0;
130
+ let mode = 'hang';
131
+ let closed;
132
+ const socketClosed = new Promise((resolve) => { closed = resolve; });
133
+ const server = createServer((_request, response) => {
134
+ requests++;
135
+ if (mode === 'hang')
136
+ response.on('close', closed);
137
+ else
138
+ response.writeHead(429, { 'retry-after': '60' }).end();
139
+ });
140
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
141
+ const previousUrl = process.env.ANIMA_SLACK_API_URL;
142
+ process.env.ANIMA_SLACK_API_URL = `http://127.0.0.1:${server.address().port}/api/`;
143
+ const unusedClient = {};
144
+ const input = {
145
+ client: unusedClient,
146
+ createClient: (signal) => createSlackPreviewWebClient('test-only-token', signal),
147
+ channelId: 'D-containing', messageTs: '1782810200.000001',
148
+ text: 'https://demo.slack.com/archives/C0PRIVATE1/p1770000100000001',
149
+ retryDelaysMs: [0], timeoutMs: 200, warn: () => { },
150
+ };
151
+ try {
152
+ assert.equal(await waitForSlackMessagePreviewAttachments(input), undefined);
153
+ let timer;
154
+ try {
155
+ await Promise.race([socketClosed, new Promise((_, reject) => {
156
+ timer = setTimeout(() => reject(new Error('preview socket was not aborted')), 500);
157
+ })]);
158
+ }
159
+ finally {
160
+ clearTimeout(timer);
161
+ }
162
+ assert.equal(requests, 1);
163
+ mode = 'rate-limit';
164
+ const start = performance.now();
165
+ assert.equal(await waitForSlackMessagePreviewAttachments({ ...input, timeoutMs: 1_000 }), undefined);
166
+ assert.ok(performance.now() - start < 800, '429 must not wait for SDK backoff or total deadline');
167
+ assert.equal(requests, 2);
168
+ }
169
+ finally {
170
+ if (previousUrl === undefined)
171
+ delete process.env.ANIMA_SLACK_API_URL;
172
+ else
173
+ process.env.ANIMA_SLACK_API_URL = previousUrl;
174
+ server.closeAllConnections();
175
+ await new Promise((resolve) => server.close(() => resolve()));
176
+ }
177
+ });
@@ -182,6 +182,7 @@ export declare const SlackInboxItem: z.ZodObject<{
182
182
  messageTs: z.ZodOptional<z.ZodString>;
183
183
  text: z.ZodString;
184
184
  }, z.core.$strip>>>;
185
+ previewStatus: z.ZodOptional<z.ZodLiteral<"unavailable">>;
185
186
  teamId: z.ZodString;
186
187
  text: z.ZodString;
187
188
  threadTs: z.ZodOptional<z.ZodString>;
@@ -553,6 +554,7 @@ export declare const InboxItemSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
553
554
  messageTs: z.ZodOptional<z.ZodString>;
554
555
  text: z.ZodString;
555
556
  }, z.core.$strip>>>;
557
+ previewStatus: z.ZodOptional<z.ZodLiteral<"unavailable">>;
556
558
  teamId: z.ZodString;
557
559
  text: z.ZodString;
558
560
  threadTs: z.ZodOptional<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../shared/inbox.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../shared/inbox.ts"],"names":[],"mappings":"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"}
@@ -96,6 +96,8 @@ export const SlackInboxItem = InboxItemBase.extend({
96
96
  messageTs: z.string(),
97
97
  permalink: z.string().optional(),
98
98
  previews: z.array(SlackMessagePreview).optional(),
99
+ /** Bounded lookup did not obtain a preview; not proof that none exists. */
100
+ previewStatus: z.literal('unavailable').optional(),
99
101
  teamId: z.string(),
100
102
  text: z.string(),
101
103
  threadTs: z.string().optional(),
@@ -39,7 +39,7 @@ Slack event filtering and normalization live in `server/inbox/slack-events.ts`.
39
39
 
40
40
  `server/inbox/slack-subscriber.ts` calls `isRoutableSlackMessage` before doing any queue work. For routable events, it derives a stable item id with `slackMessageEventId` from `server/ids.ts`.
41
41
 
42
- `server/inbox/slack-ingest.ts` turns the Slack event into a `SlackInboxItem` from `shared/inbox.ts`. `buildSlackInboxItemWithLatePreview` adds sender profile data, channel profile data, readable mention text, permalink, file metadata, and Slack unfurl previews. If Slack has not attached message previews yet, the returned `latePreview` callback updates the queued item after enqueue with `WakeQueueService.replaceQueuedItem`.
42
+ `server/inbox/slack-ingest.ts` turns the Slack event into a `SlackInboxItem` from `shared/inbox.ts`. `buildSlackInboxItem` adds sender profile data, channel profile data, readable mention text, permalink, file metadata, and Slack unfurl previews. Messages containing Slack permalinks wait for preview enrichment before enqueue: an immediate read and retries around 2 and 7 seconds share an 8-second total preview budget. Existing previews and ordinary messages skip that wait. Reads target only the containing message, never the linked channel or DM. The preview client disables SDK retries and rate-limit backoff, bounds each request, and aborts at the budget boundary. If no preview is obtained, the original wake proceeds with `previewStatus=unavailable`; both delivery paths explain that this is not evidence of an absent preview or permission to read the linked target. No post-enqueue callback mutates a claimed item or starts a second wake.
43
43
 
44
44
  ### 3. Ingest decision pipeline
45
45
 
@@ -52,7 +52,7 @@ The shared decision skeleton is `server/inbox/ingest-pipeline.ts`.
52
52
  3. `enrich`, which builds the final inbox item;
53
53
  4. tag the item with `attentionSuggestion` and parsed `wakeReason`;
54
54
  5. `enqueue` through `WakeQueueService`;
55
- 6. optional post-enqueue effects such as late previews and subscription activity;
55
+ 6. optional post-enqueue effects such as subscription activity;
56
56
  7. optional attention-suggestion activity;
57
57
  8. structured ingest log output.
58
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetquinn/animactl",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "license": "Apache-2.0",
5
5
  "private": false,
6
6
  "description": "Operator CLI for installing and running Anima.",