From b0eb8aafe305ea6b6fa83f54248c796f0fdf7c1b Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Ducasse Date: Thu, 24 Sep 2026 18:12:44 +0200 Subject: [PATCH] fix pr --- apps/dashboard/README.md | 4 +-- apps/dashboard/src/lib/trpc/shared.ts | 2 +- apps/server/slack-manifest.json | 10 +++++++- apps/server/src/routes/mcp/evals/run.ts | 4 +-- apps/server/src/routes/slack/agent.ts | 5 ++-- apps/server/src/routes/slack/blocks.test.ts | 2 +- apps/server/src/routes/slack/blocks.ts | 9 ++++++- apps/server/src/routes/slack/handler.ts | 25 ++++++++++++++----- apps/server/src/routes/slack/home.ts | 2 +- .../src/routes/slack/registry-runner.test.ts | 2 +- apps/server/src/routes/slack/system-prompt.ts | 2 +- .../src/app/api/callback/pagerduty/route.ts | 2 +- 12 files changed, 48 insertions(+), 21 deletions(-) diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md index 58c11c02..097643e8 100644 --- a/apps/dashboard/README.md +++ b/apps/dashboard/README.md @@ -63,7 +63,7 @@ pnpm -w dev:dashboard Turbo runs the dashboard (`apps/dashboard`) and `@openstatus/db` together. -6. Open [http://localhost:3000](http://localhost:3000) +6. Open [http://localhost:3001](http://localhost:3001) ## Logging in @@ -73,7 +73,7 @@ In `NODE_ENV=development` or `SELF_HOST=true`, `src/lib/auth/providers.ts` confi To log in: -1. Open [http://localhost:3000/login](http://localhost:3000/login) +1. Open [http://localhost:3001/login](http://localhost:3001/login) 2. Enter `ping@openstatus.dev` (the seeded user, bound to workspace 1) in the magic-link form 3. Watch the dashboard terminal — the magic link is logged there. Open it in your browser. diff --git a/apps/dashboard/src/lib/trpc/shared.ts b/apps/dashboard/src/lib/trpc/shared.ts index 55a37c21..501d3afa 100644 --- a/apps/dashboard/src/lib/trpc/shared.ts +++ b/apps/dashboard/src/lib/trpc/shared.ts @@ -52,7 +52,7 @@ const getBaseUrl = () => { // Note: dashboard has its own tRPC API routes if (process.env.VERCEL_URL) return "https://app.openstatus.dev"; // Vercel // Dev runs on 3001 (`PORT` in the dev script), Docker on 3000. - return `http://localhost:${process.env.PORT ?? 3000}`; // Local dev and Docker (internal calls) + return `http://localhost:${process.env.PORT || 3000}`; // Local dev and Docker (internal calls) }; // The whole tRPC surface is served from a single Node.js endpoint — there is diff --git a/apps/server/slack-manifest.json b/apps/server/slack-manifest.json index eea243d3..49dd6c72 100644 --- a/apps/server/slack-manifest.json +++ b/apps/server/slack-manifest.json @@ -7,7 +7,7 @@ }, "features": { "assistant_view": { - "assistant_description": "Drafts, updates and resolves status reports and maintenance windows from your conversations. Nothing is published until you approve it.", + "assistant_description": "Drafts, updates and resolves status reports and maintenance windows. Nothing is published until you approve it.", "suggested_prompts": [ { "title": "Create a status report", @@ -16,6 +16,14 @@ { "title": "Open status reports", "message": "Which status reports are currently open?" + }, + { + "title": "Schedule maintenance", + "message": "Schedule a maintenance window next Tuesday from 2-3 PM UTC." + }, + { + "title": "Upcoming maintenance", + "message": "What maintenance windows are coming up?" } ] }, diff --git a/apps/server/src/routes/mcp/evals/run.ts b/apps/server/src/routes/mcp/evals/run.ts index fd45683c..f6a912b1 100644 --- a/apps/server/src/routes/mcp/evals/run.ts +++ b/apps/server/src/routes/mcp/evals/run.ts @@ -1,12 +1,12 @@ /** * MCP tool-selection eval. Standalone bun script — `pnpm eval:mcp`. * - * Runs each case in `cases.ts` against Claude Haiku 4.5 (via the AI + * Runs each case in `cases.ts` against Claude Opus 5 (via the AI * Gateway), asserting the model picks the expected tool and includes * the required args. Fails the run if fewer than `PASS_THRESHOLD` of * `cases.length` succeed. * - * Not in default CI. Cost: a handful of cents per run. + * Not in default CI — every run bills Opus tokens. * * -------------------------------------------------------------------- * TODO: deduplicate tool catalogue. diff --git a/apps/server/src/routes/slack/agent.ts b/apps/server/src/routes/slack/agent.ts index cc19f1eb..da99037e 100644 --- a/apps/server/src/routes/slack/agent.ts +++ b/apps/server/src/routes/slack/agent.ts @@ -8,9 +8,8 @@ import { tb } from "@/libs/clients"; import { buildSlackTools } from "./registry-runner"; import { buildSystemPrompt } from "./system-prompt"; -// Vercel AI Gateway model id. Override via SLACK_AGENT_MODEL when rolling -// out a new Sonnet version. Dotted format (`4.6`, not `4-6`) is what the -// gateway accepts — see `apps/dashboard/src/app/api/chat/route.ts`. +// Vercel AI Gateway model id (`anthropic/`). Override via +// SLACK_AGENT_MODEL when rolling out a new model version. const DEFAULT_MODEL = "anthropic/claude-opus-5"; // `||` (not `??`) so empty / whitespace-only env values fall back to the // default rather than being passed through to `generateText`. diff --git a/apps/server/src/routes/slack/blocks.test.ts b/apps/server/src/routes/slack/blocks.test.ts index f63b11b8..b5a7170d 100644 --- a/apps/server/src/routes/slack/blocks.test.ts +++ b/apps/server/src/routes/slack/blocks.test.ts @@ -73,7 +73,7 @@ describe("buildConfirmationBlocks", () => { expect(text).not.toContain("Page ID"); }); - test("create_status_report falls back to page id when the page can't be resolved", async () => { + test("create_status_report hides the page line when the page can't be resolved", async () => { const tool = agentTools.create_status_report; const blocks = await buildConfirmationBlocks({ actionId: "link2", diff --git a/apps/server/src/routes/slack/blocks.ts b/apps/server/src/routes/slack/blocks.ts index affe6d59..e39c8905 100644 --- a/apps/server/src/routes/slack/blocks.ts +++ b/apps/server/src/routes/slack/blocks.ts @@ -109,6 +109,11 @@ function escapeLinkText(text: string): string { return escapeText(text).replace(/\|/g, "❘"); } +/** Escape a URL for use as the target of a Slack mrkdwn link (``). */ +function escapeLinkUrl(url: string): string { + return escapeText(url).replace(/\|/g, "%7C"); +} + /** * Data resolvers the Slack surface injects so `buildConfirmationBlocks` can * turn `SummaryLineRef` descriptors into names. Resolution needs DB access, @@ -274,7 +279,9 @@ export async function buildConfirmationBlocks(args: { if (report?.url) { blocks.push({ type: "context", - elements: [{ type: "mrkdwn", text: `<${report.url}|View report>` }], + elements: [ + { type: "mrkdwn", text: `<${escapeLinkUrl(report.url)}|View report>` }, + ], }); } blocks.push({ type: "divider" }, { type: "actions", elements: buttons }); diff --git a/apps/server/src/routes/slack/handler.ts b/apps/server/src/routes/slack/handler.ts index ca795997..af266dc0 100644 --- a/apps/server/src/routes/slack/handler.ts +++ b/apps/server/src/routes/slack/handler.ts @@ -121,17 +121,30 @@ export function looksLikeUncardedDraft(text: string): boolean { return (text.match(PROSE_DRAFT_FIELDS) ?? []).length >= 2; } +// Bounds the Slack calls (and the agent's context) on very long threads. +const MAX_THREAD_PAGES = 5; + +// Replies come oldest first, so a single page would miss the latest messages +// of a long thread — the ones the agent is being asked about. async function fetchThread( slack: WebClient, channel: string, threadTs: string, ): Promise { - const replies = await slack.conversations.replies({ - channel, - ts: threadTs, - limit: 100, - }); - return (replies.messages ?? []) as ThreadMessage[]; + const messages: ThreadMessage[] = []; + let cursor: string | undefined; + for (let page = 0; page < MAX_THREAD_PAGES; page++) { + const replies = await slack.conversations.replies({ + channel, + ts: threadTs, + limit: 100, + cursor, + }); + messages.push(...((replies.messages ?? []) as ThreadMessage[])); + cursor = replies.response_metadata?.next_cursor || undefined; + if (!replies.has_more || !cursor) break; + } + return messages; } /** diff --git a/apps/server/src/routes/slack/home.ts b/apps/server/src/routes/slack/home.ts index 1468a691..8caad375 100644 --- a/apps/server/src/routes/slack/home.ts +++ b/apps/server/src/routes/slack/home.ts @@ -21,7 +21,7 @@ export function buildHomeBlocks(): KnownBlock[] { type: "section", text: { type: "mrkdwn", - text: '*Create & update incidents*\nDescribe the issue in the agent pane, or mention `@openstatus` in the incident thread. It reads the thread, drafts a report, and you click *Approve*, *Approve & Notify*, or *Cancel*. Say _"we found the cause"_ or _"it\'s fixed"_ and it moves the incident to Identified or Resolved.', + text: '*Create & update incidents*\nDescribe the issue in the agent pane, or mention `@openstatus` in any channel or thread. It reads the thread, drafts a report, and you click *Approve*, *Approve & Notify*, or *Cancel*. Say _"we found the cause"_ or _"it\'s fixed"_ and it moves the incident to Identified or Resolved.', }, }, { diff --git a/apps/server/src/routes/slack/registry-runner.test.ts b/apps/server/src/routes/slack/registry-runner.test.ts index 35925f34..d0854fad 100644 --- a/apps/server/src/routes/slack/registry-runner.test.ts +++ b/apps/server/src/routes/slack/registry-runner.test.ts @@ -381,7 +381,7 @@ describe("buildSystemPrompt coverage", () => { test("mentions every tool the Slack agent is given", () => { const prompt = buildSystemPrompt("Acme Corp"); const missing = Object.keys(agentTools).filter( - (name) => !prompt.includes(name), + (name) => !new RegExp(`\\b${name}\\b`).test(prompt), ); expect(missing).toEqual([]); }); diff --git a/apps/server/src/routes/slack/system-prompt.ts b/apps/server/src/routes/slack/system-prompt.ts index f334a4ba..296f869e 100644 --- a/apps/server/src/routes/slack/system-prompt.ts +++ b/apps/server/src/routes/slack/system-prompt.ts @@ -90,7 +90,7 @@ Monitor diagnostics: - get_monitor_status returns one row per configured region (active/degraded/error). Report at the worst region's level: "Healthy in 5/7 regions; failing in gru, fra." Don't invent a composite "overall: degraded" label — the per-region facts ARE the answer. - Default to the last 1 day for get_monitor_summary and list_response_logs; use 7d or 14d only if the user asks for a longer window. - Before drafting a status report that names a monitor as degraded or down, call get_monitor_status to confirm the per-region state — don't rely on the user's framing alone. -- list_notifications shows which monitors each channel is wired to. Use it to advise ("PagerDuty is attached to monitor 17, so on-call will be paged"). +- list_notifications shows which monitors each channel is wired to (by id — resolve names with list_monitors). Use it to advise ("PagerDuty is attached to the API monitor, so on-call will be paged"). Docs and product questions: - For questions about how openstatus works (features, configuration, CLI, API, plans), call search_docs BEFORE answering — never answer product questions from memory. diff --git a/apps/web/src/app/api/callback/pagerduty/route.ts b/apps/web/src/app/api/callback/pagerduty/route.ts index b020ab21..292d8573 100644 --- a/apps/web/src/app/api/callback/pagerduty/route.ts +++ b/apps/web/src/app/api/callback/pagerduty/route.ts @@ -8,7 +8,7 @@ export async function GET(request: Request) { const APP_URL = `${ process.env.NODE_ENV === "development" // FIXME: This sucks - ? "http://localhost:3000" + ? "http://localhost:3001" : "https://app.openstatus.dev" }/notifications?${searchParams}&channel=pagerduty`; -- 2.51.2