Skip to content

fix(bot): classify comments with one AI call - #2528

Merged
ascorbic merged 1 commit into
mainfrom
fix/bot-direct-classifier
Aug 17, 2026
Merged

fix(bot): classify comments with one AI call#2528
ascorbic merged 1 commit into
mainfrom
fix/bot-direct-classifier

Conversation

@ascorbic

@ascorbic ascorbic commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes free-text @emdashbot commands being silently dropped after the classifier selected the correct action. The classifier now makes one typed Workers AI function-call request and reads the selected command directly, avoiding the failing Flue tool-result continuation.

The workers-pool harness now overrides classification deterministically and no longer connects to the remote AI binding during integration tests. The existing classifier Durable Object remains in the production build for migration compatibility.

Related to #1623, where the failed bot command was observed. This PR does not close that issue.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

Typecheck note: the bot package's existing wrangler types output lacks the generated Flue Durable Object and service-binding RPC types, so the full package typecheck fails across the existing DO tests and routes. It reports no errors in the changed classifier files.

i18n is not applicable because this does not change admin UI. A changeset is not applicable because infra/emdash-bot is private infrastructure. A Discussion is not required for this bug fix.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: OpenAI Codex (GPT-5)

Screenshots / test output

Not visual.

  • pnpm build
  • pnpm -s lint:json | jq '.diagnostics | length'0
  • pnpm --filter @emdash-cms/emdash-bot test:unit → 220 passed
  • pnpm --filter @emdash-cms/emdash-bot test:workers → 46 passed
  • pnpm --filter @emdash-cms/emdash-bot build
  • Wrangler type generation against wrangler.test.jsonc

Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks fix/bot-direct-classifier. Updated automatically when the playground redeploys.

Copilot AI lite review requested due to automatic review settings August 17, 2026 11:59
@changeset-bot

changeset-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7d00830

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions Bot added size/L review/needs-review No maintainer or bot review yet labels Aug 17, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 7d00830 Aug 17 2026, 12:03 PM

@pkg-pr-new

pkg-pr-new Bot commented Aug 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2528

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2528

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2528

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2528

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2528

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2528

emdash

npm i https://pkg.pr.new/emdash@2528

create-emdash

npm i https://pkg.pr.new/create-emdash@2528

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2528

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2528

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2528

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2528

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2528

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2528

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2528

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2528

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2528

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2528

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2528

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2528

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2528

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2528

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2528

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2528

commit: 7d00830

@ascorbic
ascorbic enabled auto-merge (squash) August 17, 2026 12:02
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do 7d00830 Aug 17 2026, 12:02 PM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 7d00830 Aug 17 2026, 12:03 PM

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the @emdashbot comment classifier flow to avoid silently dropping free-text commands by performing a single typed Workers AI function-call request and reading the chosen command directly, rather than relying on Flue tool-result continuation. It also makes workers-pool integration tests deterministic by overriding classification in the test entrypoint and removing the need to bind to remote AI during those tests.

Changes:

  • Replaced Flue dispatch/read-based classification with one Workers AI run() call that returns a select_command tool call payload.
  • Added an overridable requestClassification() hook on OrchestratorDO, and used it in the workers-pool test entrypoint to force deterministic classifier errors without remote inference.
  • Updated unit/integration tests and test wrangler config to align with the new classifier request path.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
infra/emdash-bot/wrangler.test.jsonc Removes the AI binding from the workers-pool test config now that classification is overridden in the test entrypoint.
infra/emdash-bot/tests/unit/classifier-client.test.ts Reworks unit tests to mock Workers AI tool-call outputs and validate new error modes.
infra/emdash-bot/tests/integration/webhook.test.ts Updates integration test commentary to reflect deterministic classifier failure behavior.
infra/emdash-bot/tests/integration/_entry.ts Overrides OrchestratorDO.requestClassification() to avoid remote inference and keep workers-pool tests deterministic.
infra/emdash-bot/.flue/lib/orchestrator.ts Routes classification through a new requestClassification() method (defaulting to Workers AI) to enable test overrides.
infra/emdash-bot/.flue/lib/classifier-client.ts Implements single-call Workers AI tool-call classification and parses select_command tool arguments into ClassifyResult.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +132 to 140
function selectCommandArguments(response: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Output): string | null {
if (typeof response !== "object" || response === null || !("choices" in response)) return null;
const choice = response.choices?.[0];
if (!choice || !("message" in choice)) return null;
const toolCalls = choice.message?.tool_calls;
return (
toolCalls?.find((call) => call.function.name === "select_command")?.function.arguments ?? null
);
}

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right fix for the reported symptom. Routing free-text comments through a full Flue 2 agent round-trip introduced a brittle persistence/read step that could silently drop the selected command. Doing a single typed Workers AI function call inline is simpler, stateless where it needs to be, and lets the orchestrator read the result directly. I checked the changed classifier client, orchestrator integration, test harness, worker type definitions, and machine states; the implementation is internally consistent and the test override keeps workers-pool tests hermetic.

The only real concern is that the new classifier prompt/tool definition is looser than the old Flue agent's. The old agent listed none explicitly in the action list and constrained event to the known choices plus none in the tool input schema (and enforced reasoning length). The new code validates those things only after the model returns, which makes it easier for the model to emit responses that then turn into retry-causing errors. I left one suggestion to tighten the tool JSON schema and add the explicit none action to the prompt.

