Skip to content
This repository was archived by the owner on Jul 20, 2026. It is now read-only.

promote: v0.7.13 — Quinn e2e fixes + triage pipeline polish#367

Merged
mabry1985 merged 5 commits into
mainfrom
dev
Apr 16, 2026
Merged

promote: v0.7.13 — Quinn e2e fixes + triage pipeline polish#367
mabry1985 merged 5 commits into
mainfrom
dev

Conversation

@mabry1985

@mabry1985 mabry1985 commented Apr 16, 2026

Copy link
Copy Markdown
Member

Summary

Test plan

  • CI green
  • Post-deploy: rebuild + recreate, verify Quinn no GeneratorExit + push callback URL uses workstacean:3000

Summary by CodeRabbit

  • Bug Fixes

    • Prevent duplicate GitHub completion comments from being posted
    • Skip duplicate auto-triage dispatches within 60-second time windows
  • Chores

    • Bumped package version to 0.7.13

github-actions Bot and others added 5 commits April 16, 2026 02:33
Acknowledges main's squash commit as an ancestor to prevent phantom conflicts on the next dev→main promotion.
…egistry (#364)

Three low-complexity board items in one commit:

1. GitHub webhook dedup — track recently-dispatched (owner/repo#number)
   tuples with a 60s window; skip duplicate issues.opened events.
   Root cause of Quinn's 4x comment on issue #359.

2. Suppress raw "Skill X completed by Y" echo — the outbound handler
   was posting the SkillResult stub as a GitHub comment alongside
   Quinn's own triage analysis. Now filters out single-line
   "completed by" stubs.

3. Populate blast-v1 registry from agent cards — same card-refresh
   pattern as hitl-mode-v1 (already shipped). Adds clearAgent() to
   BlastRegistry, parses capabilities.extensions[blast-v1].params.skills
   in SkillBrokerPlugin._loadBlastDeclarations(). x-blast-radius
   metadata stamping now actually fires for agents that opt in.

Closes feature-1776307294155-3rqjqdmoi (webhook dedup).
Closes feature-1776303349701-g7olxwk09 (blast registry).

Co-authored-by: Automaker <automaker@localhost>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes discovered during end-to-end Quinn testing:

1. A2AExecutor._executeStream now breaks out of the SSE loop when it
   receives a non-terminal `task` event (submitted / working /
   input-required). Holding the stream open for agents that don't push
   incremental events crashes their SSE generator when the sync caller
   times out (observed in Quinn: GeneratorExit mid-yield, polluting
   logs with OpenTelemetry context-detach errors on every long task).
   TaskTracker.pollTask handles completion for these tasks cleanly.

2. SkillDispatcherPlugin picks the push-notification callback base URL
   based on whether the target agent is docker-internal (single-label
   hostname) or external (FQDN/IP). Internal agents get
   WORKSTACEAN_INTERNAL_BASE_URL (default http://workstacean:3000)
   which resolves on the shared docker network; external agents get
   WORKSTACEAN_BASE_URL (operator-configured public/Tailscale URL).
   Before this fix, `ava:8081` only resolved over Tailscale, so all
   docker-internal agents (Quinn, Jon, Researcher) silently failed to
   deliver push callbacks.

Expose A2AExecutor.url as a public getter for the dispatcher's heuristic.

Closes feature-1776309307266-pk9kwzc67 (streaming GeneratorExit).
Closes feature-1776309298112-tltlsypow (push callback URL).

Co-authored-by: Automaker <automaker@localhost>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes include version bumps across packages and several feature additions to core components. GitHub plugin adds message filtering and auto-triage deduplication with a 60-second window; A2A executor exposes its configured URL and modifies streaming behavior to break early for non-terminal task states; BlastRegistry gains agent-specific cache clearing; callback routing now detects docker-internal versus external agents; and SkillBrokerPlugin integrates blast declaration loading into its card-discovery flow.

Changes

Cohort / File(s) Summary
Version Bumps
dashboard/package.json, package.json
Incremented package version from 0.7.12 to 0.7.13 in both files.
GitHub Plugin Enhancements
lib/plugins/github.ts
Added outbound comment filtering (regex: ^Skill ".+" completed by \w+$) to prevent posting completion-style messages; introduced auto-triage deduplication with recentDispatches Map and 60-second DEDUP_WINDOW_MS to skip duplicate owner/repo#number dispatch keys.
A2A Executor Updates
src/executor/executors/a2a-executor.ts
Added public url getter exposing this.config.url; modified _executeStream to break SSE loop for non-terminal task states (submitted, working, input-required) on initial task event, deferring completion to polling-based TaskTracker.
Blast Registry Extension
src/executor/extensions/blast.ts
Added clearAgent(agentName: string): void method to remove all registry entries with composite keys beginning with ${agentName}::.
Callback URL Routing
src/executor/skill-dispatcher-plugin.ts
Replaced static process.env.WORKSTACEAN_BASE_URL assignment with _pickCallbackBaseUrl(a2aExecutor.url) helper that detects docker-internal agents (no dot / no port in hostname) and routes to WORKSTACEAN_INTERNAL_BASE_URL or WORKSTACEAN_BASE_URL accordingly.
Skill Broker Blast Integration
src/plugins/skill-broker-plugin.ts
Imported blast extension support; extended _discoverSkills() to call new _loadBlastDeclarations(agentName, card), which clears prior agent declarations, locates the BLAST_URI capability extension, validates skill/radius entries from ext.params.skills, and declares each into defaultBlastRegistry.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A version hops from twelve to thirteen bright,
While plugins filter spam and deduplicate right,
URLs bloom with docker-internal cheer,
Blast registries clear, the blast declarations appear! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: version promotion to v0.7.13 with focus on Quinn e2e fixes (streaming early-break, push-callback URL routing) and triage pipeline polish (webhook deduplication, blast registry population, comment suppression).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands and usage tips.

@protoquinn protoquinn Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Auto-approved by Quinn: safe PR class (dependabot/renovate/promote/chore(deps)) with passing CI.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
lib/plugins/github.ts (1)

556-574: recentDispatches map grows unbounded — consider periodic cleanup.

Entries are added but never removed. Over long-running deployments, this will accumulate one entry per unique issue ever processed. A simple fix is to prune stale entries during the dedup check.

♻️ Proposed fix — prune stale entries inline
   private _handleAutoTriage(
     event: string,
     payload: Record<string, unknown>,
     ctx: GitHubEventContext,
     autoTriage: AutoTriageConfig,
     bus: EventBus,
     getToken: (owner: string, repo: string) => Promise<string>,
   ): void {
     const dedupKey = `${ctx.owner}/${ctx.repo}#${ctx.number}`;
+    const now = Date.now();
+
+    // Prune stale entries to bound memory growth
+    for (const [key, ts] of this.recentDispatches) {
+      if (now - ts >= GitHubPlugin.DEDUP_WINDOW_MS) {
+        this.recentDispatches.delete(key);
+      }
+    }
+
     const lastDispatched = this.recentDispatches.get(dedupKey);
-    if (lastDispatched && Date.now() - lastDispatched < GitHubPlugin.DEDUP_WINDOW_MS) {
-      console.log(`[github] Auto-triage: skipping duplicate ${dedupKey} (dispatched ${Date.now() - lastDispatched}ms ago)`);
+    if (lastDispatched && now - lastDispatched < GitHubPlugin.DEDUP_WINDOW_MS) {
+      console.log(`[github] Auto-triage: skipping duplicate ${dedupKey} (dispatched ${now - lastDispatched}ms ago)`);
       return;
     }
-    this.recentDispatches.set(dedupKey, Date.now());
+    this.recentDispatches.set(dedupKey, now);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/plugins/github.ts` around lines 556 - 574, recentDispatches is never
pruned so it will grow unbounded; inside _handleAutoTriage where dedupKey is
computed and lastDispatched checked against GitHubPlugin.DEDUP_WINDOW_MS, add
logic to remove stale map entries: iterate recentDispatches and delete any
entries whose timestamp is older than DEDUP_WINDOW_MS (or a multiple like 2x)
before setting the current dedupKey timestamp, ensuring the map is compacted
periodically during dedupe checks.
src/plugins/skill-broker-plugin.ts (1)

329-340: Avoid duplicating BlastRadius literals in Line 329 validation.

The local validRadii set can drift from BlastRadius/blast ordering definitions and silently reject newly added valid radii later. Derive allowed values from blast.ts as a single source of truth.

♻️ Proposed refactor
 import {
   defaultBlastRegistry,
   BLAST_URI,
+  BLAST_ORDER,
   type BlastRadius,
 } from "../executor/extensions/blast.ts";
+
+const VALID_BLAST_RADII = new Set<BlastRadius>(
+  Object.keys(BLAST_ORDER) as BlastRadius[],
+);

@@
-    const validRadii = new Set(["self", "project", "repo", "fleet", "public"]);
     let declared = 0;

@@
-      if (typeof entry.radius !== "string" || !validRadii.has(entry.radius)) continue;
+      if (
+        typeof entry.radius !== "string" ||
+        !VALID_BLAST_RADII.has(entry.radius as BlastRadius)
+      ) continue;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/skill-broker-plugin.ts` around lines 329 - 340, The code
currently hardcodes validRadii as a Set and risks drifting from the actual
BlastRadius definitions; replace the local Set with the canonical list exported
from blast.ts (e.g., derive allowed values from the BlastRadius enum or the
exported radii array) and use that when validating entry.radius inside the loop
that calls defaultBlastRegistry.declare (retain the same checks and the call to
defaultBlastRegistry.declare with radius: entry.radius as BlastRadius). Ensure
you import the correct symbol (BlastRadius or the exported radii list) from
blast.ts and use it to build the validation Set so new radii added in blast.ts
are accepted automatically.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/plugins/github.ts`:
- Line 331: The if-statement that tests content using the regex matching "Skill
... completed by ..." currently uses \w+ for the agent name and therefore
rejects names with hyphens; update that regex on the line containing the /^Skill
".+" completed by \w+$/i.test(content) check so the final token allows hyphens
(for example replace the \w+ token with a character class that permits word
characters and hyphens or with a non-space token) so agent names like
"proto-agent" are matched and the early return works correctly.

In `@src/executor/executors/a2a-executor.ts`:
- Around line 466-475: The early break in src/executor/executors/a2a-executor.ts
stops SSE consumption for any non-terminal taskState, which can drop handling
when the first state is "input-required"; update the logic so "input-required"
is treated as non-terminal that must keep the stream open (i.e., do not break
when taskState === "input-required"), and align this change with the dispatcher
by adding "input-required" to the tracked states in
src/executor/skill-dispatcher-plugin.ts (the arrays/conditions around the
existing "submitted"/"working" checks). Ensure the SSE break condition only
triggers for terminal states ("completed","failed","canceled","rejected") or
when the dispatcher explicitly tracks the non-terminal state.

---

Nitpick comments:
In `@lib/plugins/github.ts`:
- Around line 556-574: recentDispatches is never pruned so it will grow
unbounded; inside _handleAutoTriage where dedupKey is computed and
lastDispatched checked against GitHubPlugin.DEDUP_WINDOW_MS, add logic to remove
stale map entries: iterate recentDispatches and delete any entries whose
timestamp is older than DEDUP_WINDOW_MS (or a multiple like 2x) before setting
the current dedupKey timestamp, ensuring the map is compacted periodically
during dedupe checks.

In `@src/plugins/skill-broker-plugin.ts`:
- Around line 329-340: The code currently hardcodes validRadii as a Set and
risks drifting from the actual BlastRadius definitions; replace the local Set
with the canonical list exported from blast.ts (e.g., derive allowed values from
the BlastRadius enum or the exported radii array) and use that when validating
entry.radius inside the loop that calls defaultBlastRegistry.declare (retain the
same checks and the call to defaultBlastRegistry.declare with radius:
entry.radius as BlastRadius). Ensure you import the correct symbol (BlastRadius
or the exported radii list) from blast.ts and use it to build the validation Set
so new radii added in blast.ts are accepted automatically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4819fc9-4327-4bf1-bf6b-9dce7122c787

📥 Commits

Reviewing files that changed from the base of the PR and between ee1828a and c1dfbc7.

📒 Files selected for processing (7)
  • dashboard/package.json
  • lib/plugins/github.ts
  • package.json
  • src/executor/executors/a2a-executor.ts
  • src/executor/extensions/blast.ts
  • src/executor/skill-dispatcher-plugin.ts
  • src/plugins/skill-broker-plugin.ts

Comment thread lib/plugins/github.ts

const content = String((msg.payload as Record<string, unknown>).content ?? "").trim();
if (!content) return;
if (/^Skill ".+" completed by \w+$/i.test(content)) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Regex won't match agent names containing hyphens.

The pattern \w+ only matches [a-zA-Z0-9_]. If this.config.name contains a hyphen (e.g., "proto-agent"), the regex fails and the "echo" comment is posted anyway.

🐛 Proposed fix
-      if (/^Skill ".+" completed by \w+$/i.test(content)) return;
+      if (/^Skill ".+" completed by [\w-]+$/i.test(content)) return;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (/^Skill ".+" completed by \w+$/i.test(content)) return;
if (/^Skill ".+" completed by [\w-]+$/i.test(content)) return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/plugins/github.ts` at line 331, The if-statement that tests content using
the regex matching "Skill ... completed by ..." currently uses \w+ for the agent
name and therefore rejects names with hyphens; update that regex on the line
containing the /^Skill ".+" completed by \w+$/i.test(content) check so the final
token allows hyphens (for example replace the \w+ token with a character class
that permits word characters and hyphens or with a non-space token) so agent
names like "proto-agent" are matched and the early return works correctly.

Comment on lines +466 to +475
// Non-terminal state (submitted / working / input-required) on the
// initial task event → stop consuming the SSE stream and let
// TaskTracker drive the task to completion via tasks/get polling.
// Holding the stream open longer forces slow agents to keep their
// SSE generator alive for minutes, which crashes them when the
// sync caller eventually times out and closes the connection.
if (taskState !== "completed" && taskState !== "failed"
&& taskState !== "canceled" && taskState !== "rejected") {
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Non-terminal early break can drop input-required task handling.

At Line [466]-[475], the stream now breaks on all non-terminal task states. But the dispatcher currently tracks only submitted/working (src/executor/skill-dispatcher-plugin.ts, Line [28] and Line [319]). If the first state is input-required (or another non-terminal), it can bypass tracking/polling and be treated as completed too early.

Suggested alignment fix (dispatcher side)
-const WORKING_STATES = new Set(["submitted", "working"]);
+const NON_TERMINAL_STATES = new Set(["submitted", "working", "input-required"]);
...
-        && WORKING_STATES.has(taskState)
+        && NON_TERMINAL_STATES.has(taskState)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/executor/executors/a2a-executor.ts` around lines 466 - 475, The early
break in src/executor/executors/a2a-executor.ts stops SSE consumption for any
non-terminal taskState, which can drop handling when the first state is
"input-required"; update the logic so "input-required" is treated as
non-terminal that must keep the stream open (i.e., do not break when taskState
=== "input-required"), and align this change with the dispatcher by adding
"input-required" to the tracked states in
src/executor/skill-dispatcher-plugin.ts (the arrays/conditions around the
existing "submitted"/"working" checks). Ensure the SSE break condition only
triggers for terminal states ("completed","failed","canceled","rejected") or
when the dispatcher explicitly tracks the non-terminal state.

@mabry1985
mabry1985 merged commit 178d1eb into main Apr 16, 2026
8 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant