Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/junior/src/chat/slack/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ function mapSlackError(error: unknown): SlackActionError {
return new SlackActionError(message, "invalid_arguments", baseOptions);
}

if (apiError === "invalid_cursor") {
return new SlackActionError(message, "invalid_arguments", baseOptions);
}

if (apiError === "invalid_name") {
return new SlackActionError(message, "invalid_arguments", baseOptions);
}
Expand Down
34 changes: 25 additions & 9 deletions packages/junior/src/chat/tools/slack/channel-list-messages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Type } from "@sinclair/typebox";
import { SlackActionError } from "@/chat/slack/client";
import { listChannelMessages } from "@/chat/slack/channel";
import { tool } from "@/chat/tools/definition";
import type { ToolRuntimeContext } from "@/chat/tools/types";
Expand Down Expand Up @@ -67,15 +68,30 @@ export function createSlackChannelListMessagesTool(
};
}

const result = await listChannelMessages({
channelId: targetChannelId,
limit: limit ?? 100,
cursor,
oldest,
latest,
inclusive,
maxPages: max_pages,
});
let result;
try {
result = await listChannelMessages({
channelId: targetChannelId,
limit: limit ?? 100,
cursor,
oldest,
latest,
inclusive,
maxPages: max_pages,
});
} catch (error) {
if (
error instanceof SlackActionError &&
error.apiError === "invalid_cursor"
) {
return {
ok: false,
error:
"The supplied Slack history cursor is no longer valid. Retry the lookup without `cursor` to start from the newest page again.",
};
}
throw error;
}

return {
ok: true,
Expand Down
27 changes: 27 additions & 0 deletions packages/junior/tests/integration/slack-channel-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,33 @@ describe("slack channel tools", () => {
expect(String(historyCalls[1]?.params.limit)).toBe("1");
});

it("returns a recoverable tool error when Slack rejects a stale history cursor", async () => {
queueSlackApiError("conversations.history", {
error: "invalid_cursor",
});
const tool = createSlackChannelListMessagesTool(
createContext("list channel messages"),
);

const result = await executeTool(tool, {
cursor: "expired-cursor",
limit: 10,
});

expect(result).toEqual({
ok: false,
error:
"The supplied Slack history cursor is no longer valid. Retry the lookup without `cursor` to start from the newest page again.",
});

const historyCalls = getCapturedSlackApiCalls("conversations.history");
expect(historyCalls).toHaveLength(1);
expect(historyCalls[0]?.params).toMatchObject({
channel: "C123",
cursor: "expired-cursor",
});
});

it("adds a reaction to the implicitly targeted inbound message", async () => {
queueSlackApiResponse("reactions.add", {
body: reactionsAddOk(),
Expand Down
18 changes: 18 additions & 0 deletions packages/junior/tests/unit/slack/slack-client-retries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,24 @@ describe("withSlackRetries", () => {
expect(task).toHaveBeenCalledTimes(1);
});

it("maps invalid_cursor as invalid_arguments while preserving the API error", async () => {
const task = vi.fn<() => Promise<string>>().mockRejectedValue({
data: {
error: "invalid_cursor",
},
message: "An API error occurred: invalid_cursor",
});

await expect(withSlackRetries(task, 3)).rejects.toEqual(
expect.objectContaining<Partial<SlackActionError>>({
name: "SlackActionError",
code: "invalid_arguments",
apiError: "invalid_cursor",
}),
);
expect(task).toHaveBeenCalledTimes(1);
});

it("maps already_reacted as a dedicated Slack action error", async () => {
const task = vi.fn<() => Promise<string>>().mockRejectedValue({
data: {
Expand Down
Loading