feat(resource): per-agent max_text_chars config + kb_diag_agent YAML align - #400
Conversation
…ections (#391) * fix(opencode): eliminate EventBus loopback causing duplicate SSE projections The OpenCodeEventBridge republished protocol projections (MessageUpdatedEvent, PartUpdatedEvent, etc.) back into the same EventBus that carries native agent events. This created a feedback loop: native events → event bridge → broadcast → EventBus republish → SSE delivery, causing duplicate renders in attached OpenCode TUI clients (issue #380). Architecture change — direct-wire SSE: - state.broadcast_event() now fans projections directly to per-connection SSE subscriber queues instead of republishing to EventBus - global_routes._event_generator reads from state.event_subscribers queues (no EventBus subscription, no CustomEvent unwrapping) - Reconnect replay via state.replay_projections() using Last-Event-ID - Deleted event_bridge.py (the loopback republisher) EventBus source isolation (defense-in-depth): - EventEnvelope gains source_hint field; publish() accepts source_hint - subscribe() accepts exclude_source param (filters both live fanout and replay) - ProtocolEventConsumerMixin hooks: _get_subscription_replay() and _get_subscription_exclude_source() (defaults: replay=True, exclude=None) - OpenCode overrides: replay=False, exclude_source={"opencode_event_bridge"} Session consumer replay alignment: - OpenCode session-level consumers now use replay=False (matching the global SSE endpoint's first-connect policy), preventing stale events from being redelivered on consumer startup Testing: - 7082 tests passed (full suite), ruff/mypy clean - New e2e test: test_attach_existing_session_first_prompt_renders_once - New unit tests: EventBus source_hint/exclude_source (4 tests) - Rewritten integration tests for direct-wire SSE model Note: A residual TUI-side duplication may still be visible in opencode attach mode due to the TUI's local echo (createUserMessage) not matching the server-generated message ID in the SSE event. This is tracked as an opencode TUI bug (anomalyco/opencode#14372, #24773, #29478) with upstream fix PR #31945 still unmerged. The server-side fix in this commit eliminates the EventBus loopback path; the remaining duplication is purely client-side. * fix(opencode): address review — QueueFull policy, typed session-id extraction, mock alignment Review-driven fixes on PR #391 (direct-wire SSE loopback elimination): BLOCKER: broadcast_event now handles asyncio.QueueFull per-subscriber with the same drop-oldest policy as EventBus._enqueue, so a stalled SSE client can no longer abort fanout to every other subscriber. Adds structured warning logging on overflow plus a debug fanout log (telemetry on the delivery critical path). New regression test: test_broadcast_event_drop_oldest_on_queue_full. MAJOR: ServerState.extract_session_id now delegates to the typed global_routes._extract_session_id (match-based, no getattr) instead of the getattr probe that read info.id for MessageUpdatedEvent — buffering message.updated under per-message keys and leaking memory. Typed variant reads props.info.session_id as documented. Test mocks in test_global_event.py / test_sse_compliance.py aligned with production: deque(maxlen=100) buffers, drop-oldest overflow, typed extractor — the queue-full behavior is no longer suppressed out of the green suite. MINOR: replay_projections replays merged buffers in global event_id order (monotonic SSE ids for reconnecting clients) and stops with a warning on QueueFull instead of silently dropping a suffix. MINOR: _get_subscription_exclude_source documented as currently inert (its only producer was deleted with the loopback bridge; kept as defense-in-depth, exercised by unit tests). Nits: stale event_bridge docstrings/comment/names updated across test_event_pipeline_e2e.py and conftest.py; corrected the pre-existing dedup-set claim in src/wolfharness/AGENTS.md (the set is a private ACPEventConverter field, not on SessionController); changelog trailing newline; ADR eventbus-replay.md annotated as superseded by the direct-wire design (PR #391). Verified: ruff clean, mypy strict clean (686 files), 326 affected tests pass.
…align (#394) * feat(resource): per-agent max_text_chars config + kb_diag_agent YAML align ResourceConfig now accepts max_text_chars (default 10000, min 100) in agent YAML config. NativeAgent creates a per-agent ResourceCapability with the agent's max_text_chars instead of sharing the pool-level instance. ResourceCapability.__init__ gains max_text_chars parameter (backward compatible). Truncation suffix improved with guidance directing the model to use narrower URIs or paginated read tools. kb_diag_agent.yaml aligned with live knowledge_diag server v3.4.4: - Enabled search_kb (removed from disabled_tools) - Added get_doc_toc and read_chapter_page tool-schema-overlap rewrites - Added search_kb rewrite with methods (FULL/FAST/WIKI) param docs - Updated existing tool descriptions to reference page-based workflow Supersedes PR #393. * feat(mcp): resource subscribe-on-read wiring in McpServerCap Best-effort subscribe to resource URIs after successful read_resource() calls, enabling notifications/resources/updated for resources the agent has read. Tracked subscriptions are re-established on reconnect and cleaned up on disconnect. No-op for servers with subscribe:false (like knowledge_diag v3.4.4) — subscribe fails silently, read proceeds normally. Activates automatically when server enables subscription support. * fix(mcp): address PR #394 review — wire max_text_chars, fix broken test, consolidate truncation - Wire self._max_text_chars into read_mcp_resource (was hardcoded _DEFAULT_READ_TEXT_LIMIT) and use _truncate_text helper with guidance suffix - Remove dead _truncate_text static method from ResourceCapability (zero callers, old suffix format) - Add constructor validation: max_text_chars < 100 raises ValueError - Consolidate default constant: _DEFAULT_MAX_TEXT_CHARS in resource_resolver.py, aliased in resource_capability.py - Fix broken test assertion in test_resource_resolution.py to match new guidance suffix format - Add tests: max_text_chars validation, read_mcp_resource truncation with per-agent limit, suffix guidance text - Correct changelog: limit was previously hardcoded, not a pre-existing constructor param - Update capabilities/AGENTS.md: ResourceCapability is per-agent constructed, not registered at SESSION scope
Review — PR #400 (per-agent
|
|
Review posted: #400 (comment) Verdict: request changes. Reviewed the #394 delta (actual new work) plus a docs sweep across the full diff. Blockers
Major — no tests for the subscription lifecycle ( Minor — dead always-True guard in Docs — subsystem AGENTS.md/changelog/ADR/RFC updates verified accurate; suggested adding Delegate note: both specialist subagents cross-verified findings against the working tree; I independently confirmed the blocker wiring, the malformed JSON, and the tracked sensitive files before posting. |
The except Exception block logged but swallowed startup errors (e.g. OSError: Address already in use), causing silent exit 0. Add raise after log.exception to propagate to the CLI. Fixes CI failure in test_start_async_propagates_serve_oserror.

Supersedes #394 (reverted during main rollback in PR #397 review).
Original PR #394 was merged but got caught in the force-push rollback. This PR re-submits the framework changes.
Changes
max_text_charsconfig for resource truncationDependencies
Built on top of cleaned #382 + #391. Review/merge #399 first.