@inkeep/agents-work-apps 0.48.0 → 0.48.2

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.
@@ -4,10 +4,10 @@ import "./routes/setup.js";
4
4
  import "./routes/tokenExchange.js";
5
5
  import { WebhookVerificationResult, verifyWebhookSignature } from "./routes/webhooks.js";
6
6
  import { Hono } from "hono";
7
- import * as hono_types6 from "hono/types";
7
+ import * as hono_types0 from "hono/types";
8
8
 
9
9
  //#region src/github/index.d.ts
10
- declare function createGithubRoutes(): Hono<hono_types6.BlankEnv, hono_types6.BlankSchema, "/">;
11
- declare const githubRoutes: Hono<hono_types6.BlankEnv, hono_types6.BlankSchema, "/">;
10
+ declare function createGithubRoutes(): Hono<hono_types0.BlankEnv, hono_types0.BlankSchema, "/">;
11
+ declare const githubRoutes: Hono<hono_types0.BlankEnv, hono_types0.BlankSchema, "/">;
12
12
  //#endregion
13
13
  export { GenerateInstallationAccessTokenResult, GenerateTokenError, GenerateTokenResult, GitHubAppConfig, InstallationAccessToken, InstallationInfo, LookupInstallationError, LookupInstallationForRepoResult, LookupInstallationResult, WebhookVerificationResult, clearConfigCache, createAppJwt, createGithubRoutes, determineStatus, fetchInstallationDetails, fetchInstallationRepositories, generateInstallationAccessToken, getGitHubAppConfig, getGitHubAppName, getStateSigningSecret, getWebhookSecret, githubRoutes, isGitHubAppConfigured, isGitHubAppNameConfigured, isStateSigningConfigured, isWebhookConfigured, lookupInstallationForRepo, validateGitHubAppConfigOnStartup, validateGitHubInstallFlowConfigOnStartup, validateGitHubWebhookConfigOnStartup, verifyWebhookSignature };
@@ -1,7 +1,7 @@
1
- import * as hono1 from "hono";
1
+ import * as hono0 from "hono";
2
2
 
3
3
  //#region src/github/mcp/auth.d.ts
