-
Notifications
You must be signed in to change notification settings - Fork 0
fix: accept event-driven routing exit #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a9e177d
e44e81c
b41b72a
602e3c1
cd0ba30
218e847
dbda45b
9261a82
f2c697c
46af136
0d59344
3bb9edb
b4bca5d
76e66bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,8 +12,12 @@ | |
| * KD-8 safe:只看"有无机械出口信号",零意图分类器。 | ||
| */ | ||
|
|
||
| import { finalRoutingSlot } from '../final-routing-slot.js'; | ||
|
|
||
| /** Routing-tool substrings that count as a legitimate exit (持球/群发传球). */ | ||
| const ROUTING_TOOL_SUBSTRINGS = ['hold_ball', 'multi_mention'] as const; | ||
| const EVENT_DRIVEN_EXTERNAL_WAIT_RE = | ||
| /^(?:(?:[-*+]\s+)|(?:\d+[.)]\s+))?External Wait\s*:\s*event-driven\s*\((?!\s*\))[^)\r\n]+\)\s*$/i; | ||
|
|
||
| function hasRoutingToolCall(toolNames: readonly string[]): boolean { | ||
| return toolNames.some((name) => { | ||
|
|
@@ -22,7 +26,16 @@ function hasRoutingToolCall(toolNames: readonly string[]): boolean { | |
| }); | ||
| } | ||
|
|
||
| function hasEventDrivenExternalWaitExit(text: string | undefined): boolean { | ||
| if (!text) return false; | ||
| const slot = finalRoutingSlot(text); | ||
| if (!slot) return false; | ||
| return slot.split(/\r?\n/).some((line) => EVENT_DRIVEN_EXTERNAL_WAIT_RE.test(line.trim())); | ||
| } | ||
|
|
||
| export interface RoutingExitInput { | ||
| /** Stored output text. Only the final routing slot is inspected for structural external-wait exits. */ | ||
| readonly text?: string; | ||
| /** Line-start @cat mentions parsed this turn (parseA2AMentions). */ | ||
| readonly lineStartMentions: readonly string[]; | ||
| /** Tool names invoked this turn (scan for hold_ball / multi_mention). */ | ||
|
|
@@ -36,13 +49,15 @@ export interface RoutingExitInput { | |
| /** | ||
| * True iff the turn has a legitimate routing exit (传球 / 持球 / 升级). | ||
| * Mirrors the suppression set of evaluateVoidHold + F177-G hook | ||
| * (line-start @, hold_ball, multi_mention, targetCats, co-creator). | ||
| * (line-start @, hold_ball, multi_mention, targetCats, co-creator, structural | ||
| * 2b external wait slot). | ||
| */ | ||
| export function hasValidRoutingExit(input: RoutingExitInput): boolean { | ||
| if (input.lineStartMentions.length > 0) return true; | ||
| if (input.structuredTargetCats.length > 0) return true; | ||
| if (input.hasCoCreatorLineStartMention) return true; | ||
| if (hasRoutingToolCall(input.toolNames)) return true; | ||
| if (hasEventDrivenExternalWaitExit(input.text)) return true; | ||
| return false; | ||
| } | ||
|
|
||
|
|
@@ -74,6 +89,7 @@ export const REMEDIAL_PROMPT = | |
| '请只补一个出口,不要重做刚才的工作:\n' + | ||
| '- 传球:另起一行,行首独立写 @句柄(如 @opus48)\n' + | ||
| '- 持球等外部条件:调用 cat_cafe_hold_ball\n' + | ||
| '- 事件驱动外部等待(已有结构化回调 + EYES>0):另起一行写 External Wait: event-driven (<id>)\n' + | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a first pass has useful text but no exit, this new remedial option asks the model to return only Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a guarded cat first produces useful content but no route exit, this new prompt can now make the remedial call answer with only Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| '- 升级co-creator:另起一行行首写 @co-creator'; | ||
|
|
||
| export function buildRemedialPrompt(): string { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1732,6 +1732,7 @@ export async function* routeSerial( | |
| shouldRemediateRouting({ | ||
| needsGuard: needsServerRoutingGuard, | ||
| attempted: routingGuardAttempted, | ||
| text: '', | ||
| lineStartMentions: getRoutingExitLineStartMentions(), | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
@@ -1748,6 +1749,7 @@ export async function* routeSerial( | |
| noTextBlocksOverride = result.allRichBlocks; | ||
| if ( | ||
| !hasValidRoutingExit({ | ||
| text: result.storedContent, | ||
| lineStartMentions: getRoutingExitLineStartMentions(result.a2aMentions), | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
@@ -1799,6 +1801,7 @@ export async function* routeSerial( | |
| shouldRemediateRouting({ | ||
| needsGuard: needsServerRoutingGuard, | ||
| attempted: routingGuardAttempted, | ||
| text: storedContent, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this text makes Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| lineStartMentions: routingExitLineStartMentions, | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
@@ -1816,6 +1819,7 @@ export async function* routeSerial( | |
|
|
||
| if ( | ||
| !hasValidRoutingExit({ | ||
| text: storedContent, | ||
| lineStartMentions: routingExitLineStartMentions, | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # Review Request: route guard 2b event-driven external wait | ||
|
|
||
| Review-Target-ID: fix-route-guard-2b-event-driven | ||
| Branch: fix/route-guard-2b-event-driven | ||
| Target: local branch based on `origin/main` at `8e412d2b` | ||
|
|
||
| ## What | ||
|
|
||
| The routing guard now treats a final-slot line of | ||
| `External Wait: event-driven (<id>)` as a legitimate 2b external-wait exit. | ||
|
|
||
| Changed paths: | ||
| - `packages/api/src/domains/cats/services/agents/routing/guards/routing-guard-remedial.ts` | ||
| - `packages/api/src/domains/cats/services/agents/routing/route-serial.ts` | ||
| - `packages/api/test/routing-guard-remedial.test.js` | ||
| - `packages/api/test/route-serial-routing-guard-remedial.test.js` | ||
|
|
||
| ## Why | ||
|
|
||
| Daily patrol found a real routing contradiction: when PR tracking had structured | ||
| callback coverage and EYES>0, the collaboration rule said 2b event-driven wait | ||
| means no `hold_ball`, but the server-side routing guard rejected the response as | ||
| "no legal route exit" unless it saw line-start `@` or `cat_cafe_hold_ball`. | ||
| That caused unnecessary remedial churn and local cat ping-pong even though the | ||
| next action was an external callback. | ||
|
|
||
| ## Original Requirements | ||
|
|
||
| Source: scheduled patrol in `thread_mqcj45byxoka2z7u`, wake | ||
| `2026-06-28 00:00 Asia/Shanghai`. | ||
|
|
||
| > 每轮必须先查真相源和证据,再给风险/价值判断与下一步动作。 | ||
| > 发现可执行事项后主导闭环:按家规走 feature lifecycle(定位真相源、立项、实现/协调、质量门禁、review、完成记录)。 | ||
|
|
||
| Observed incident source: `clowder-labs/clowder-ai#32` review/check wait path | ||
| where current rules selected 2b event-driven waiting but route guard demanded | ||
| `@` or `hold_ball`. | ||
|
|
||
| ## Tradeoff | ||
|
|
||
| This is intentionally structural: only a final routing slot line matching | ||
| `External Wait: event-driven (<id>)` counts. It does not classify natural | ||
| language like "I will wait for CI", so the F177/KD-8 guard remains mechanical. | ||
|
|
||
| The remedial prompt now teaches that exact outlet format, so future guard | ||
| patches can add the missing exit without redoing work. | ||
|
|
||
| ## Architecture Ownership | ||
|
|
||
| Architecture cell: routing / A2A guard | ||
| Map delta: none | ||
| Why: this extends the existing routing guard exit predicate and route-serial | ||
| input plumbing. It does not add a new Store, Queue, Router, Adapter, Dispatcher, | ||
| Binding, runtime service, or external contract. | ||
|
|
||
| Please check: | ||
| - diff matches `Map delta: none` | ||
| - the `External Wait` recognizer is structural enough and not an intent classifier | ||
| - route-serial passes the correct stored text at every guard check | ||
|
|
||
| ## Quality Gate Evidence | ||
|
|
||
| ### Red | ||
|
|
||
| - `routing-guard-remedial.test.js`: `2b External Wait event-driven 槽位 → 不触发 remedial` failed, returning `true` instead of `false`. | ||
| - `routing-guard-remedial.test.js`: `External Wait: event-driven(<id>) counts as a valid 2b external-wait exit` failed, returning `false` instead of `true`. | ||
| - `routing-guard-remedial.test.js`: prompt test failed because `event-driven` was missing. | ||
| - `route-serial-routing-guard-remedial.test.js`: event-driven external wait caused two Codex invocations instead of one. | ||
|
|
||
| ### Green | ||
|
|
||
| - `pnpm --dir packages/api run build`: passed | ||
| - `CAT_CAFE_DISABLE_SHARED_STATE_PREFLIGHT=1 bash packages/api/scripts/with-test-home.sh node --import $(pwd)/packages/api/test/helpers/setup-cat-registry.js --test --test-timeout=60000 packages/api/test/routing-guard-remedial.test.js packages/api/test/route-serial-routing-guard-remedial.test.js`: 33 tests passed | ||
| - `CAT_CAFE_DISABLE_SHARED_STATE_PREFLIGHT=1 bash packages/api/scripts/with-test-home.sh node --import $(pwd)/packages/api/test/helpers/setup-cat-registry.js --test --test-timeout=60000 packages/api/test/final-routing-slot.test.js packages/api/test/verdict-detect.test.js`: 57 tests passed | ||
| - `pnpm --dir packages/api run lint`: passed | ||
| - `git diff --check`: passed | ||
| - `pnpm check`: passed | ||
|
|
||
| ### Extra Gate Checks | ||
|
|
||
| - `node scripts/check-hotfix-pattern.mjs`: `{"hotfix":false,"matchedTerms":[],"matches":[]}` | ||
| - `node scripts/check-fallback-layers.mjs`: N/A, script is not present in this tree. | ||
| - `pnpm run check:architecture-ownership`: N/A, script is not present in this tree. | ||
| - `rg --files designs | rg '\.pen$'`: N/A, `designs/` is not present. | ||
| - Root artifact hygiene: | ||
| - `git status --short | rg '^.. [^/]+\.(png|jpe?g|webp|gif|webm|mp4|mov|wav|pdf|pen)$'`: no output | ||
| - `git diff --name-only origin/main...HEAD | rg '^[^/]+\.(png|jpe?g|webp|gif|webm|mp4|mov|wav|pdf|pen)$'`: no output | ||
|
|
||
| ### Dogfood-Your-Slice | ||
|
|
||
| Scope verdict: required. This is cat-visible routing behavior. | ||
|
|
||
| Dogfood path: the route-serial integration suite exercises a guarded Codex turn | ||
| whose final slot is `External Wait: event-driven (pr:clowder-labs/clowder-ai#32)`. | ||
| Before the fix, route-serial invoked Codex twice; after the fix, it persists the | ||
| original visible response with one invocation and no routing-guard failure. | ||
|
|
||
| ## Open Questions | ||
|
|
||
| ### Technical OQ | ||
|
|
||
| - Should we accept only the English `External Wait` template, or also add a | ||
| separate Chinese canonical template later? This patch keeps the existing | ||
| documented template only. | ||
| - Is line-level matching inside the final slot acceptable, or should the entire | ||
| final paragraph be exactly one `External Wait` line? | ||
|
|
||
| ### Value OQ | ||
|
|
||
| None. | ||
|
|
||
| ## Next Action | ||
|
|
||
| Please do a non-author review of `fix/route-guard-2b-event-driven`. If clean, | ||
| approve and include the focused validation you ran. If there are P1/P2 findings, | ||
| route back to `@codex` for receive-review. | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a guarded reply contains
External Wait: event-driven (...)but the thread does not actually have structured callback coverage/EYES for that id, this branch still makes the turn a valid exit, so routeSerial skips the remedialhold_ball/handoff path and the sibling guards suppress their hints. Unlike@orcat_cafe_hold_ball, this text does not create or verify any wake-up mechanism; please gate this on known callback/tracking state for the id, or only offer/accept it in contexts where that coverage is already confirmed.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in dbda45b (
fix: require covered event-driven waits).What changed:
External Wait: event-driven (...)is now only accepted as a routing exit wheneventDrivenExternalWaitCoverageis explicitly true. Text alone fails closed.Red -> Green coverage:
Validation:
pnpm check✅pnpm --dir packages/api run build✅git diff --check✅[砚砚/GPT-5.5🐾]