Skip to content

feat(ingress): ingress layer v1 — chat routing config actor + resolver + 4 text entries + /ws/voice (#691–#696)#699

Merged
eanzhao merged 266 commits into
devfrom
feature/router
May 22, 2026
Merged

feat(ingress): ingress layer v1 — chat routing config actor + resolver + 4 text entries + /ws/voice (#691–#696)#699
eanzhao merged 266 commits into
devfrom
feature/router

Conversation

@eanzhao
Copy link
Copy Markdown
Contributor

@eanzhao eanzhao commented May 19, 2026

Summary

Lands Ingress layer v1 (milestone #20) as one PR. Closes #672, #674, and the per-phase trackers #691 #692 #693 #694 #695 #696.

Originally Phase 0 only (proto + ADR). After review feedback we collapsed the entire 6-PR train (#703 #704 #705 #709 #710) onto feature/router so the ingress layer ships in a single reviewable diff against dev.

What this PR contains

  • Phase 0 — proto + ADR. src/Aevatar.ChatRouting.Abstractions (chat_route_policy.proto, typed enums) and ADR-0024 — the form is "config actor + stateless boundary resolver + readmodel", explicitly not a router actor (rejected per Refactor ChatRuntime to match Harness boundary (#568) — blocks NyxID-fronted Responses API gateway #608 Harness review).
  • Phase 1 — config actor + projection. agents/Aevatar.GAgents.ChatRouting:
    • ChatRoutePolicyGAgent (per-scope config aggregate; validates default_target, sorts rules by priority).
    • ChatRoutePolicyCurrentStateDocument + ChatRoutePolicyCurrentStateProjector (overwrite-materializes committed state, version from the committed StateEvent).
    • AddChatRoutingAgents wires materialization runtime + materializer + InMemory document store; codex caught the missing materializer registration during review and the fix is included here.
  • Phase 2 — stateless resolver + query port + env fallback. src/Aevatar.ChatRouting.Core:
    • ChatRouteResolver — pure Resolve(snapshot, input), priority-ordered rules, records matched_rule_id, falls through to default and env fallback. Hardened during review to skip rules with empty actions (proto3 message field can be null).
    • IChatRoutePolicyQueryPort + ChatRoutePolicyQueryPort — readmodel-only lookup, null snapshot when no rows.
    • IChatRouteFallbackProvider + env-driven EnvChatRouteFallbackProvider.
    • AddChatRoutingCore() DI, wired in the Mainnet host.
  • Phase 3 — text ingress entries. Resolver wired into the four text paths with no new actor hop:
    • NyxIdChat create (NyxIdChatEndpoints) — ForwardToGAgent overrides the target; Reject → 403. Review feedback fix: rollback only destroys the actor we created locally, never a forwarded routed target.
    • Responses + Messages endpoints — resolve after caller-scope lookup; ForwardToModel rewrites the model.
    • Relay ConversationGAgent — resolves inside the turn before runner admission; decision rides the typed NeedsLlmReplyEvent.target_ref; reply_token stays actor-private. Review feedback fix: sender NyxID is now resolved at relay ingress (INyxIdCurrentUserResolver.ResolveCurrentUserIdAsync) and carried on a new typed TransportExtras.nyx_sender_user_id, so per-user channel policies actually match.
  • Phase 4 — policy-aware /ws/voice + dev bypass. PolicyAwareVoiceEndpoints:
    • /ws/voice resolves through query port + resolver with typed VoiceInput; pre-upgrade Reject → 403, ForwardToModel → 501, ForwardToGAgent → attach-permission + presence-session resolution. Review feedback fix: returns 503 (retryable) instead of 404 when the routed voice GAgent's module is still warming up, matching the dev bypass.
    • First MapVoicePresenceWebSocket mount on the Mainnet host: /ws/voice/{actorId} kept behind the new voice-dev authorization policy (requires voice:bypass scope or admin).
  • Phase 5 — docs closeout. Adds the "router = config actor + boundary resolver" entry to docs/canon/architecture-vocabulary.md, citing ADR-0024.

Architecture invariants honored

Invariant How
No router actor ChatRoutePolicyGAgent is config-only; resolver is a library function
Decision never persisted ChatRouteDecision is transient; not in any state proto
No metadata bag Every routing-influencing field is strong-typed (CLAUDE.md decision tree step 1)
Foundation does not depend on ChatRouting Aevatar.Foundation.VoicePresence reverse-dep grep clean
/ws/voice not /ws/chat ADR-0024 D4 spells out the binary-vs-JSON reason
External repos untouched No NyxID / chrono-* surface required

Codex review findings — all addressed in-PR

PR Severity Finding Fix commit
#703 P1 AddChatRoutingAgents didn't register the materializer — readmodel never populated a535222
#704 P2 Resolver NREed on matching rule with empty/null action 7fa8de8
#705 P1 Create rollback could destroy a pre-existing forwarded target actor 39e430a
#705 P2 Relay OwnerScope.NyxUserId came from registrationScopeId — per-user channel policies always missed 39e430a
#709 P2 /ws/voice returned 404 (not retryable) for cold voice module 4934a9b

Test plan

  • dotnet build aevatar.slnx — green
  • bash tools/docs/lint.sh — green, ADR-0024 included
  • bash tools/ci/projection_state_version_guard.sh — green
  • bash tools/ci/projection_route_mapping_guard.sh — green
  • bash tools/ci/test_stability_guards.sh — green
  • Per-phase test projects green: Aevatar.GAgents.ChatRouting.Tests (14), Aevatar.ChatRouting.Core.Tests (13), Aevatar.AI.Tests (559, includes 4 new relay regression tests), Aevatar.GAgents.Channel.Protocol.Tests (138), Aevatar.ChatRouting.Voice.Integration.Tests (9)
  • architecture_guards.sh passes everything except the pre-existing playground_asset_drift_guard (unrelated; zero frontend assets touched in this milestone)
  • Reviewer confirms no new guard failures introduced

Related

loning and others added 3 commits May 19, 2026 16:52
…— last round

Applied 4 fixes (FIX_DONE:687:round-3:applied-4:rejected-0:blocked-0):
- (A) architect: add missing Refactor (iter15/cluster-024) anchor on
  AIGAgentBase.cs:202 documenting the deleted protected ChatAsync helper
  surface (subclasses now use ChatStreamAsync + local aggregation only
  when needed)
- (A) architect: remove inaccurate Refactor comment from
  ScriptGenerateGAgent.GenerateWithReasoningAsync — that method was
  already streaming on origin/dev. Only GenerateAsync keeps the
  direct-ChatAsync refactor comment
- (A) architect: same removal on WorkflowGenerateGAgent.GenerateWithReasoningAsync
- (A) tests: widen ChatRuntimeStreamingBufferTests source-regression
  scan from src/Aevatar.AI.Core only to: src/Aevatar.AI.Core +
  src/Aevatar.Studio.Hosting + agents/Aevatar.GAgents.ChatbotClassifier,
  ignoring provider-boundary abstractions and comment-only lines

Build pass; Aevatar.AI.Tests 558 pass; Aevatar.Studio.Tests 521 pass;
test_stability_guards pass; git diff --check pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation for the unified ingress layer (#672 text router + #674 voice
router). This commit only introduces the wire shape and the architecture
decision — no behavior changes yet; nothing else in the repo depends on
the new project.

New artifacts:
- proto/aevatar/chat_routing/v1/chat_route_policy.proto (single proto in
  src/Aevatar.ChatRouting.Abstractions/) with State / Rule / Match /
  Action / Decision / Input / VoiceInput / VoiceTargetRef messages and
  ChatSourceKind / ToolMode / VoiceCodec / VoiceConversationMode /
  VadMode enums.
- ADR-0024: chat-route-policy form (config actor + boundary resolver +
  readmodel), endpoint naming (/ws/voice not /ws/chat), v1 action subset
  (ForwardToGAgent + ForwardToModel), and boundaries with #568 / #596 /
  #608 / #560.

Project layout follows Aevatar.Foundation.Abstractions: proto-only csproj
with Google.Protobuf + Grpc.Tools. ChatRouteCallerScope mirrors the
existing OwnerScope field-for-field to keep this project at the bottom
of the dependency graph; cross-project consolidation is deferred.

Closes part of #691. Tracks toward milestone "Ingress layer v1" (#20).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@eanzhao eanzhao added this to the Ingress layer v1 milestone May 19, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d232cd4a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +190 to +192
// Wipes rules and clears default_target. Caller responsibility to set
// a sane default_target after — resolver will fall back to env default
// until a valid one is upserted.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep reset from clearing required default target

When an existing policy actor receives ResetChatRoutePolicyRequested, this contract says it clears default_target, but the same proto/ADR defines default_target as required whenever the actor exists and the fallback path only applies when the actor/readmodel is unavailable. In the reset scenario the readmodel would still return a policy snapshot, just with no routable default action, so ingress has no valid route until a separate upsert happens; make reset preserve or atomically replace the default target instead of producing an invalid persisted policy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in a297c1d. ResetChatRoutePolicyRequested removed entirely — default_target is REQUIRED whenever the actor exists, and the env fallback (ADR D6) only covers actor-absent / readmodel-unavailable cold start. Callers that want to wipe and replace should use UpsertChatRoutePolicyRequested with the new default_target and an empty rules list — atomic, single committed event, no transient invalid window.

Triple-codex design solver with fixed roles (mirror Phase 8's
architect/tests/quality pattern):
- solver-minimal: smallest viable change; documented exception OK if scope narrow
- solver-structural: CLAUDE-aligned, new abstraction allowed if justified
- solver-delete: question necessity; abstain if feature genuinely needed

4th codex meta-judge arbitrates:
- 3/3 unanimous → auto-dispatch implement (skip maintainer decision)
- Split with named technical divergence → converge (max 2 rounds)
- Architecture philosophy triggers → escalate to human (always)

Auric's policy: 3/3 unanimous required. "早暴露问题比晚暴露问题好" —
2/3 majority would let split designs slip past while leaving harder
problems for later.

Hardcoded escalation triggers (always go to human):
- Top-level CLAUDE.md clause change
- New core abstraction (actor type / envelope kind / pipeline phase)
- docs/canon/* repo vocabulary change
- Rule exception broader than this one transient context
- Cross-cluster coupling
- Performance constraint unverifiable locally
- human_brief.why_needs_design contains philosophy keywords

GitHub traceability per Auric: every dispatch/judgment/round-result
posts bilingual comment to the issue; new labels phase9-solving /
phase9-judging / phase9-converging / phase9-auto-solve.

Default OFF per cluster — operator opts in via auto_solve flag or
phase9-auto-solve label. Maintainer comment mid-Phase-9 halts the
loop and returns to Phase 7 dialog (human input always wins).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Codecov Report

❌ Patch coverage is 86.58399% with 119 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.06%. Comparing base (c0b5602) to head (1aecbde).

Files with missing lines Patch % Lines
...tractions/ToolProviders/AgentToolRequestContext.cs 11.76% 34 Missing and 11 partials ⚠️
src/Aevatar.AI.Core/RoleGAgent.cs 92.93% 6 Missing and 7 partials ⚠️
.../Streaming/EventSinkProjectionLeaseOrchestrator.cs 30.76% 8 Missing and 1 partial ⚠️
...stration/CommittedStateProjectionActivationHook.cs 85.45% 5 Missing and 3 partials ⚠️
...n/ProjectionScopeStatusDocumentMetadataProvider.cs 0.00% 8 Missing ⚠️
src/Aevatar.AI.Core/RoleGAgentFactory.cs 45.45% 6 Missing ⚠️
...ion.Hosting/AevatarAuthenticationHostExtensions.cs 60.00% 5 Missing and 1 partial ⚠️
...ection/ProjectionScopeStatusRuntimeRegistration.cs 89.47% 5 Missing and 1 partial ⚠️
src/Aevatar.AI.Core/Tools/ToolCallLoop.cs 85.00% 2 Missing and 1 partial ⚠️
...re/Orchestration/ProjectionScopeStatusProjector.cs 93.47% 0 Missing and 3 partials ⚠️
... and 8 more
@@            Coverage Diff             @@
##              dev     #699      +/-   ##
==========================================
+ Coverage   82.48%   83.06%   +0.58%     
==========================================
  Files         941      981      +40     
  Lines       60101    61936    +1835     
  Branches     7872     8069     +197     
==========================================
+ Hits        49573    51447    +1874     
+ Misses       7132     7009     -123     
- Partials     3396     3480      +84     
Flag Coverage Δ
ci 83.06% <86.58%> (+0.58%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...vatar.AI.Abstractions/LLMProviders/ILLMProvider.cs 100.00% <ø> (ø)
...Aevatar.AI.Abstractions/LLMProviders/LLMRequest.cs 77.27% <100.00%> (+6.96%) ⬆️
...tractions/LLMProviders/LLMRequestRoutingContext.cs 100.00% <100.00%> (ø)
...bstractions/ToolProviders/AgentToolContextScope.cs 100.00% <100.00%> (ø)
...actions/ToolProviders/AgentToolExecutionContext.cs 100.00% <100.00%> (ø)
...Abstractions/ToolProviders/IToolApprovalHandler.cs 92.85% <ø> (ø)
src/Aevatar.AI.Core/AIGAgentBase.cs 84.35% <ø> (+2.46%) ⬆️
...evatar.AI.Core/Chat/ChatStreamContentAggregator.cs 100.00% <100.00%> (ø)
src/Aevatar.AI.Core/Chat/ContextCompressor.cs 87.50% <100.00%> (+0.15%) ⬆️
...AI.Core/LLMProviders/FailoverLLMProviderFactory.cs 64.65% <ø> (-0.15%) ⬇️
... and 49 more

... and 80 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

cluster-024: ChatStreamAsync as the only AI executor
ForwardToGAgent forward_to_gagent = 2;
ForwardToWorkflow forward_to_workflow = 3;
Reject reject = 4;
Bypass bypass = 5;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[codex] severity=major, category=arch

Bypass is part of ChatRouteAction, and the same type is used by ChatRoutePolicyState.default_target and ChatRouteRule.action, so a persisted policy can encode the dev-only /ws/voice/{actorId} bypass path. That contradicts the ADR text (D5: "the policy-aware endpoint should never produce it") and the field's own comment ("not a rule action"), and it weakens CLAUDE.md's "API 字段单一语义" by making one action type serve both policy decisions and the explicit-actor bypass contract. Consider splitting the types — e.g. keep persisted policy actions to ForwardToModel | ForwardToGAgent | Reject, and give the dev/explicit-entry path a separate action message if Bypass still needs a wire shape.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a297c1d. Bypass removed from ChatRouteAction (deleted message Bypass; tag 5 + name bypass are reserved on ChatRouteAction to prevent silent reuse). The dev /ws/voice/{actorId} endpoint never produces a ChatRouteAction — it reads actorId from the route and short-circuits the resolver — so no replacement wire shape is needed. ADR-0024 §D5 rewritten to spell this out.

string rule_id = 1;
}

message ResetChatRoutePolicyRequested {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[codex] severity=major, category=design

ResetChatRoutePolicyRequested explicitly clears default_target ("Wipes rules and clears default_target"), but ChatRoutePolicyState.default_target is documented as REQUIRED when the actor exists, and ADR D6 says the env-driven fallback is only for the cold-start case where the policy actor itself doesn't exist. So a reset leaves a durable, existing policy state with no default_target that the resolver cannot distinguish from a malformed policy — the message comment's claim that "resolver will fall back to env default until a valid one is upserted" directly contradicts D6. Either make Reset require a replacement default_target, model reset as deleting/tombstoning the policy actor, or widen ADR D6 so the env fallback also covers a reset (target-less) actor — and align all three docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a297c1d — same change as your major/arch comment on Bypass. Chose option (b) from your list: dropped ResetChatRoutePolicyRequested entirely. ADR D6 stays as-is (env fallback only for actor-absent). Reset semantics now expressed as Upsert(default_target, rules=[]) — atomic, never produces an invalid persisted state. §D5 text added explaining the removal.

Comment thread aevatar.slnx Outdated
<Project Path="src\Aevatar.AI.ToolProviders.Workflow\Aevatar.AI.ToolProviders.Workflow.csproj" />
<Project Path="src\Aevatar.Bootstrap.Extensions.AI\Aevatar.Bootstrap.Extensions.AI.csproj" />
<Project Path="src\Aevatar.Bootstrap\Aevatar.Bootstrap.csproj" />
<Project Path="src/Aevatar.ChatRouting.Abstractions/Aevatar.ChatRouting.Abstractions.csproj" />
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Consensus: 4 models] severity=minor, category=style

glm-5.1, mimo-v2.5-pro, kimi, and gemini all independently flagged this: the new <Project Path> entry uses forward slashes (src/Aevatar.ChatRouting.Abstractions/...) while its immediate neighbors — and the large majority of entries in this file — use backslashes (src\...). MSBuild accepts both, so there is no functional impact; the lone forward-slash entry wedged between two backslash entries is purely a consistency / diff-noise nit. Switch to src\Aevatar.ChatRouting.Abstractions\Aevatar.ChatRouting.Abstractions.csproj to match the surrounding block.

Per-model verbatim
  • glm-5.1: Forward-slash path separator (src/Aevatar.ChatRouting.Abstractions/...) is inconsistent with every other <Project Path> entry in this file (all use backslashes). Both work in MSBuild, but the mismatch creates unnecessary diff noise. Change to src\Aevatar.ChatRouting.Abstractions\Aevatar.ChatRouting.Abstractions.csproj for consistency.
  • mimo-v2.5-pro: Path separator inconsistency: new entry uses forward slashes (src/Aevatar.ChatRouting.Abstractions/...) while both immediate neighbors at lines 81 and 82 use backslashes (src\Aevatar.Bootstrap\..., src\Aevatar.Configuration\...). The slnx already has mixed separators elsewhere (lines 5–8 use /), but the inserted position between two \ entries makes it visually jarring. Recommend using src\Aevatar.ChatRouting.Abstractions\Aevatar.ChatRouting.Abstractions.csproj to match the surrounding block.
  • kimi: The new <Project Path="src/Aevatar.ChatRouting.Abstractions/..." /> uses forward slashes while every other entry in this file uses backslashes (src\...). Prefer backslashes for consistency with the existing 80+ entries.
  • gemini: Inconsistent path separator. Neighbors in this section (e.g., lines 79-81) use backslashes (\), but this entry uses forward slashes (/). Suggest using src\Aevatar.ChatRouting.Abstractions\Aevatar.ChatRouting.Abstractions.csproj for consistency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a297c1d. Switched <Project Path> to backslashes (src\Aevatar.ChatRouting.Abstractions\...) to match the surrounding /src/ block.

Per Auric (2026-05-19): "issues 我需要各 solver 的讨论也放回 issues, 好给人类
做判断, 修改一下." Humans need raw reasoning to override the meta-judge.

Change Phase 9 GitHub traceability:
- Each individual solver completion → its own issue comment with FULL
  output (bilingual by construction per solver-*.md prompts)
- Meta-judge completion → its own issue comment with FULL judge output
- 3+ comments per Phase 9 round are intentional audit trail; summary
  tables are forbidden

This also makes manual override easy: the human sees each solver's
file:line evidence + cost numbers + ESCALATE_REASON list and can add
a comment "use minimal framing despite delete escalation" without
needing to ssh to controller machine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
// (CompositeCallerScopeResolver output → ChatRouteCallerScope). Moving
// OwnerScope to Foundation.Abstractions is a deferred refactor — out of
// scope for the ingress-layer milestone.
message ChatRouteCallerScope {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[gemini] severity=nit, category=arch

Mirroring ChatRouteCallerScope field-for-field with Aevatar.GAgents.Scheduled.OwnerScope is a sound temporary measure to avoid a circular dependency. The doc comment and ADR-0024 (§D7 + "Out of scope") already record this as a deferred refactor; consider also adding a tracking issue reference here so the duplication has a concrete consolidation owner rather than only prose. (Low priority — the ADR already documents the intent.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in a297c1d. Filed #700 to track the OwnerScope → Foundation.Abstractions move and updated the ChatRouteCallerScope doc comment to reference it inline, so the deferred consolidation has a concrete owner rather than only the ADR prose.

eanzhao and others added 2 commits May 19, 2026 17:30
5 review comments on chat_route_policy.proto, ADR-0024, and aevatar.slnx:

- (major/arch) Remove `Bypass` from `ChatRouteAction` oneof — it could be
  persisted in `ChatRoutePolicyState.default_target` / `ChatRouteRule.action`,
  letting a stored policy encode the dev-only `/ws/voice/{actorId}` shape.
  Bypass message is deleted; tag 5 + name `bypass` are `reserved` on the
  ChatRouteAction wire to prevent silent reuse. Dev endpoint reads `actorId`
  from the route directly and does not need a ChatRouteAction representation.
- (major/design) Drop `ResetChatRoutePolicyRequested` — `default_target` is
  REQUIRED whenever the actor exists, so a Reset that clears it would leave
  an invalid persisted state (env fallback only covers actor-absent per
  ADR D6). Replaced by `UpsertChatRoutePolicyRequested` with empty rules
  and the desired `default_target` — atomic, single-event, no temporary
  invalid window.
- (minor/style, 4-model consensus) Switch `aevatar.slnx` entry from
  forward to backslash path separator to match the surrounding block.
- (nit/arch) Reference tracking issue #700 in the `ChatRouteCallerScope`
  comment so the OwnerScope-consolidation deferral has a concrete owner.
- (P2) Codex independent flag of the Reset / default_target tension —
  addressed by the same Reset removal above.

ADR-0024 §D5 rewritten to explain the Bypass removal and Reset drop;
§D3 oneof variant list trimmed; intro text drops `Reset*` from the command
catalogue.

Verified `dotnet build` green on the changed project; `tools/docs/lint.sh`
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Auric (2026-05-19): "改skills. 不要漏监控." After he opened #701
mid-session, my previous Monitor armed with hardcoded list [681,682,684]
silently missed all #701 activity (4 substantive comments). Re-armed
manually to include 701, but the root cause is the design pattern itself.

New Mode B Monitor:
- Discovers open issues every 60s tick by querying labels
  (refactor-design-needed OR phase9-auto-solve), never enumerated
- Picks up new issues opened mid-session without re-arm
- Controller-level invariant: every wakeup verifies Monitor is alive AND
  covers every open issue carrying the labels; gap → TaskStop + re-arm
- Hard rule: hardcoding PENDING_ISSUES list is broken

Added "Controller-level gap check (mandatory every wakeup)" subsection
so future controller code reviews catch this regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@eanzhao
Copy link
Copy Markdown
Contributor Author

eanzhao commented May 19, 2026

✅ Fix-check: all 5 review comments resolved

Ran /opencode-pr-fix-check (6-model panel) against a297c1d2e and cross-checked every verdict against the post-fix diff.

Review comment Verdict
chat_route_policy.protoBypass reachable from persisted ChatRouteAction ✅ resolved — message Bypass deleted; reserved 5 / reserved "bypass" added to ChatRouteAction
chat_route_policy.protoResetChatRoutePolicyRequested clears required default_target ✅ resolved — message removed; reset now expressed as Upsert(default_target, rules=[])
chat_route_policy.proto — same Reset contradiction (codex bot) ✅ resolved — same removal
aevatar.slnx — forward-slash path separator ✅ resolved — switched to backslashes
chat_route_policy.protoOwnerScope mirror lacks a tracking reference ✅ resolved — doc comment now cites (tracked as #700); #700 filed

5 resolved · 0 partial · 0 unresolved.

Panel: deepseek-v4-pro · glm-5.1 · mimo-v2.5-pro · kimi · gemini · codex. ADR-0024 §D5 was also updated to document the Bypass / Reset removals with rationale — good.

🤖 automated fix-check via /opencode-pr-fix-check

loning and others added 2 commits May 19, 2026 18:00
Implements issue #692 (Ingress v1 Phase 1) on top of the #672 / ADR-0024
config-actor + boundary-resolver design. Adds the Aevatar.GAgents.ChatRouting
agent package:

- ChatRoutePolicyGAgent — per-scope, long-lived config aggregate over
  ChatRoutePolicyState. Handles UpsertChatRoutePolicyRequested and
  RemoveChatRouteRuleRequested, emitting committed ChatRoutePolicyUpdated
  events. Upsert requires a non-empty default_target (actionable error
  otherwise) and persists rules pre-ordered by priority, ties broken by
  rule_id. Config-only — no query handler, no turn dispatch (IProjectedActor).
- ChatRoutePolicyCurrentStateDocument — query-side readmodel that mirrors
  ChatRoutePolicyState 1:1 with typed sub-messages (no Any state_root bag).
  chat_route_policy_readmodel.proto imports chat_route_policy.proto via
  Grpc.Tools AdditionalImportDirs, so the wire types stay single-sourced in
  Aevatar.ChatRouting.Abstractions.
- ChatRoutePolicyCurrentStateProjector — overwrite-materializes committed
  ChatRoutePolicyState into the readmodel; state_version / last_event_id
  come from the committed StateEvent (no locally-invented version).
- AddChatRoutingAgents registers the InMemory document projection store and
  is wired into the Mainnet host bootstrap.

Reset command: issue #692 listed ResetChatRoutePolicyRequested, but PR #699's
review removed it from chat_route_policy.proto — a reset that clears the
required default_target would persist invalid state. This PR follows the
current proto: two commands, no Reset.

Tests (test/Aevatar.GAgents.ChatRouting.Tests, 12 green):
- GAgent: upsert + remove-rule happy paths, default_target-missing rejection,
  rules priority ordering, unknown-rule rejection, and a guard that the actor
  only handles config commands.
- Projector: committed-state overwrite, same-version idempotency,
  non-committed no-op, null-argument / null-dependency guards.
- DI: AddChatRoutingAgents registers the document store reader + writer.

Verification:
- dotnet build aevatar.slnx — 0 errors
- dotnet test test/Aevatar.GAgents.ChatRouting.Tests — 12/12 pass
- tools/ci/projection_state_version_guard.sh — pass
- tools/ci/projection_route_mapping_guard.sh — pass
- tools/ci/architecture_guards.sh — pass (proto buf lint, projection, and
  studio_projection_readmodel_registration guards all green)

Tracks milestone "Ingress layer v1"; part of #672.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
eanzhao and others added 8 commits May 19, 2026 18:10
Sequential sibling to codex-refactor-loop: walks every issue in a GitHub
milestone in order, with one codex per issue (implement) → stacked PR
(issue N's base = issue N-1's head; issue 1 → dev) → Claude subagent
reviewer → fix codex if rework → re-review until pass or hit
max_review_rounds cap → advance to next issue. PRs stay open for human
merge; any failure halts the train.

Files:
- SKILL.md (controller phase machine + dispatch rules)
- REFERENCE.md (state.json schema + recovery playbook + vs refactor-loop)
- prompts/implement.md (codex: implement one issue, IMPLEMENT_DONE marker)
- prompts/review.md (subagent: 7-dimension PR review, REVIEW_VERDICT)
- prompts/fix.md (codex: address findings, A/B/C/D/E classification)
- scripts/spawn-codex.sh (copy of refactor-loop wrapper; ≥3600s floor)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…at/2026-05-19_ingress-phase1-chat-route-policy
Per Auric (2026-05-19): "凡是新回复都要完整重新让多个solver分析, 必须达成
共识才可以."

Old: MAX_CONVERGENCE_ROUNDS=2 → if not consensus, escalate.
New: no hard round cap. Loop iterates until:
  - 3/3 unanimous + meta-judge consensus → auto-implement, OR
  - hardcoded architecture-philosophy trigger fires → escalate, OR
  - maintainer adds auto-loop-resume label with explicit framing.

Maintainer-reply-resets-the-round (mandatory):
  - When auto-discover Monitor fires on a verified team-member comment
    that is substantive, controller TaskStops in-flight Phase 9 codex,
    treats new comment as fresh constraint, dispatches a NEW round
    (not "continue convergence"; truly fresh with prior rounds as context).
  - Round counter increments but does NOT trip any escalation cap.
  - Previous escalation state (auto-loop-stuck label) auto-clears on reset.

Anti-spiral replacements for the dropped cap:
  - Stall detection: 3 consecutive rounds with NO maintainer input AND
    no solver verdict-text change → escalate as stalled:no-progress-no-input.
  - Maintainer reply RESETS stall counter to 0.
  - 12h cumulative solver runtime per issue (raised from 6h).
  - Hardcoded architecture-philosophy triggers escalate immediately
    regardless of round (no change here).

Triggered by today's #701 experience: round 2 escalated, Auric posted
substantive architecture vision in c14/c15, controller should have
auto-restarted with his comment as constraint instead of waiting for
him to pick I/II/III.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…PR revert)

cluster-024 fix codex round 3 reverted this file out of PR #687 scope
per quality reviewer's "unrelated drive-by" feedback. That left dev /
auto-refact-dev WITHOUT the fix, and Phase 6 sync pulled the unfixed
version back into auto-refact-dev.

Result: when no --add-dir args passed, the `ADD_DIRS[@]` expansion under
`set -u` throws "unbound variable" → spawn-codex.sh exits 1 → 3 Phase 9
solvers for #701 round 3 all failed immediately.

This is a SKILL-level fix (not cluster work), so it goes on
auto-refact-dev directly (not a cluster PR) and lives there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…env fallback

Adds the stateless Aevatar.ChatRouting.Core library: ChatRouteResolver
(pure function), projection-backed IChatRoutePolicyQueryPort, env/options
fallback provider, and AddChatRoutingCore() DI wiring in the Mainnet host.

Implemented per .implement-loop/runs/implement-issue-693.md.

Closes #693

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ntries

Wires ChatRouteResolver into NyxIdChat create, Responses, Messages, and the
relay ConversationGAgent — each resolves before its existing dispatch, with
no new actor hop. Adds typed NeedsLlmReplyEvent.target_ref and the relay
caller-scope field; registers src/Aevatar.ChatRouting.Abstractions as a buf
module so the cross-module proto import resolves under buf lint.

Implemented per .implement-loop/runs/implement-issue-694.md.

Closes #694

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Auric (2026-05-19): "codex cli, 提示词直接写到一个临时文件就可以,
输出也输出到一个临时文件, 方便debug. 改skills."

Wrapper changes:
- Add --prompt-text "..." option (wrapper auto-mktemps /tmp/codex-prompt.XXXXXXXX);
  caller no longer needs to write prompt file manually
- Print "SPAWN: prompt=<path> log=<path> cd=<dir> timeout=<s>" to stderr at start
- Print "DONE: log=<path> exit=<N> prompt=<path>" to stderr at end
- Header docstring documents the file-based contract explicitly
- macOS mktemp note: trailing X pattern (BSD), no extension (codex doesn't care)

CLAUDE.md "Codex CLI 调用规范" updates:
- New "Prompt + 输出必须双 file (强制,debug 友好)" subsection
- Add inline-string and missing --log to anti-pattern list
- Auto-banner described in 标准包装 section

Debug recipe: `cat <prompt-path>` to see what codex saw;
              `cat <log-path>` to see what codex did.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e audio disabled

Per Phase 9 #701 consensus (Auric architecture + Option A, 3/3 unanimous round 5):

- Collapse RemoteActorVoicePresenceSessionResolver from locked host-owned
  relay to setup/control-only bridge. Remove _gate, _state, AttachmentState,
  subscription handling, relay task, PCM-as-VoiceRemoteAudioInputReceived
  dispatch, and VoiceRemoteTransportOutput.AudioOutput forwarding.
- Remote media attach now dispatches Open + immediate Close with reason
  "remote_audio_transport_unavailable", disposes transport, throws
  NotSupportedException so callers fail explicitly.
- Keep remote setup/close/control envelope plumbing live through existing
  EventEnvelope + IActorDispatchPort (Auric pipeline: function-call boundary
  uses envelope, chunks never via envelope).
- VoicePresenceSessionDispatch: direct host envelopes no longer accept
  VoiceRemoteAudioInputReceived.
- VoicePresenceModule: actor still owns remote session open/close/control
  state; remote audio input inert; provider audio not republished as
  remote transport audio output.
- WebSocket/WHIP endpoints surface remote_audio_transport_unavailable.
- Behavior tests rewritten to assert setup/close/control remains and audio
  chunks never relayed through envelopes.

Diff: 7 files, +172 / -440 (net -268).
Build pass; VoicePresence.Tests 118/118; arch+stability guards pass.

No new actor type, envelope kind, pipeline phase, or core abstraction added.
Proto compatibility surface preserved (remote audio arms unused but reserved
for future raw-transport restoration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
loning and others added 28 commits May 22, 2026 02:55
…ed attach (#795)

* iter25 cluster-002: CQRS Core ObservationLifecycle port + post-accepted attach (per Auric directive 升级架构) ⟦AI:AUTO-LOOP⟧

* fix r1 PR #795: architect+tests reject ⟦AI:AUTO-LOOP⟧

* fix r2 PR #795: architect+quality reject ⟦AI:AUTO-LOOP⟧
…ervationLifecycle API

#788(iter25 cluster-026 ScriptServiceRunCommand)和 #795(iter25 cluster-002
observation-lifecycle)单独 merge OK,但顺序 merge 后 auto-refact-dev trunk
编译挂:#788 引入的 ScriptServiceRunCommandTargetBinder 用旧 ICommandTargetBinder<,,>
+ CommandTargetBindingResult<>,#795 改名为 ICommandObservationLifecycle<,,,,>
+ CommandObservationBindingResult<>。

修复:
- ScriptServiceRunInteraction.cs: ICommandTargetBinder → ICommandObservationLifecycle,
  BindAsync(cmd, target, ctx, ct) → BindAsync(cmd, execution, ct),
  CommandTargetBindingResult → CommandObservationBindingResult
- ServiceCollectionExtensions.cs:DI 注册类型签名同步
- ScriptServiceRunInteractionTests.cs:测试 mock 同步

Local build:0 error。

⟦AI:AUTO-LOOP⟧
刚 land 的 f0c8e1e 把 ICommandTargetBinder → ICommandObservationLifecycle 迁移,
但 BindAsync 内 target.BindRunInput 改 target.RunId 后,Execute 在 receipt 构建
时引用的 target 不是 same instance 或顺序错位 → Receipt.RunId = "" 而非
command.RunId。

修:确保 target binding 完整后再构建 receipt(或调整 BindAsync 内顺序),
ScriptServiceRunInteractionTests.Interaction_ShouldAttachProjectionDispatchRuntimeAndCleanup 通过。

⟦AI:AUTO-LOOP⟧
…tream architecture unified) (#792)

* iter25 cluster-027: TelegramWaitReplyGAgent task-scoped actor (lark stream actor 统一) ⟦AI:AUTO-LOOP⟧

* fix r5 PR #792: arch guard direct State mutation → event applier + rebased over auto-refact-dev

Squash of fix r1-r5 + post-#795 rebase. Direct State.Generation mutation moved to
event emission + applier per actor execution model.

⟦AI:AUTO-LOOP⟧
Per-target HealthProbeTargetGAgent (configure command + self-rescheduling
tick) projects committed state events into a current-state readmodel via
the standard Projection Pipeline. Two default executors ship:
http_status (generic HTTP check with ${configuration:Key} placeholders)
and readmodel_freshness (pluggable IReadmodelFreshnessSource).
Adds /status (HTML, sub2api-style) and /api/status (JSON aggregate) on
the mainnet host, both AllowAnonymous. Wires the status-dashboard agent
module via AddStatusDashboard and registers a channel-bot registration
freshness source. Manifest in appsettings ships six probes (self
liveness/readiness, NyxID catalog, Elasticsearch, channel-bot freshness,
LLM dry-run disabled until ops sets the probe token).
…uter

# Conflicts:
#	.gitignore
#	agents/Aevatar.GAgents.Channel.Runtime/Aevatar.GAgents.Channel.Runtime.csproj
#	agents/Aevatar.GAgents.Channel.Runtime/protos/conversation_events.proto
#	agents/Aevatar.GAgents.NyxidChat/Aevatar.GAgents.NyxidChat.csproj
#	src/Aevatar.CQRS.Core/Interactions/DefaultCommandInteractionService.cs
#	src/Aevatar.Mainnet.Host.Api/Messages/MessagesEndpoints.cs
#	src/platform/Aevatar.GAgentService.Hosting/Endpoints/ScopeServiceEndpoints.cs
@eanzhao eanzhao merged commit fccb80d into dev May 22, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unify direct chat, NyxID relay, and NyxID Responses through an agent chat router

5 participants