@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.
- package/dist/server/inbox/slack-ingest.d.ts +3 -5
- package/dist/server/inbox/slack-ingest.d.ts.map +1 -1
- package/dist/server/inbox/slack-ingest.js +19 -39
- package/dist/server/inbox/slack-subscriber.d.ts +0 -6
- package/dist/server/inbox/slack-subscriber.d.ts.map +1 -1
- package/dist/server/inbox/slack-subscriber.js +4 -24
- package/dist/server/runtime/delivery-prompt.d.ts.map +1 -1
- package/dist/server/runtime/delivery-prompt.js +7 -2
- package/dist/server/slack/client.d.ts +1 -0
- package/dist/server/slack/client.d.ts.map +1 -1
- package/dist/server/slack/client.js +11 -0
- package/dist/server/slack/message-previews.d.ts +2 -0
- package/dist/server/slack/message-previews.d.ts.map +1 -1
- package/dist/server/slack/message-previews.js +44 -19
- package/dist/server/storage/schema/wake-queue.store.d.ts +1 -0
- package/dist/server/storage/schema/wake-queue.store.d.ts.map +1 -1
- package/dist/server/tests/slack-ingest.test.js +175 -96
- package/dist/server/tests/slack-message-previews.test.js +97 -0
- package/dist/shared/inbox.d.ts +2 -0
- package/dist/shared/inbox.d.ts.map +1 -1
- package/dist/shared/inbox.js +2 -0
- package/docs/architecture/internals.md +2 -2
- package/package.json +1 -1
|
@@ -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;
|
|
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,
|
|
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
|
|
9
|
-
//
|
|
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
|
-
|
|
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 (
|
|
42
|
-
|
|
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":"
|
|
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 {
|
|
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
|
-
|
|
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;
|
|
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 [
|
|
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:
|
|
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 +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;
|
|
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
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,0BAA0B,CAAC;AAUlC,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC3C;AAED,eAAO,MAAM,sBAAsB,QAA2B,CAAC;AAC/D,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAa1C,eAAO,MAAM,mBAAmB
|
|
1
|
+
{"version":3,"file":"wake-queue.store.d.ts","sourceRoot":"","sources":["../../../../server/storage/schema/wake-queue.store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,0BAA0B,CAAC;AAUlC,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC3C;AAED,eAAO,MAAM,sBAAsB,QAA2B,CAAC;AAC/D,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAa1C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAM/B,CAAC;AA2DF,eAAO,MAAM,qBAAqB,GAAI,SAAS,MAAM,KAAG,SAAS,CAAC,aAAa,CAK3E,CAAC;AAEL,UAAU,wBAAwB;IAChC,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACxG;AAED,MAAM,WAAW,qBAAqB;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,SAAS,EAAE,CAAC;CACxB;AAED,qBAAa,cAAc;IAEvB,QAAQ,CAAC,OAAO,EAAE,MAAM;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADb,OAAO,EAAE,MAAM,EACP,KAAK,GAAE,wBAAyD;IAG7E,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAI1D,kEAAkE;IAC5D,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrC,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,CAAC;IAoBvF;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAenE;;;;OAIG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAKpE;;;;OAIG;IACG,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAwBlE;;;OAGG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAoB7D,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IAShD,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAepD,IAAI,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAK5B,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAiD/E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzC;;;;OAIG;IACG,oBAAoB,CACxB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,KAAK,SAAS,GAC1D,OAAO,CAAC,0BAA0B,CAAC;IAkChC,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAI9D,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAI1D,eAAe,CAAC,KAAK,EAAE;QAC3B,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA4BlB,YAAY,CAAC,KAAK,EAAE;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,SAAS,CAAC;IAOhB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IASrD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAiBrD,iBAAiB,CACrB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;QAAE,YAAY,CAAC,EAAE,iBAAiB,CAAA;KAAO,GACjD,OAAO,CAAC,SAAS,EAAE,CAAC;IAIjB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAI/C,WAAW,CAAC,KAAK,EAAE;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,SAAS,CAAC;IAQhB,YAAY,CAAC,KAAK,EAAE;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,SAAS,CAAC;IAUhB,iBAAiB,CAAC,KAAK,EAAE;QAC7B,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA+BlB,WAAW,CAAC,KAAK,EAAE;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;YAcpB,UAAU;YAaV,gBAAgB;YAuBhB,gBAAgB;YAuBhB,UAAU;YASV,mBAAmB;YAgBnB,IAAI;YAIJ,MAAM;CAUrB;AAwBD,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;CACrD;AAED,MAAM,MAAM,0BAA0B,GAClC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC"}
|
|
@@ -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 {
|
|
7
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
442
|
+
queue,
|
|
443
|
+
surfaceLog: () => ({}),
|
|
412
444
|
});
|
|
413
|
-
|
|
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
|
|
416
|
-
|
|
417
|
-
|
|
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('
|
|
532
|
+
test('hanging preview request times out before enqueue; late results cannot mutate the delivered item', async () => {
|
|
421
533
|
await withIngestHome(async () => {
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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.
|
|
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
|
+
});
|
package/dist/shared/inbox.d.ts
CHANGED
|
@@ -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":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,eAAe;;;;;EAAuD,CAAC;AACpF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiC5B,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,eAAO,MAAM,eAAe;;;;;;;;;;;iBAc1B,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,eAAO,MAAM,aAAa;;;;;;iBAMxB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,eAAO,MAAM,aAAa;;;;;;iBAAgB,CAAC;AAC3C,MAAM,MAAM,aAAa,GAAG,aAAa,CAAC;AAE1C,eAAO,MAAM,uBAAuB;;;;;;;iBAElC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE9E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;iBAU9B,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,eAAO,MAAM,gBAAgB;;;;;;iBAM3B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAQhE,6FAA6F;AAC7F,eAAO,MAAM,UAAU;;;;;EAA+D,CAAC;AACvF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,eAAO,MAAM,cAAc
|
|
1
|
+
{"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../shared/inbox.ts"],"names":[],"mappings":"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"}
|
package/dist/shared/inbox.js
CHANGED
|
@@ -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`. `
|
|
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
|
|
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
|
|