experimental-ash 0.8.0 → 0.8.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # experimental-ash
2
2
 
3
+ ## 0.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 1434a11: Add named return types `SlackChannel` and `AshChannel` for `slackChannel(...)` and `ashChannel(...)`. Default-exporting either call from a `channels/*.ts` module now type-checks under `declaration: true` without TypeScript falling back to an internal path (TS2883).
8
+ - da443bb: Fix race condition in Slack channel where `onNewMention` could fire after the request context was cleared, causing "mention received but no request context is active" errors
9
+ - 54e0a2a: Allow overriding the Slack channel webhook route path via the `route` option in `SlackChannelConfig`
10
+
3
11
  ## 0.8.0
4
12
 
5
13
  ### Minor Changes
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
6
6
  let cachedPackageInfo;
7
7
  // The package build stamps the published version into `dist` so bundled
8
8
  // deployments can still report package metadata without resolving package.json.
9
- const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.8.0";
9
+ const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.8.1";
10
10
  const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
11
11
  const WORKFLOW_MODULE_ALIASES = {
12
12
  "workflow/api": "src/compiled/@workflow/core/runtime.js",
@@ -5,4 +5,11 @@ export interface AshChannelInput {
5
5
  readonly auth: AuthFn<Request>;
6
6
  readonly uploadPolicy?: Partial<UploadPolicy>;
7
7
  }
8
- export declare function ashChannel(input: AshChannelInput): Channel;
8
+ /**
9
+ * Concrete return type of {@link ashChannel}. Named so consumers can
10
+ * default-export an `ashChannel(...)` call under `declaration: true`
11
+ * without TypeScript falling back to an internal path for `Channel`.
12
+ */
13
+ export interface AshChannel extends Channel {
14
+ }
15
+ export declare function ashChannel(input: AshChannelInput): AshChannel;
@@ -1,4 +1,4 @@
1
1
  export { slack, type SlackOptions } from "#public/channels/slack/slack.js";
2
- export { slackChannel, type SlackApiHandle, type SlackApiResponse, type SlackChannelConfig, type SlackChannelEvents, type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackInteractionAction, type SlackReceiveArgs, } from "#public/channels/slack/slackChannel.js";
2
+ export { slackChannel, type SlackApiHandle, type SlackApiResponse, type SlackChannel, type SlackChannelConfig, type SlackChannelEvents, type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackInteractionAction, type SlackReceiveArgs, } from "#public/channels/slack/slackChannel.js";
3
3
  export { Actions, Button, Card, CardText, Divider, Fields, Image, LinkButton, Modal, RadioSelect, Section, Select, SelectOption, Table, TextInput, } from "#compiled/chat/index.js";
4
4
  export type { AdapterPostableMessage, Attachment, Author, CardElement, FileUpload, Message, PostableMessage, SentMessage, Thread, } from "#compiled/chat/index.js";
@@ -72,6 +72,10 @@ export interface SlackChannelEvents {
72
72
  export interface SlackChannelConfig {
73
73
  readonly credentials?: SlackChannelCredentials;
74
74
  readonly botName?: string;
75
+ /**
76
+ * Override the default webhook route path (`/ash/v1/slack`).
77
+ */
78
+ readonly route?: string;
75
79
  /**
76
80
  * Inbound upload policy applied to file attachments before they
77
81
  * reach the harness. Violating attachments are dropped with a
@@ -83,5 +87,12 @@ export interface SlackChannelConfig {
83
87
  onInteraction?(action: SlackInteractionAction, ctx: SlackContext): void | Promise<void>;
84
88
  readonly events?: SlackChannelEvents;
85
89
  }
86
- export declare function slackChannel(config?: SlackChannelConfig): Channel<SlackChannelState>;
90
+ /**
91
+ * Concrete return type of {@link slackChannel}. Named so consumers can
92
+ * default-export a `slackChannel(...)` call under `declaration: true`
93
+ * without TypeScript falling back to an internal path for `Channel`.
94
+ */
95
+ export interface SlackChannel extends Channel<SlackChannelState> {
96
+ }
97
+ export declare function slackChannel(config?: SlackChannelConfig): SlackChannel;
87
98
  export {};
@@ -2,7 +2,7 @@ import { createLogger } from "#internal/logging.js";
2
2
  import { buildSlackTurnMessage, collectSlackFileParts, createSlackFetchFile, } from "#public/channels/slack/attachments.js";
3
3
  import { deriveHitlResponse, isHitlAction, renderInputRequestBlocks, } from "#public/channels/slack/hitl.js";
4
4
  import { mergeUploadPolicy } from "#public/channels/upload-policy.js";
5
- import { defineChannel, POST, } from "#public/definitions/defineChannel.js";
5
+ import { defineChannel, POST } from "#public/definitions/defineChannel.js";
6
6
  const log = createLogger("slack.channel");
7
7
  function decodeThreadId(id) {
8
8
  const parts = id.replace(/^slack:/u, "").split(":");
@@ -112,7 +112,6 @@ export function slackChannel(config = {}) {
112
112
  const slackFetchFile = createSlackFetchFile({
113
113
  botToken: config.credentials?.botToken,
114
114
  });
115
- let activeSend = null;
116
115
  let chatPromise = null;
117
116
  async function getChat() {
118
117
  if (chatPromise)
@@ -139,39 +138,6 @@ export function slackChannel(config = {}) {
139
138
  userName: config.botName ?? "ash-agent",
140
139
  });
141
140
  await chat.initialize();
142
- chat.onNewMention(async (thread, message) => {
143
- // Slack sends both `app_mention` and `message.channels` for the same message.
144
- // The Chat SDK dedup relies on in-memory state that doesn't survive across
145
- // serverless invocations, so both events reach this handler. Only process
146
- // `app_mention` to prevent duplicate runs.
147
- const rawEvent = message.raw;
148
- if (rawEvent?.type !== "app_mention")
149
- return;
150
- const send = activeSend;
151
- if (!send) {
152
- throw new Error("slackChannel: mention received but no request context is active.");
153
- }
154
- const teamId = rawEvent.team_id ?? rawEvent.team;
155
- const slackCtx = {
156
- thread,
157
- slack: buildSlackApiHandle(thread, config.credentials?.botToken, teamId),
158
- };
159
- const runOpts = config.run ? config.run(slackCtx, message) : { auth: null };
160
- if (runOpts === null)
161
- return;
162
- const decoded = decodeThreadId(thread.id ?? "");
163
- const continuationToken = `slack:${decoded.channelId}:${decoded.threadTs}`;
164
- const fileParts = collectSlackFileParts(message, uploadPolicy);
165
- const turnMessage = buildSlackTurnMessage(message.text, fileParts);
166
- await send(turnMessage, {
167
- auth: runOpts.auth,
168
- continuationToken,
169
- state: {
170
- serializedThread: thread.toJSON(),
171
- teamId: teamId ?? null,
172
- },
173
- });
174
- });
175
141
  return { chat };
176
142
  })();
177
143
  chatPromise = promise;
@@ -188,7 +154,7 @@ export function slackChannel(config = {}) {
188
154
  return rebuildSlackContext(state, config.credentials?.botToken);
189
155
  },
190
156
  routes: [
191
- POST("/ash/v1/slack", async (req, { send, waitUntil }) => {
157
+ POST(config.route ?? "/ash/v1/slack", async (req, { send, waitUntil }) => {
192
158
  const { chat } = await getChat();
193
159
  const contentType = req.headers.get("content-type") ?? "";
194
160
  if (contentType.includes("application/x-www-form-urlencoded")) {
@@ -249,16 +215,36 @@ export function slackChannel(config = {}) {
249
215
  }
250
216
  return new Response("ok", { status: 200 });
251
217
  }
252
- activeSend = send;
253
- try {
254
- const webhookHandler = chat.webhooks.slack;
255
- return await webhookHandler(req, {
256
- waitUntil,
218
+ chat.onNewMention(async (thread, message) => {
219
+ const rawEvent = message.raw;
220
+ // Slack sends both `app_mention` and `message.channels` for the same
221
+ // utterance. The Chat SDK dedup relies on in-memory state that doesn't
222
+ // survive serverless invocations, so both events reach this handler.
223
+ // Only process `app_mention` to prevent duplicate runs.
224
+ if (rawEvent?.type !== "app_mention")
225
+ return;
226
+ const teamId = rawEvent.team_id ?? rawEvent.team;
227
+ const slackCtx = {
228
+ thread,
229
+ slack: buildSlackApiHandle(thread, config.credentials?.botToken, teamId),
230
+ };
231
+ const runOpts = config.run ? config.run(slackCtx, message) : { auth: null };
232
+ if (runOpts === null)
233
+ return;
234
+ const decoded = decodeThreadId(thread.id ?? "");
235
+ const continuationToken = `slack:${decoded.channelId}:${decoded.threadTs}`;
236
+ const fileParts = collectSlackFileParts(message, uploadPolicy);
237
+ const turnMessage = buildSlackTurnMessage(message.text, fileParts);
238
+ await send(turnMessage, {
239
+ auth: runOpts.auth,
240
+ continuationToken,
241
+ state: {
242
+ serializedThread: thread.toJSON(),
243
+ teamId: teamId ?? null,
244
+ },
257
245
  });
258
- }
259
- finally {
260
- activeSend = null;
261
- }
246
+ });
247
+ return await chat.webhooks.slack(req, { waitUntil });
262
248
  }),
263
249
  ],
264
250
  async receive(input, { send }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-ash",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "bin": {
5
5
  "ash": "./bin/ash.js",
6
6
  "experimental-ash": "./bin/ash.js"