4
- declare const githubMcpAuth: () => hono1.MiddlewareHandler<{
4
+ declare const githubMcpAuth: () => hono0.MiddlewareHandler<{
5
5
  Variables: {
6
6
  toolId: string;
7
7
  };
@@ -1,11 +1,11 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types1 from "hono/types";
2
+ import * as hono_types3 from "hono/types";
3
3
 
4
4
  //#region src/github/mcp/index.d.ts
5
5
  declare const app: Hono<{
6
6
  Variables: {
7
7
  toolId: string;
8
8
  };
9
- }, hono_types1.BlankSchema, "/">;
9
+ }, hono_types3.BlankSchema, "/">;
10
10
  //#endregion
11
11
  export { app as default };
@@ -36,8 +36,8 @@ declare const ChangedFileSchema: z.ZodObject<{
36
36
  path: z.ZodString;
37
37
  status: z.ZodEnum<{
38
38
  added: "added";
39
- removed: "removed";
40
39
  modified: "modified";
40
+ removed: "removed";
41
41
  renamed: "renamed";
42
42
  copied: "copied";
43
43
  changed: "changed";
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types2 from "hono/types";
2
+ import * as hono_types4 from "hono/types";
3
3
 
4
4
  //#region src/github/routes/setup.d.ts
5
- declare const app: Hono<hono_types2.BlankEnv, hono_types2.BlankSchema, "/">;
5
+ declare const app: Hono<hono_types4.BlankEnv, hono_types4.BlankSchema, "/">;
6
6
  //#endregion
7
7
  export { app as default };
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types0 from "hono/types";
2
+ import * as hono_types6 from "hono/types";
3
3
 
4
4
  //#region src/github/routes/tokenExchange.d.ts
5
- declare const app: Hono<hono_types0.BlankEnv, hono_types0.BlankSchema, "/">;
5
+ declare const app: Hono<hono_types6.BlankEnv, hono_types6.BlankSchema, "/">;
6
6
  //#endregion
7
7
  export { app as default };
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types4 from "hono/types";
2
+ import * as hono_types8 from "hono/types";
3
3
 
4
4
  //#region src/github/routes/webhooks.d.ts
5
5
  interface WebhookVerificationResult {
@@ -7,6 +7,6 @@ interface WebhookVerificationResult {
7
7
  error?: string;
8
8
  }
9
9
  declare function verifyWebhookSignature(payload: string, signature: string | undefined, secret: string): WebhookVerificationResult;
10
- declare const app: Hono<hono_types4.BlankEnv, hono_types4.BlankSchema, "/">;
10
+ declare const app: Hono<hono_types8.BlankEnv, hono_types8.BlankSchema, "/">;
11
11
  //#endregion
12
12
  export { WebhookVerificationResult, app as default, verifyWebhookSignature };
@@ -63,6 +63,19 @@ app.post("/commands", async (c) => {
63
63
  return c.json(response);
64
64
  });
65
65
  app.post("/events", async (c) => {
66
+ const retryNum = c.req.header("x-slack-retry-num");
67
+ const retryReason = c.req.header("x-slack-retry-reason");
68
+ if (retryNum) return tracer.startActiveSpan(`${SLACK_SPAN_NAMES.WEBHOOK} retry`, (span) => {
69
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, "ignored_slack_retry");
70
+ span.setAttribute("slack.retry_num", retryNum);
71
+ if (retryReason) span.setAttribute("slack.retry_reason", retryReason);
72
+ logger.info({
73
+ retryNum,
74
+ retryReason
75
+ }, "Acknowledging Slack retry without re-processing");
76
+ span.end();
77
+ return c.json({ ok: true });
78
+ });
66
79
  return tracer.startActiveSpan(SLACK_SPAN_NAMES.WEBHOOK, async (span) => {
67
80
  let outcome = "ignored_unknown_event";
68
81
  try {
@@ -1,5 +1,5 @@
1
1
  import { getLogger } from "../../logger.js";
2
- import { WebClient, retryPolicies } from "@slack/web-api";
2
+ import { WebClient } from "@slack/web-api";
3
3
 
4
4
  //#region src/slack/services/client.ts
5
5
  /**
@@ -29,7 +29,7 @@ async function paginateSlack({ fetchPage, extractItems, getNextCursor, limit })
29
29
  * @returns Configured Slack WebClient instance
30
30
  */
31
31
  function getSlackClient(token) {
32
- return new WebClient(token, { retryConfig: retryPolicies.fiveRetriesInFiveMinutes });
32
+ return new WebClient(token);
33
33
  }
34
34
  /**
35
35
  * Fetch user profile information from Slack.
@@ -71,6 +71,7 @@ async function handleAppMention(params) {
71
71
  const replyThreadTs = threadTs || messageTs;
72
72
  const isInThread = Boolean(threadTs && threadTs !== messageTs);
73
73
  const hasQuery = Boolean(text && text.trim().length > 0);
74
+ let thinkingMessageTs;
74
75
  try {
75
76
  const [agentConfig, existingLink] = await Promise.all([resolveChannelAgentConfig(teamId, channel, workspaceConnection), findCachedUserMapping(tenantId, slackUserId, teamId)]);
76
77
  if (!agentConfig) {
@@ -167,11 +168,11 @@ _Using: ${agentDisplayName}_`
167
168
  slackTeamId: teamId,
168
169
  slackUserId
169
170
  });
170
- const ackMessage$1 = await slackClient.chat.postMessage({
171
+ thinkingMessageTs = (await slackClient.chat.postMessage({
171
172
  channel,
172
173
  thread_ts: threadTs,
173
174
  text: `_${agentDisplayName} is reading this thread..._`
174
- });
175
+ })).ts || void 0;
175
176
  const conversationId$1 = generateSlackConversationId({
176
177
  teamId,
177
178
  threadTs,
@@ -203,7 +204,7 @@ Respond naturally as if you're joining the conversation to help.`;
203
204
  slackClient,
204
205
  channel,
205
206
  threadTs,
206
- thinkingMessageTs: ackMessage$1.ts || "",
207
+ thinkingMessageTs: thinkingMessageTs || "",
207
208
  slackUserId,
208
209
  teamId,
209
210
  jwtToken: slackUserToken$1,
@@ -227,11 +228,11 @@ Respond naturally as if you're joining the conversation to help.`;
227
228
  slackTeamId: teamId,
228
229
  slackUserId
229
230
  });
230
- const ackMessage = await slackClient.chat.postMessage({
231
+ thinkingMessageTs = (await slackClient.chat.postMessage({
231
232
  channel,
232
233
  thread_ts: replyThreadTs,
233
234
  text: `_${agentDisplayName} is preparing a response..._`
234
- });
235
+ })).ts || void 0;
235
236
  const conversationId = generateSlackConversationId({
236
237
  teamId,
237
238
  threadTs: replyThreadTs,
@@ -249,7 +250,7 @@ Respond naturally as if you're joining the conversation to help.`;
249
250
  slackClient,
250
251
  channel,
251
252
  threadTs: replyThreadTs,
252
- thinkingMessageTs: ackMessage.ts || "",
253
+ thinkingMessageTs: thinkingMessageTs || "",
253
254
  slackUserId,
254
255
  teamId,
255
256
  jwtToken: slackUserToken,
@@ -268,6 +269,12 @@ Respond naturally as if you're joining the conversation to help.`;
268
269
  teamId
269
270
  }, "Failed in app mention handler");
270
271
  if (error instanceof Error) setSpanWithError(span, error);
272
+ if (thinkingMessageTs) try {
273
+ await slackClient.chat.delete({
274
+ channel,
275
+ ts: thinkingMessageTs
276
+ });
277
+ } catch {}
271
278
  const userMessage = getUserFriendlyErrorMessage(classifyError(error));
272
279
  try {
273
280
  await slackClient.chat.postEphemeral({
@@ -14,6 +14,8 @@ import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../
14
14
  const logger = getLogger("slack-streaming");
15
15
  const STREAM_TIMEOUT_MS = 12e4;
16
16
  const CHATSTREAM_OP_TIMEOUT_MS = 1e4;
17
+ /** Shorter timeout for best-effort cleanup in error paths to bound total error handling time. */
18
+ const CLEANUP_TIMEOUT_MS = 3e3;
17
19
  /**
18
20
  * Wrap a promise with a timeout to prevent indefinite blocking on Slack API calls.
19
21
  */
@@ -78,12 +80,12 @@ async function streamAgentResponse(params) {
78
80
  } catch (fetchError) {
79
81
  clearTimeout(timeoutId);
80
82
  if (fetchError.name === "AbortError") {
81
- const errorType = SlackErrorType.TIMEOUT;
82
- const errorMessage = getUserFriendlyErrorMessage(errorType, agentName);
83
+ const errorType$1 = SlackErrorType.TIMEOUT;
84
+ const errorMessage$1 = getUserFriendlyErrorMessage(errorType$1, agentName);
83
85
  await slackClient.chat.postMessage({
84
86
  channel,
85
87
  thread_ts: threadTs,
86
- text: errorMessage
88
+ text: errorMessage$1
87
89
  });
88
90
  if (thinkingMessageTs) try {
89
91
  await slackClient.chat.delete({
@@ -94,13 +96,30 @@ async function streamAgentResponse(params) {
94
96
  span.end();
95
97
  return {
96
98
  success: false,
97
- errorType,
98
- errorMessage
99
+ errorType: errorType$1,
100
+ errorMessage: errorMessage$1
99
101
  };
100
102
  }
103
+ const errorType = classifyError(fetchError);
104
+ const errorMessage = getUserFriendlyErrorMessage(errorType, agentName);
105
+ await slackClient.chat.postMessage({
106
+ channel,
107
+ thread_ts: threadTs,
108
+ text: errorMessage
109
+ }).catch((e) => logger.warn({ error: e }, "Failed to send fetch error notification"));
110
+ if (thinkingMessageTs) try {
111
+ await slackClient.chat.delete({
112
+ channel,
113
+ ts: thinkingMessageTs
114
+ });
115
+ } catch {}
101
116
  if (fetchError instanceof Error) setSpanWithError(span, fetchError);
102
117
  span.end();
103
- throw fetchError;
118
+ return {
119
+ success: false,
120
+ errorType,
121
+ errorMessage
122
+ };
104
123
  }
105
124
  if (!response.ok) {
106
125
  clearTimeout(timeoutId);
@@ -167,6 +186,7 @@ async function streamAgentResponse(params) {
167
186
  thread_ts: threadTs
168
187
  });
169
188
  try {
189
+ let agentCompleted = false;
170
190
  while (true) {
171
191
  const { done, value } = await reader.read();
172
192
  if (done) break;
@@ -179,7 +199,13 @@ async function streamAgentResponse(params) {
179
199
  if (!jsonStr || jsonStr === "[DONE]") continue;
180
200
  try {
181
201
  const data = JSON.parse(jsonStr);
182
- if (data.type === "data-operation") continue;
202
+ if (data.type === "data-operation") {
203
+ if (data.data?.type === "completion") {
204
+ agentCompleted = true;
205
+ break;
206
+ }
207
+ continue;
208
+ }
183
209
  if (data.type === "text-start" || data.type === "text-end") continue;
184
210
  if (data.type === "text-delta" && data.delta) {
185
211
  fullText += data.delta;
@@ -194,10 +220,21 @@ async function streamAgentResponse(params) {
194
220
  }
195
221
  } catch {}
196
222
  }
223
+ if (agentCompleted) break;
197
224
  }
198
225
  clearTimeout(timeoutId);
199
226
  const contextBlock = createContextBlock({ agentName });
200
- await withTimeout(streamer.stop({ blocks: [contextBlock] }), CHATSTREAM_OP_TIMEOUT_MS, "streamer.stop");
227
+ try {
228
+ await withTimeout(streamer.stop({ blocks: [contextBlock] }), CHATSTREAM_OP_TIMEOUT_MS, "streamer.stop");
229
+ } catch (stopError) {
230
+ span.setAttribute(SLACK_SPAN_KEYS.STREAM_FINALIZATION_FAILED, true);
231
+ logger.warn({
232
+ stopError,
233
+ channel,
234
+ threadTs,
235
+ responseLength: fullText.length
236
+ }, "Failed to finalize chatStream — content was already delivered");
237
+ }
201
238
  if (thinkingMessageTs) try {
202
239
  await slackClient.chat.delete({
203
240
  channel,
@@ -218,8 +255,26 @@ async function streamAgentResponse(params) {
218
255
  } catch (streamError) {
219
256
  clearTimeout(timeoutId);
220
257
  if (streamError instanceof Error) setSpanWithError(span, streamError);
258
+ if (fullText.length > 0) {
259
+ span.setAttribute(SLACK_SPAN_KEYS.CONTENT_ALREADY_DELIVERED, true);
260
+ logger.warn({
261
+ streamError,
262
+ channel,
263
+ threadTs,
264
+ responseLength: fullText.length
265
+ }, "Error during Slack streaming after content was already delivered — suppressing user-facing error");
266
+ await withTimeout(streamer.stop(), CLEANUP_TIMEOUT_MS, "streamer.stop-cleanup").catch((e) => logger.warn({ error: e }, "Failed to stop streamer during error cleanup"));
267
+ if (thinkingMessageTs) try {
268
+ await slackClient.chat.delete({
269
+ channel,
270
+ ts: thinkingMessageTs
271
+ });
272
+ } catch {}
273
+ span.end();
274
+ return { success: true };
275
+ }
221
276
  logger.error({ streamError }, "Error during Slack streaming");
222
- await withTimeout(streamer.stop(), CHATSTREAM_OP_TIMEOUT_MS, "streamer.stop").catch((e) => logger.warn({ error: e }, "Failed to stop streamer during error cleanup"));
277
+ await withTimeout(streamer.stop(), CLEANUP_TIMEOUT_MS, "streamer.stop-cleanup").catch((e) => logger.warn({ error: e }, "Failed to stop streamer during error cleanup"));
223
278
  if (thinkingMessageTs) try {
224
279
  await slackClient.chat.delete({
225
280
  channel,
@@ -34,7 +34,9 @@ declare const SLACK_SPAN_KEYS: {
34
34
  readonly IS_BOT_MESSAGE: "slack.is_bot_message";
35
35
  readonly HAS_QUERY: "slack.has_query";
36
36
  readonly IS_IN_THREAD: "slack.is_in_thread";
37
+ readonly STREAM_FINALIZATION_FAILED: "slack.stream_finalization_failed";
38
+ readonly CONTENT_ALREADY_DELIVERED: "slack.content_already_delivered";
37
39
  };
38
- type SlackOutcome = 'handled' | 'ignored_bot_message' | 'ignored_unknown_event' | 'ignored_no_action_match' | 'url_verification' | 'validation_error' | 'signature_invalid' | 'error';
40
+ type SlackOutcome = 'handled' | 'ignored_bot_message' | 'ignored_unknown_event' | 'ignored_no_action_match' | 'ignored_slack_retry' | 'url_verification' | 'validation_error' | 'signature_invalid' | 'error';
39
41
  //#endregion
40
42
  export { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, SlackOutcome, setSpanWithError, tracer };
@@ -32,7 +32,9 @@ const SLACK_SPAN_KEYS = {
32
32
  OUTCOME: "slack.outcome",
33
33
  IS_BOT_MESSAGE: "slack.is_bot_message",
34
34
  HAS_QUERY: "slack.has_query",
35
- IS_IN_THREAD: "slack.is_in_thread"
35
+ IS_IN_THREAD: "slack.is_in_thread",
36
+ STREAM_FINALIZATION_FAILED: "slack.stream_finalization_failed",
37
+ CONTENT_ALREADY_DELIVERED: "slack.content_already_delivered"
36
38
  };
37
39
 
38
40
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/agents-work-apps",
3
- "version": "0.48.0",
3
+ "version": "0.48.2",
4
4
  "description": "First party integrations for Inkeep Agents",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -33,7 +33,7 @@
33
33
  "jose": "^6.1.0",
34
34
  "minimatch": "^10.1.1",
35
35
  "slack-block-builder": "^2.8.0",
36
- "@inkeep/agents-core": "0.48.0"
36
+ "@inkeep/agents-core": "0.48.2"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@hono/zod-openapi": "^1.1.5",