Aside from that, the code looks correct: the direct env.AI.run() call matches the generated Workers AI overloads, selectCommandArguments correctly extracts the function arguments, resolveClassification reuses the existing valibot schema, and the workers-pool entry overrides requestClassification so integration tests don't reach remote inference.

Comment on lines +76 to +125
const actionList = commands
.map(
(command) =>
`- ${command.event}: ${command.description}${command.arg ? ` Set arg to the ${command.arg}.` : ""}`,
)
.join("\n");
return {
messages: [
{
role: "system",
content: [
"Route the comment to exactly one available action.",
"The state only limits the available list; every listed action is valid.",
"Call select_command exactly once. Prefer none over guessing. Do not answer with prose.",
].join(" "),
},
{
role: "user",
content: [
`Issue: ${input.issueNumber}`,
`State: ${input.state ?? "unmanaged"}`,
"Available actions:",
actionList,
"Bot's last message:",
input.botContext?.trim() || "(none)",
"Comment:",
input.comment,
].join("\n"),
},
],
tools: [
{
type: "function",
function: {
name: "select_command",
description: "Return the single command intended by the comment, or none.",
parameters: {
type: "object",
properties: {
event: { type: "string", description: "The selected action or none" },
arg: { type: "string", description: "The directive for the action, if any" },
reasoning: {
type: "string",
description: "A short reason quoting the decisive phrase",
},
},
required: ["event", "reasoning"],
},
},
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The new classifier prompt and tool schema are weaker than the old Flue agent's, which can undermine the reliability this PR is trying to restore.

The old ClassifyCommand agent explicitly listed - \none`: no actionable intent matches the available actionsin the prompt and constrained the tool inputeventto the known choices plusnone; it also enforced reasoninglength. The new code only validates those constraints after the model returns viaresolveClassification`, so the model is more likely to return invalid events or out-of-range reasoning that become retries/errors.

Tighten the tool schema and prompt to match the old guidance:

Suggested change
const actionList = commands
.map(
(command) =>
`- ${command.event}: ${command.description}${command.arg ? ` Set arg to the ${command.arg}.` : ""}`,
)
.join("\n");
return {
messages: [
{
role: "system",
content: [
"Route the comment to exactly one available action.",
"The state only limits the available list; every listed action is valid.",
"Call select_command exactly once. Prefer none over guessing. Do not answer with prose.",
].join(" "),
},
{
role: "user",
content: [
`Issue: ${input.issueNumber}`,
`State: ${input.state ?? "unmanaged"}`,
"Available actions:",
actionList,
"Bot's last message:",
input.botContext?.trim() || "(none)",
"Comment:",
input.comment,
].join("\n"),
},
],
tools: [
{
type: "function",
function: {
name: "select_command",
description: "Return the single command intended by the comment, or none.",
parameters: {
type: "object",
properties: {
event: { type: "string", description: "The selected action or none" },
arg: { type: "string", description: "The directive for the action, if any" },
reasoning: {
type: "string",
description: "A short reason quoting the decisive phrase",
},
},
required: ["event", "reasoning"],
},
},
},
const actionList = [
...commands.map(
(command) =>
`- ${command.event}: ${command.description}${command.arg ? ` Set arg to the ${command.arg}.` : ""}`,
),
"- none: no actionable intent matches the available actions",
].join("\n");
Suggested change
const actionList = commands
.map(
(command) =>
`- ${command.event}: ${command.description}${command.arg ? ` Set arg to the ${command.arg}.` : ""}`,
)
.join("\n");
return {
messages: [
{
role: "system",
content: [
"Route the comment to exactly one available action.",
"The state only limits the available list; every listed action is valid.",
"Call select_command exactly once. Prefer none over guessing. Do not answer with prose.",
].join(" "),
},
{
role: "user",
content: [
`Issue: ${input.issueNumber}`,
`State: ${input.state ?? "unmanaged"}`,
"Available actions:",
actionList,
"Bot's last message:",
input.botContext?.trim() || "(none)",
"Comment:",
input.comment,
].join("\n"),
},
],
tools: [
{
type: "function",
function: {
name: "select_command",
description: "Return the single command intended by the comment, or none.",
parameters: {
type: "object",
properties: {
event: { type: "string", description: "The selected action or none" },
arg: { type: "string", description: "The directive for the action, if any" },
reasoning: {
type: "string",
description: "A short reason quoting the decisive phrase",
},
},
required: ["event", "reasoning"],
},
},
},
properties: {
event: {
type: "string",
enum: [...commands.map((command) => command.event), "none"],
description: "The selected action or none",
},
arg: { type: "string", description: "The directive for the action, if any" },
reasoning: {
type: "string",
minLength: 3,
maxLength: 400,
description: "A short reason quoting the decisive phrase",
},
},

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Aug 17, 2026
@ascorbic
ascorbic merged commit 87d201f into main Aug 17, 2026
52 checks passed
@ascorbic
ascorbic deleted the fix/bot-direct-classifier branch August 17, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/awaiting-author Reviewed; waiting on the author to respond size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants