For the complete documentation index, see llms.txt. This page is also available as Markdown.

May 2026

Archived entries for May 2026, newest first. For recent work see the live changelog.


🛠️ PR Feedback Remediation — Production Hardening (2026-05-17)

Repo: EDDI (feature/feature-gap-remediation) What changed: Addressed ~25 findings from CodeQL, code quality bot, Copilot, and CodeRabbit reviews. All actionable items resolved.

Security Fixes

  • NonceCacheService TOCTOU: Replaced non-atomic get()+put() with putIfAbsent() for replay detection. The get-then-put pattern allowed two concurrent requests with the same nonce to both pass the replay check.

  • NonceCacheService null guard: Added null/blank nonce early rejection.

  • Log injection (centralized): Replaced per-file sanitizeForLog() methods in GroupConversationService, MongoTenantQuotaStore, PostgresTenantQuotaStore with centralized LogSanitizer.sanitize(). Added Unicode line separator (U+2028/U+2029) handling per CodeQL feedback. Also wrapped e.getMessage() in log calls.

  • Fail-closed cost accounting: PostgresTenantQuotaStore.tryAddCost() now returns DENIED on SQL failure instead of OK — prevents budget bypass when database is unreachable.

  • Key version validation: AgentSigningService.generateKeyPairVersioned() and rotateKey() now reject version <= 0.

  • JacksonCanonicalizer strict duplicate detection: Enabled StreamReadFeature.STRICT_DUPLICATE_DETECTION to prevent collision attacks where different JSON payloads produce identical canonical output. Removed inaccurate RFC 8785 claim from javadoc.

  • AgentSigningService versioned key cleanup: deleteKeyPair() now deletes both legacy unversioned and all versioned vault secrets. generateKeyPairVersioned() now evicts version-specific cache entries.

Performance Fixes

  • Incremental peer verification: verifyPriorEntriesIfRequired() now tracks last-verified transcript index per conversation (O(N) amortized instead of O(N²) per-turn re-verification). Public keys cached per speaker to avoid redundant agentStore lookups.

  • signEnvelope private key caching: Now uses privateKeyCache.computeIfAbsent() with versioned cache key, avoiding vault round-trips on every call.

Architecture Fixes

  • DiscoverToolsTool CDI exclusion: Added @Vetoed to prevent Quarkus CDI from auto-discovering the class as a bean (it is manually constructed by AgentOrchestrator).

  • LAZY tool activation: Fixed gap where discovered tools couldn't actually be called. collectEnabledTools() now returns ALL tools (registering executors), while executeWithTools() initially presents only discover_tools spec. After the LLM calls discover_tools, matching built-in specs are activated via activateDiscoveredTools().

  • PostgresTenantQuotaStore transactional delete: deleteQuota() now wraps both tenant_quotas and tenant_usage deletes in a single transaction with rollback on failure.

  • PostgresTenantQuotaStore schema auto-creation: Added CREATE TABLE IF NOT EXISTS with ensureSchema() pattern (matching PostgresGlobalVariableStore, PostgresSecretPersistence, etc.).

  • MongoTenantQuotaStore unique index: Added unique ascending index on tenantId for both tenant_quotas and tenant_usage collections to prevent duplicate rows from upsert races.

  • DiscoverToolsTool JSON serialization: Replaced manual StringBuilder JSON assembly with Jackson ObjectMapper for proper escaping of special characters in tool descriptions.

  • JacksonCanonicalizer overload rename: canonicalize(Object)canonicalizeObject(Object) to eliminate static dispatch ambiguity.

  • GroupConversationService FQN cleanup: Replaced 5 fully-qualified class references (ai.labs.eddi.configs.agents.crypto.*) with proper imports.

  • AgentOrchestrator log fix: Compute external tool count explicitly instead of activeSpecs.size() - 1 to avoid misleading -1 in logs.

Changelog accuracy

  • Fixed Item 1 and Item 2 descriptions below (see corrections inline).

Files: NonceCacheService.java, GroupConversationService.java, MongoTenantQuotaStore.java, PostgresTenantQuotaStore.java, AgentSigningService.java, AgentOrchestrator.java, DiscoverToolsTool.java, JacksonCanonicalizer.java, SignedEnvelope.java, LogSanitizer.java, changelog.md


🛡️ Crypto Security Review — Fail-Safe Remediations (2026-05-15)

Repo: EDDI (feature/feature-gap-remediation) What changed: Security-focused code review identified 7 findings (2 high, 3 medium, 2 low). All remediated. Key principle: signing failures are fail-safe — discard the broken signature and fall back to unsigned, rather than storing broken data.

S1+S2 (HIGH): Signing failures now fail-safe to unsigned

  • Self-verify failure (verifyEnvelope returns false) → discard signature, fall back to unsigned entry

  • Nonce validation failure → discard signature, fall back to unsigned entry

  • Previously: logged warning/error but continued with broken signature stored permanently

S3+S4 (MEDIUM): Null guards for crypto infrastructure

  • Signing block: agentStore, agentSigningService, nonceCacheService all guarded for null

  • agentConfig.getIdentity() guarded before getKeyValidAt() call

S7 (LOW): NonceCacheService unused ttlMs variable

  • Removed computed ttlMs that was never passed to cache factory

  • Added documentation comment explaining the cache TTL configuration requirement

Tests: 15 new tests (84 total affected)

  • TranscriptEntry: full 13-param constructor, hasEnvelopeData() (4 edge cases), signature-only constructor

Docs updated

  • docs/architecture.md: added Cryptographic Agent Identity section

  • planning/manager-ui-handoff.md: removed signMcpInvocations, forkingEnabled, maxForksPerConversation, updated Security section to show active signing flags


🔐 Cryptographic Agent Identity — End-to-End Hardening (2026-05-15)

Repo: EDDI (feature/feature-gap-remediation) What changed: Evolved the partial SignedEnvelope infrastructure into a fully-wired, production-standard cryptographic identity system. Removed dead config fields, added peer verification, and made all security features functional.

Config Cleanup — Remove Dead Fields

  • Removed: signMcpInvocations from SecurityConfig (no MCP signing implementation exists)

  • Removed: forkingEnabled + maxForksPerConversation from SessionManagement (no forking service exists)

  • Rationale: "Configs without functionality" creates false confidence. Features are added alongside their implementation, not before.

  • Files: AgentConfiguration.java, RestAgentStore.java (removed validateSessionFlags()), tests updated

TranscriptEntry — Full Envelope Storage

  • Added: signatureNonce, signatureTimestampMs, signatureKeyVersion fields to TranscriptEntry record

  • Added: hasEnvelopeData() convenience method for verification checks

  • Backward-compatible: Two compact constructors for unsigned and signature-only entries

  • Files: GroupConversation.java

GroupConversationService — End-to-End Crypto Wiring

  • Injected: NonceCacheService for replay protection

  • Signing block: Now creates full SignedEnvelope with nonce, immediately self-verifies, registers nonce, and stores all envelope fields in TranscriptEntry

  • Added: verifyPriorEntriesIfRequired() — when receiving agent has requirePeerVerification=true, reconstructs envelopes from stored fields and verifies each speaker's signature against their public key

  • Defense-in-depth: Signing self-verifies at creation time; peer verification at consumption time catches key rotation issues or data corruption

  • Files: GroupConversationService.java

LlmConfiguration — Configurable maxToolsInContext

  • Added: maxToolsInContext field (default: 20) to LlmConfiguration.Task for LAZY tool loading

  • Previously: Hardcoded int maxToolsInContext = 20 in AgentOrchestrator

  • Files: LlmConfiguration.java, AgentOrchestrator.java

MongoTenantQuotaStore — TOCTOU Documentation

  • Added: Comment documenting the minor TOCTOU race at window boundaries in multi-instance deployments

  • Files: MongoTenantQuotaStore.java

Test Fixes

  • Updated SessionManagementTest, AgentConfigurationTest, RestAgentStoreTest — removed references to deleted fields

  • Updated GroupConversationServiceTest — added NonceCacheService constructor parameter

  • All 69 affected tests pass (0 failures, 0 errors)


🔧 Feature Gap Remediation — 6 Items Resolved (2026-05-15)

Repo: EDDI (feature/feature-gap-remediation) What changed: Systematic audit found 8 gaps between documented features and actual implementation. Fixed 6 items (2 required no changes).

Item 1: Session Forking — Config Removed

  • Problem: forkingEnabled=true accepted silently but no ConversationForkService exists

  • Original fix: Added validateSessionFlags() in RestAgentStore to reject the flag with a clear error

  • Final state: Both forkingEnabled and maxForksPerConversation config fields were fully removed (config-without-functionality anti-pattern). validateSessionFlags() was also removed since there are no session flags left to validate.

  • Files: AgentConfiguration.java, RestAgentStore.java

Item 2: Signing Flags — Config Removed

  • Problem: signMcpInvocations flag accepted silently but no MCP signing implementation exists

  • Original fix: Split validateSecurityFlags() to reject signMcpInvocations while allowing signInterAgentMessages and requirePeerVerification

  • Final state: signMcpInvocations field was fully removed from SecurityConfig. The validation method was also removed since both remaining flags (signInterAgentMessages, requirePeerVerification) now have runtime implementations.

  • Files: AgentConfiguration.java, RestAgentStore.java

Item 3: DiscoverToolsTool — Recovered + Wired

  • Problem: Token-saving lazy tool loading deleted as dead code (commit 05edf602)

  • Fix: Recovered DiscoverToolsTool.java + test, added ToolLoadingStrategy enum (EAGER/LAZY) to LlmConfiguration.Task, wired LAZY branch into AgentOrchestrator.collectEnabledTools() — when LAZY, only discover_tools meta-tool is sent initially, LLM discovers available tools, specs injected mid-loop

  • Files: DiscoverToolsTool.java (recovered), LlmConfiguration.java, AgentOrchestrator.java

Item 4: Cryptographic Infrastructure — Recovered + Wired

  • Problem: SignedEnvelope, JacksonCanonicalizer, NonceCacheService deleted as dead code (commit 4a717fa5)

  • Fix: Recovered all 3 files + tests, re-added signEnvelope()/verifyEnvelope()/rotateKey()/generateKeyPairVersioned() to AgentSigningService, upgraded GroupConversationService signing from simple string signing to full SignedEnvelope with nonce-based replay protection

  • Files: SignedEnvelope.java, JacksonCanonicalizer.java, NonceCacheService.java (all recovered), AgentSigningService.java, GroupConversationService.java

Item 5: Tenant Quota DB Persistence — Dual-Backend Stores

  • Problem: ITenantQuotaStore only had InMemoryTenantQuotaStore — restarts reset all quota counters, no cross-instance synchronization

  • Fix: Created MongoTenantQuotaStore (uses findAndModify for atomicity) and PostgresTenantQuotaStore (uses UPDATE...WHERE...RETURNING), wired into DataStoreProducers following existing dual-backend pattern

  • Files: MongoTenantQuotaStore.java (new), PostgresTenantQuotaStore.java (new), DataStoreProducers.java

Item 6: NATS Documentation

  • NATS code works correctly for what it does (durable ordered processing with retry/dead-letter)

  • No code changes needed — documentation accuracy to be addressed separately

Items 7-8: No Changes Needed

  • HIPAA docs accurately describe documentation, not code enforcement

  • OpenTelemetry opt-in is standard industry practice

Slack Integration Hardening — IM Fix, Test Repairs, Docs Overhaul (2026-05-17)

Repo: EDDI (feature/channel-integrations)

What changed: Fixed silent DM message dropping, repaired 8 broken tests, added 24 new tests for coverage, and overhauled both Slack and group-conversation documentation.

Bug Fix: DMs Silently Dropped

  • Root cause: SlackEventHandler.handleEvent() filtered all top-level message events, assuming app_mention handles them. But Slack never fires app_mention in DMs — only message events with channel_type: "im". DMs were silently dropped.

  • Two-part fix:

    1. SlackEventHandler now detects channel_type: "im" and lets DM messages through the filter

    2. ChannelTargetRouter.resolveDefaultForDm() added — DM channels use dynamic D-prefixed IDs that are never pre-configured, so DMs fall back to the first available Slack integration's default target

Files: SlackEventHandler.java, ChannelTargetRouter.java

Test Repairs (8 failures → 0)

All 8 failures caused by UX mode changes from the previous session:

  • All styles now use expanded mode (EXPANDED_STYLES includes all 5 styles)

  • Start message format changed to lowercase

  • Synthesis uses header+thread pattern (2 postMessage calls)

Rewrote SlackGroupDiscussionListenerTest to match current behavior.

New Test Coverage (24 new tests)

  • SlackWebApiClientTest — 19 new tests for convertMarkdownToSlackMrkdwn

  • SlackGroupDiscussionListenerTest — 5 new tests: all styles, header+thread synthesis, start message format

Documentation Overhaul

  • slack-integration.md — Major rewrite: ChannelIntegrationConfiguration as primary config model, DM support section, unified header+thread UX, trigger keywords, Markdown→mrkdwn conversion, fixed component names, DM troubleshooting

  • group-conversations.md — Added Slack Integration section: header+thread UX, all 5 styles' phase flow in Slack, trigger keywords, follow-up conversations

Verification

  • All Slack tests pass: 104 tests, 0 failures

  • Clean compile: BUILD SUCCESS


Channel Integration — Second-Pass Review Fixes (2026-05-14)

Repo: EDDI (feature/channel-integrations)

What changed: Second critical review pass, 6 additional findings fixed.

  • M5: Fixed stale ${eddivault:...}${vault:...} in ChannelTargetRouter.deepCopyConfig() Javadoc

  • M6: Added SPDX headers to IRestChannelIntegrationStore, RestChannelIntegrationStore (missed in first pass)

  • L3: Applied LogSanitizer.sanitize() to all Slack-sourced log parameters in SlackEventHandler (CodeQL compliance)

  • L4: ChannelTarget.getTriggers() now returns a defensive copy (consistent with getTargets()/getPlatformConfig())

  • L5: Added null guard to postMessageChunked() to prevent NPE on null text

  • L6: Added ObserveConfig bounds validation (cooldownSeconds, maxDailyResponses, maxCostPerDay ≥ 0)

Files: ChannelTargetRouter.java, IRestChannelIntegrationStore.java, RestChannelIntegrationStore.java, ChannelTarget.java, SlackEventHandler.java


Channel Integration — Pre-Merge Review Fixes (2026-05-14)

Repo: EDDI (feature/channel-integrations)

What changed: Addressed findings from thorough code review before merge.

Critical fixes

  • C1 — Removed ThreadLocal<ResolvedTarget>: Virtual threads and ThreadLocal are a known Loom footgun — carrier thread reuse can leak stale values. Replaced with explicit botToken parameter passing through postMessage(), postMessageChunked(), and postHelp(). All callers now pass botToken (or null for router fallback) directly.

  • C2 — Intent key format change documented: The conversation mapping intent key changed from slack:<channelId>:<threadKey> to channel:slack:<channelId>:<agentId>:<threadKey>. This is intentional (adds agent specificity for multi-target channels) but means existing Slack conversation mappings from pre-6.1 will be orphaned — new conversations will be created. This is acceptable for a pre-GA feature with very few users.

Medium fixes

  • M1 — eddivaultvault Javadoc: Updated stale ${eddivault:key-name} reference in ChannelIntegrationConfiguration to ${vault:key-name} (prefix was renamed on main in 1b884109).

  • M4 — Trigger backtick formatting: Fixed postHelp() to render triggers as `architect`: instead of `architect:` — the colon is part of the user syntax, not the keyword.

Low fixes

  • L2 — SPDX headers: Added Copyright EDDI contributors / Apache-2.0 headers to all 12 new files.

Merge conflicts resolved

  • docs/changelog.md — both branches added entries; kept both sets.

  • SlackChannelRouter.java / SlackChannelRouterTest.java — deleted on this branch, modified on main (CodeQL fixes). Resolved by keeping deletion (replaced by ChannelTargetRouter).

Files: SlackEventHandler.java, ChannelIntegrationConfiguration.java, docs/changelog.md, 12 new files (SPDX headers)


🔍 DreamService PR Review Remediation — Pass 2 (2026-05-16)

Repo: EDDI (feature/dream-summarization) What changed: 9 findings from Copilot (8) + CodeRabbit (1) review, all resolved.

High Severity (3 — data loss / data unreachability)

  • Multi-agent self visibility upgrade — When consolidating entries from multiple agents (preserveAgentProvenance=false), self-scoped visibility is upgraded to global so no agent loses its memories

  • GroupIds preserved — Consolidated entries now inherit the union of all groupIds from originals, fixing group-scoped entries becoming unreachable after consolidation

  • summarizeTargetEntries validation — Setter now rejects <1 (was silently accepting 0, which would cap to empty list, insert nothing, then delete all originals)

Medium Severity (5 — atomicity, metrics, resilience)

  • Partial insert rollback — If any consolidated entry fails to insert, already-inserted entries are rolled back before preserving originals (was leaving orphaned consolidated entries)

  • Accurate metricsentriesSummarized counter now tracks actual successful deletes minus inserts (was tracking intent, overstating when deletes failed)

  • Soft cost ceiling documented — Added comment explaining the pre-check design is intentional (can't pre-estimate output tokens). This is not a bug.

  • Null category NPE fixedCollectors.groupingBy now uses null-safe lambda defaulting to "fact" (legacy Mongo entries may have null category)

  • LLM output guardrailsparseConsolidatedEntries now rejects blank keys/values and truncates to MAX_KEY_LENGTH=100/MAX_VALUE_LENGTH=1000 (matches UserMemoryConfig guardrails)

Low Severity (1 — log level)

  • SummarizationService log level — Changed warnferrorf in both exception handlers (RuntimeException + checked) per coding guidelines

New Tests (11 added: 51 DreamService total)

  • summarize_multiAgentSelfScope_upgradesVisibility — visibility upgrade to global

  • summarize_preservesGroupIds — merged groupIds on consolidated entries

  • summarize_nullCategory_defaultsToFact — null-safe grouping

  • parseConsolidatedEntries_blankKeyFiltered — blank key rejection

  • parseConsolidatedEntries_longKeyTruncated — key length guardrail

  • truncate_shortString_unchanged, truncate_longString_truncated, truncate_null_returnsNull — truncate utility

  • summarize_partialInsertFails_rollsBack — rollback on partial insert failure

  • setSummarizeTargetEntries_rejectsZero, setSummarizeTargetEntries_rejectsNegative — config validation

Verification

  • ./mvnw clean test -Dtest=DreamServiceTest,ConversationSummarizerTest,SummarizationServiceTest → 71 tests, 0 failures

  • JaCoCo: DreamService 91.9% line / 86.1% branch, SummarizationService 100% line


🔍 DreamService PR Review Remediation — Pass 1 (2026-05-16)

Repo: EDDI (feature/dream-summarization) What changed: Initial review — 11 findings from self-review, all resolved.

Must-Fix (3)

  • Triple DB reload eliminatedprocess() was calling getAllEntries() three times when pruning + contradiction + summarization were all enabled. Hoisted the post-prune reload so it's shared (contradiction detection is read-only)

  • maxCostPerRun default aligned — Java default changed from $5.00 to $0.50 to match user-memory.md and scheduling.md documentation. Prevents a 10× cost surprise for operators

  • scheduling.md contradiction claim fixed — Changed "Identifies and resolves" to "Identifies and logs for review"

Should-Fix (5)

  • Cost estimator input undercount fixedestimateCost() now takes inputContentLength parameter and estimates from input+output chars when providers don't report tokens (was output-only, underestimating by 5-10×)

  • Dead exception catch block fixedSummarizationService.summarizeWithUsage() now re-throws exceptions (was swallowing them, making DreamService's catch block unreachable). summarize() wrapper retains swallow-and-return-empty behavior for backward compat with ConversationSummarizer

  • contradictionResolution field annotated — Added Javadoc noting it's reserved for future use (V1 detector only counts/logs)

  • HANDOFF.md test counts corrected — DreamServiceTest 37→40, SummarizationServiceTest +1, total 90→94

  • SummarizationResult.hasContent() removed — Unused convenience method

Nitpicks (3)

  • buildEntriesJson now uses injected ObjectMapper — Replaced hand-rolled StringBuilder JSON with objectMapper.writerWithDefaultPrettyPrinter(), keeping manual fallback for resilience

  • Stale Javadoc fixedSummarizationService class doc: "future Dream consolidation" → "Dream memory consolidation"

  • enableSummarization() test helper — Now also sets maxCostPerRun to explicit value for clarity

New Tests (6 added: 40 DreamService + 8 SummarizationService)

  • estimateCost_withTokenUsage — token-based cost calculation

  • estimateCost_withoutTokenUsage_fallsBackToCharEstimate — input+output char fallback

  • summarize_costCeilingReached_stopsEarly — loop stops at cost ceiling

  • summarizeWithUsage_llmError_propagatesException — verifies re-throw (vs summarize() which swallows)

  • summarizeWithUsage_returnsTokenCounts — token usage extraction from LLM response

  • summarizeWithUsage_checkedExceptionWrappedInRuntime — checked exception wrapping

Verification

  • ./mvnw clean test -Dtest=DreamServiceTest,ConversationSummarizerTest,SummarizationServiceTest → 60 tests, 0 failures

  • JaCoCo coverage: DreamService 92% line / 88% branch, SummarizationService 100% line

🧠 DreamService: LLM-Driven Memory Summarization (2026-05-15)

Repo: EDDI (feature/dream-summarization) What changed: Implemented summarizeInteractions() in DreamService — config-driven LLM memory consolidation that compresses related user memory entries via SummarizationService.

DreamConfig (AgentConfiguration.java)

  • Added 6 new config fields: summarizeMinEntries (5), summarizeTargetEntries (2), summarizeGroupBy ("category"/"all"), preserveAgentProvenance (false), maxSummarizationCalls (10), summarizationPrompt (customizable default)

  • All fields have sensible defaults; existing configs with summarizeInteractions=false are unaffected

DreamService

  • Added SummarizationService as constructor dependency (CDI injection)

  • Added entriesSummarizedCounter metric

  • Refactored process() to reload entries only after pruning (contradiction detection is read-only)

  • Implemented summarizeInteractions() with insert-before-delete safety pattern

  • LLM call wrapped in try-catch — failure skips the group, does not kill the dream cycle

  • escapeJson() now uses Jackson's JsonStringEncoder for complete RFC 8259 compliance

  • Helpers: buildGroups() (category/all grouping + agent provenance sub-grouping), parseConsolidatedEntries() (markdown fence stripping, JSON array extraction), mostRestrictiveVisibility(), buildEntriesJson()

Safety Guarantees

  • LLM returns empty/garbage → group skipped, originals untouched

  • LLM throws exception → group skipped, originals untouched, dream cycle continues

  • LLM returns ≥ original count → group skipped

  • LLM returns > target count → result capped to summarizeTargetEntries

  • Insert fails → originals never deleted

  • Delete partially fails → duplicates may remain until next dream cycle (contradiction detector currently only counts/logs; dedup cleanup is a future enhancement)

  • Cost bounded by maxSummarizationCalls

Tests (37 total: 8 existing + 29 new)

  • Updated setUp() for new constructor signature

  • 12 summarization behavior tests: threshold, consolidation, empty/garbage LLM, markdown fences, count validation, insert failure, call limit, groupBy all, agent provenance, custom prompt, visibility merge

  • 9 coverage-hardening tests: null updatedAt, prune delete failure, same-key-same-value no contradiction, LLM result capping, delete partial failure, LLM exception isolation, summarize-after-pruning reload, missing key field filtering, escapeJson control chars/null

  • 8 unit tests: parseConsolidatedEntries (valid/null/blank/fences/missing-key), mostRestrictiveVisibility (self/global/group), escapeJson (control chars/null)

Documentation Updates

  • docs/user-memory.md — Dream config table expanded (6 new fields), config example updated, removed "V2, not yet active" label, added dream.entries.summarized metric

  • docs/scheduling.md — Dream config example updated with new fields

  • HANDOFF.md — Dream description and test count updated

Verification

  • ./mvnw compile → BUILD SUCCESS

  • ./mvnw test -Dtest=DreamServiceTest → 37 tests, 0 failures, 0 errors


🔧 PR Review Remediation — 8 Findings Resolved (2026-05-14)

Repo: EDDI (feature/agentic-improvements) What changed: Addressed all PR review findings from Copilot (7) and CodeRabbit (1), round 2.

Security & Safety Guards

  • RestAttachmentUploadtenantId query param now sanitized (regex: alphanumeric + dash/underscore, max 64 chars). Invalid values silently discarded to null. Security note documents trust boundary.

  • RestAttachmentUploadCompletableFuture.runAsync() now uses injected ManagedExecutor (matches BaseRuntime pattern) instead of default ForkJoinPool. Preserves request context (security, MDC).

  • MultimodalMessageEnhancer — Added MAX_MULTIMODAL_FORWARD_BYTES (10MB) guard on STORED image attachments. Files exceeding this limit get a TextContent placeholder instead of a ~13MB base64 data URI, preventing OOM and LLM API request bloat.

  • ToolResponseTruncator — Added PAGINATE_MAX_STORABLE_CHARS (500K) ceiling. Responses exceeding this fall back to truncation instead of materializing all page substrings in the Caffeine cache.

Bug Fixes

  • AgentSigningServicegenerateKeyPair() now evicts privateKeyCache entry for the tenant:agentId. Previously, key rotation via re-generation would silently keep using the stale cached private key, producing signatures that don't match the new public key.

  • PostgresAttachmentStore / GridFsAttachmentStoreresolvedMime now uses MimeValidator.normalize() (strip ; params, trim, lowercase) before persisting. Prevents non-canonical values like image/png; charset=utf-8 in the database.

Observability

  • RestAttachmentUpload — All upload log messages now include conversationId for correlation (was missing from rejection and success logs, only present in list/delete error logs).

New Utility

  • MimeValidator.normalize() — Static method to produce canonical MIME types. Used by both attachment stores.

Tests (12 new/updated)

  • RestAttachmentUploadTest — Updated for ManagedExecutor constructor. Added shouldRejectInvalidTenantId test (SQL injection → sanitized to null).

  • AgentSigningServiceTest — Added generateKeyPair_evictsCacheOnRegeneration (sign-verify roundtrip proves new key is in use after re-gen).

  • MultimodalMessageEnhancerExtendedTest — Added oversizedStoredImageProducesTextFallback (10MB+1 byte → text placeholder).

  • ToolResponseTruncatorExtendedTest — Added testPaginateCeilingFallback (500K+1 chars → truncation, store never called).

  • MimeValidatorTest — Added 7 NormalizeTests (params, case, trim, null, blank, combined, passthrough).

Verification

  • Clean compile: BUILD SUCCESS, 0 Checkstyle violations

  • 104 targeted tests: 0 failures, 0 errors

🔧 PR Review Remediation — 10 Findings Resolved (2026-05-13)

Repo: EDDI (feature/agentic-improvements) What changed: Addressed all PR review findings from Copilot and CodeRabbit.

Bug Fixes

  • GroupConversationServicesign() was called with gc.getUserId() but private keys are stored under tenant ID. Fixed to use defaultTenantId (from eddi.tenant.default-id config property), matching the AuditLedgerService pattern.

  • RestAgentStorevalidateSecurityFlags() only checked identity.publicKey but ignored identity.keys list. Key-rotated configs were incorrectly rejected. Now accepts either legacy key or rotated keys list.

  • ToolResponseTruncatorSUMMARY_HEADER prepended to summary could push total output past maxChars. Guard 5 now checks summary.length() + header.length() > maxChars.

Architecture Compliance

  • RestAttachmentUpload — All 3 endpoints (upload, list, delete) converted from synchronous Response to AsyncResponse with CompletableFuture.runAsync().

  • RestAttachmentUpload — Added early file size guard (Files.size() before readAllBytes) to prevent OOM. Configurable via eddi.attachments.max-size-bytes (default: 20MB).

  • RestAttachmentUpload — Added LogSanitizer.sanitize() on user-provided file names in log statements.

Documentation

  • changelog.md — Fixed "scheduled/batch" → "scheduled" wording to match code behavior.

Tests Updated

  • RestAttachmentUploadTest — Rewritten for AsyncResponse pattern with CountDownLatch-based capture helper. Added test for OOM size guard.

  • GroupConversationServiceTest — Constructor calls updated for new defaultTenantId parameter.

🧠 Summarize Truncation Strategy — Production Implementation (2026-05-13)

Repo: EDDI (feature/agentic-improvements) What changed: Activated the summarize tool-response truncation strategy, replacing the WARN stub with a fully functional LLM summarization pipeline.

Architecture: Inherit from Parent Task

  • Problem: SummarizationService only passes modelName to ChatModelRegistry — no API key. This works for ConversationSummarizer only because langchain4j falls back to env vars, which is a fragile implicit dependency.

  • Solution: The truncator now receives the parent task's type + parameters (which include apiKey, baseUrl, etc.) and calls ChatModelRegistry.getOrCreate() directly. Only modelName is overridden with summarizerModel. This inherits the full provider context automatically.

Changes

  • ToolResponseTruncator.java — Injected ChatModelRegistry. Implemented summarizeResponse() with 6-point fallback chain: no model → no task context → cost ceiling (200K chars) → model/LLM failure → empty summary → summary-longer-than-limit → all degrade to truncate. Response prefixed with [SUMMARY — original: N chars, tool: name] header.

  • AgentOrchestrator.java — Updated truncateIfNeeded() call to pass task.getType() and task.getParameters().

  • ToolResponseTruncatorTest.java — Updated to new 5-arg API signature and 2-arg constructor.

  • ToolResponseTruncatorExtendedTest.java — 28 tests covering all strategies, all fallback paths, API key inheritance verification, parameter immutability, and case-insensitive strategy selection.

  • LlmTaskTest.java — Updated constructor call to match new signature.

Config Example

Decision: No New Config Fields

summarizerModel already existed on ToolResponseLimits. No summarizerProvider or summarizerApiKey needed — the summarizer inherits everything from the parent task, making the 95% use case (same provider, cheaper model) zero-config beyond setting the model name.

🔧 Checkpoint Integrity & Dead Code Cleanup (2026-05-12)

Repo: EDDI (feature/agentic-improvements) What changed: Fixed 4 findings from final code review — performance bug, dead code, redundant import, and a design bug in property scope preservation.

Bug Fix: Double Deep-Copy (Finding 1)

  • MemorySnapshotService.extractProperties() was calling DeepCopyUtil.deepCopy(), then MemoryCheckpoint.create() deep-copied again — wasting CPU on every checkpoint

  • Fix: extractProperties() now returns a shallow LinkedHashMap copy; MemoryCheckpoint.create() handles the single deep-copy via copyProperties()

Bug Fix: Property Scope Loss on Rollback (Finding 4)

  • MemoryCheckpoint.propertiesCopy was Map<String, Object> (flattened values) — scope, visibility, and type metadata were stripped at checkpoint time

  • restoreProperties() reconstructed all properties with hardcoded Scope.conversation, losing longTerm/step/secret scope

  • Fix: Changed propertiesCopy to Map<String, Property>, which preserves the full Property object (scope, visibility, all value types). copyProperties() clones each Property via its all-args constructor. restoreProperties() now simply puts back the original Property objects

Dead Code Removed (Finding 2)

  • AgentSigningService — Removed generateKeyPairVersioned() + vaultKeyNameVersioned() (38 lines). Only caller was deleted rotateKey(). Tests removed too

  • AgentSigningServiceTest — Removed 2 dead test methods exercising the deleted methods

Minor Cleanup (Finding 3)

  • DeepCopyUtil — Removed redundant import java.util.Collections (already covered by import java.util.*)

Test Improvements

  • MemoryCheckpointTest — Added 3 new tests: scope preservation, visibility preservation, deep-copy mutation isolation

  • MemorySnapshotServiceTest — Updated rollback test to assert scope preservation (longTerm properties survive rollback)

Verification

  • Clean compile: BUILD SUCCESS, 0 Checkstyle violations

  • 5,041 unit tests: 0 failures, 0 errors (21 Docker-dependent infra test errors = pre-existing)

🧹 Dead Code Removal & Immutability Fix (2026-05-08)

Repo: EDDI (feature/agentic-improvements) What changed: Removed all dead code identified during critical branch audit, fixed failing test, improved coverage.

Dead Code Removed (10 files deleted)

  • AttachmentForwarder + test@ApplicationScoped but never injected. MultimodalMessageEnhancer handles the actual attachment→Content conversion

  • NonceCacheService + test — Caffeine-based replay protection, never injected by any endpoint

  • SignedEnvelope + test — Envelope signing record, never used (basic sign()/verify() on AgentSigningService is the live API)

  • JacksonCanonicalizer + test — RFC 8785 canonicalization, only consumer was dead SignedEnvelope

  • DiscoverToolsTool + test — Meta-tool for lazy tool loading, never instantiated by AgentOrchestrator

Dead Code Removed (from live files)

  • AgentSigningService — Removed signEnvelope(), verifyEnvelope(), rotateKey() (never called)

  • LlmConfiguration — Removed ToolLoadingStrategy inner class + field + getter/setter (never read by any pipeline component)

  • AgentSigningServiceTest — Removed 5 tests for deleted methods

Bug Fix

  • DeepCopyUtil.deepCopy() — Wrapped return value in Collections.unmodifiableMap(). MemoryCheckpoint properties are contractually immutable; the test correctly asserted this but the implementation returned a mutable LinkedHashMap

  • DeepCopyUtil.java — Was present in working tree but never committed to Git. Now tracked

Documentation

  • architecture.md — Replaced deleted AttachmentForwarder reference with MultimodalMessageEnhancer

  • ToolResponseTruncator — Summarize strategy log upgraded from DEBUG to WARN with clear "not yet implemented" message. Removed misleading TODO

Coverage Improvements

  • DeepCopyUtilTest (NEW) — 8 tests covering null/empty, primitives, nested maps/lists/sets, immutability

  • DeploymentContextConditionTest — 4 new edge case tests: setConditions no-op, setContainingRuleSet no-op, uninitialized getConfigs, blank when

Verification

  • Clean compile: BUILD SUCCESS

  • 350 targeted tests: 0 failures, 0 errors

🔧 Test Stabilization & Integration Wiring (2026-05-08)

Repo: EDDI (feature/agentic-improvements) What changed: Fixed all compilation and test failures caused by constructor signature changes from cryptographic signing, memory snapshot, and attachment store integrations.

Test Constructor Fixes

  • LlmTaskTest — Added null, null for MemorySnapshotService and IAttachmentStore params.

  • AgentOrchestratorTest — Added null for MemorySnapshotService param.

  • MultimodalMessageEnhancerTest / MultimodalMessageEnhancerExtendedTest — Added null for IAttachmentStore param.

  • GroupConversationServiceTest — Added null, null for AgentSigningService and IAgentStore params at both constructor sites.

  • RestAttachmentUploadTest — Complete rewrite from IAttachmentStorage/Instance<> pattern to new IAttachmentStore-based API. Now tests upload (success, rejection, tenant ID, MIME defaulting), list, and delete endpoints (10 tests).

Production Code Fixes

  • RestAttachmentUpload.java — Fixed attachment.fileName()attachment.filename() to match Attachment record field name.

  • MultimodalMessageEnhancerExtendedTest — Updated storedImageProducesTextFallback assertion from "not yet implemented" to "no attachment store configured" to match implemented STORED path behavior.

JSON Serialization Test Updates

  • DiscoverToolsToolTest — Updated 5 JSON substring assertions to accept both manual ("tools": []) and Jackson compact ("tools":[]) formats after Jackson migration.

  • FetchToolResponsePageToolTest — Updated 3 JSON assertions for Jackson compact format.

Verification

  • Clean compile: BUILD SUCCESS, 0 checkstyle violations

  • 264 targeted tests: 0 failures, 0 errors

🔧 PR Review Remediation — 11 Issues (2026-05-07)

Repo: EDDI (feature/agentic-improvements) What changed: Fixed all 11 issues flagged by GitHub code-quality bot (CodeQL) and Copilot during PR review.

Code Quality Fixes

  • JacksonCanonicalizer: Renamed canonicalize(Object) to canonicalizeObject(Object) to eliminate overload dispatch ambiguity (CodeQL finding).

  • DiscoverToolsTool: Replaced partial String.replace() escaping with full escapeJson() utility for all interpolated fields (name, description). Prevents invalid JSON from tool names containing backslashes/newlines.

  • FetchToolResponsePageTool: Applied escapeJson() to error and toolName fields (previously unescaped).

  • DeploymentContextCondition: setConfigs(null) now explicitly clears when and tagMatches fields to prevent stale config on condition reuse.

  • CapabilityMatchCondition: Fixed stepIndex off-by-one (.size().size() - 1) for 0-based consistency with audit ledger.

  • CapabilityRegistryService: Extracted lookupBySkill() internal method to prevent findBySkillAndAttributes() from double-counting strategy metrics via findBySkill("all").

Javadoc Accuracy

  • LlmConfiguration.summarizerModel: Removed phantom claim about eddi.mcp.summarizer.model config-property defaulting (no such binding exists; null means fallback to truncation).

  • MemorySnapshotService.rollbackToCheckpoint: Doc now accurately states only properties are restored, not step index or step stack.

  • MemoryCheckpoint: Class-level doc updated from "full step stack" to "stepIndex + properties snapshot".

Documentation

  • langchain.md: Auto-downgrade now correctly says "scheduled channel" only, not "scheduled or batch mode".

Test Improvements

  • RestAgentStoreTest: Replaced 2 brittle NPE-based assertions with proper agentStore.create() mocking + assertDoesNotThrow().

  • CapabilityMatchConditionTest: Updated stepIndex assertion from 3 to 2 to match 0-based fix.

📝 Documentation Corrections & Postgres Verification (2026-05-07)

Repo: EDDI (feature/agentic-improvements) What changed: Fixed 3 documentation bugs found during multi-perspective audit, improved usability notes, verified PostgreSQL compatibility.

Documentation Fixes

  • langchain.md: Removed non-existent agentName field from identityMasking examples and parameter table (field was never implemented in IdentityMaskingConfig).

  • langchain.md: Corrected placement values from append/prepend to suffix/prefix to match the actual CounterweightService code. Corrected default from append to suffix.

  • langchain.md: Added explicit enabled: true to counterweight JSON example and added usability notes explaining that both enabled: true AND a non-normal level (or at least one rule for masking) are required for activation.

  • langchain.md: Added counterweight.enabled row to parameter table (was missing), fixed customInstructions type from string to string[].

Migration Note

  • identityMasking was moved from AgentConfiguration (agent-level) to LlmConfiguration.Task (task-level). Old agent configs with identityMasking at the agent level will have this field silently ignored (Jackson FAIL_ON_UNKNOWN_PROPERTIES=false). Since this is a new feature on this branch, no production configs are affected.

PostgreSQL Verification

  • Confirmed all modified components work identically on both MongoDB and PostgreSQL:

    • IAttachmentStorePostgresAttachmentStore uses correct engine.attachments import

    • IConversationCheckpointStorePostgresConversationCheckpointStore has full CRUD + prune

    • IPromptSnippetStoreAbstractResourceStore via PostgresResourceStorageFactory (no Postgres-specific snippet store needed)

    • ISecretPersistencePostgresSecretPersistence (for AgentSigningService key storage)

    • DataStoreProducers correctly wires all stores for both backends

    • Jackson SerializationCustomizer applies to both backends (shared ObjectMapper)

📊 Test Coverage Audit & Documentation Enrichment (2026-05-07)

Repo: EDDI (feature/agentic-wave3-capabilities) What changed: Boosted test coverage across all Wave 1–6 components to >86% (11/13 at ≥97%), and enriched architecture and LLM configuration docs.

Test Coverage Improvements

  • MimeValidatorTest: Added 11 tests for previously uncovered magic-byte branches (BMP, WebP, TIFF LE/BE, MP4, WAV, MP3 frame-sync variants) and isCompatible edge cases (null handling, ZIP subtypes, case insensitivity). Coverage: 72.8% → 99.6%.

  • AgentSigningServiceTest: Added 7 tests for versioned key generation, envelope sign/verify roundtrip, unversioned key path, tampered payload detection, invalid base64 handling, and delete-nonexistent path. Coverage: 48.5% → 86.7%.

  • MemorySnapshotServiceTest: Added test exercising all type branches in restoreProperties (String, Integer, Float, Boolean, List, Map, Long fallback). Coverage: 77.9% → 100%.

Documentation Enrichment

  • langchain.md: Added "Behavioral Safety (Counterweight & Identity Masking)" section with configuration examples, parameter tables, and execution order documentation.

  • architecture.md: Added "System Prompt Modifiers" and "Attachment Storage" subsections under Key Components, documenting the new engine.attachments package organization.

🏗️ Architectural Fixes — Pillar Compliance (2026-05-07)

Repo: EDDI (feature/agentic-wave3-capabilities) What changed: Fixed 3 architectural concerns identified during deep audit against the 9 Pillars.

Concern 1: CounterweightService → Prompt Snippets (Pillar 1)

  • Before: Preset text (CAUTIOUS_PRESET, STRICT_PRESET) was hardcoded as Java constants — agent behavior baked into code.

  • After: CounterweightService now injects PromptSnippetService and resolves presets from snippets (counterweight-cautious, counterweight-strict) first, falling back to built-in defaults.

  • Impact: Admins can customize counterweight presets via the Prompt Snippets REST API without recompilation.

  • Files: CounterweightService.java, CounterweightServiceTest.java, LlmTaskTest.java

Concern 2: IdentityMaskingConfig → LlmConfiguration.Task (Pillar 8)

  • Before: IdentityMaskingConfig was on AgentConfiguration and smuggled through IConversationMemory via bespoke getter/setter. This mixed configuration passthrough with conversational state.

  • After: IdentityMaskingConfig class moved to LlmConfiguration alongside CounterweightConfig. Config read from task.getIdentityMasking() — consistent with task.getCounterweight().

  • Impact: Removed 2 methods from IConversationMemory, 1 transient field from ConversationMemory, wiring from Agent.java and AgentStoreClientLibrary. Memory interface is cleaner.

  • Files: LlmConfiguration.java, AgentConfiguration.java, IConversationMemory.java, ConversationMemory.java, Agent.java, AgentStoreClientLibrary.java, LlmTask.java, IdentityMaskingService.java, IdentityMaskingServiceTest.java

Concern 3: IAttachmentStore/MimeValidator → engine.attachments (Package Organization)

  • Before: IAttachmentStore and MimeValidator in engine.memory package despite having nothing to do with conversation memory.

  • After: Moved to new ai.labs.eddi.engine.attachments package. All imports updated across source and test files.

  • Files: IAttachmentStore.java, MimeValidator.java, MimeValidatorTest.java, GridFsAttachmentStore.java, PostgresAttachmentStore.java, DataStoreProducers.java, AttachmentForwarder.java, AttachmentForwarderTest.java

Verification

  • Clean compile: BUILD SUCCESS

  • All 111 affected tests pass (0 failures, 0 errors)


🔐 Wave 6 — Cryptographic Agent Identity (2026-05-07)

Repo: EDDI (feature/agentic-wave3-capabilities) What changed: Implemented Wave 6 — Ed25519 cryptographic identity with key rotation, signed envelopes, and nonce-based replay protection.

New Components

  • JacksonCanonicalizer — RFC 8785 JSON canonicalization using pure Jackson tree model (recursive key sorting, no external dep).

  • SignedEnvelope — Immutable record with forSigning()/withSignature() factories and canonicalForm() for deterministic signing.

  • NonceCacheService — Caffeine-backed replay protection: freshness (5min default), clock-skew (30s), and duplicate detection with Micrometer counters.

  • AgentPublicKey — Versioned key record with isValidAt(epochMs), createCurrent(), and withExpiry() for rotation windows.

Modified Components

  • AgentIdentity — Added List<AgentPublicKey> keys with getKeyForVersion(int) and getKeyValidAt(long) for multi-key rotation.

  • AgentSigningService — Added signEnvelope(), verifyEnvelope(), rotateKey(), generateKeyPairVersioned(). Versioned vault keys stored as agent-signing-key:{id}:v{n}.

Design Decisions

  • Pure Jackson canonicalization — No JCS library dep. Uses TreeMap + recursive sortKeys() for RFC 8785 compliance.

  • Envelope canonical form excludes signature — Prevents circular dependency: the canonical form is the data being signed.

  • Versioned vault key naming — Pattern agent-signing-key:{agentId}:v{version} allows parallel old/new keys during rotation.

  • Feature flageddi.a2a.signing.enabled (default false) guards all signing at call sites.

Tests (30 new)

  • JacksonCanonicalizerTest — 11 tests: key sorting, data types, determinism, error handling

  • SignedEnvelopeTest — 5 tests: forSigning, withSignature, canonicalForm

  • NonceCacheServiceTest — 7 tests: freshness, clock skew, replay detection

  • AgentPublicKeyTest — 7 tests: validity windows, factory methods, equality

📎 Wave 5 — Multimodal Attachments (2026-05-07)

Repo: EDDI (feature/agentic-wave3-capabilities) What changed: Implemented Wave 5 — multimodal attachment support with dual-backend storage and magic-byte MIME validation.

New Components

  • IAttachmentStore (interface) — Store/load/delete/list with conversation-scoped access control and GDPR erasure.

  • PostgresAttachmentStore — PostgreSQL impl using BYTEA columns with size cap config.

  • GridFsAttachmentStore — MongoDB GridFS impl with metadata-based conversation scoping.

  • MimeValidator — Magic-byte MIME detection for 14 file types (no external dep). Compatibility checking with ZIP subtype support.

  • AttachmentForwarder — Converts attachments to langchain4j ImageContent (images) or TextContent markers (other files).

Design Decisions

  • No Apache Tika — Not in transitive deps; magic-byte header check is sufficient and avoids 20MB+ dep.

  • BYTEA over large objects — Simpler for PostgreSQL with 20MB cap. Large objects add complexity without benefit.

  • Cross-conversation access denied — Every load() validates conversation ownership. Defense in depth.

  • Base64 data URIs — Images forwarded to LLM as data URIs for maximum provider compatibility.

Tests (26 new)

  • MimeValidatorTest — 17 tests: detection for 8 types + compatibility + edge cases

  • AttachmentForwarderTest — 9 tests: image/non-image/error handling/isImageType

🔒 Wave 4 — Session Safety (Snapshot + Fork) (2026-05-07)

Repo: EDDI (feature/agentic-wave3-capabilities) What changed: Implemented Wave 4 of the agentic improvements — memory checkpoints, snapshot/rollback, and session management configuration.

New Components

  • MemoryCheckpoint (record) — Immutable snapshot of conversation state (step index, properties copy, triggered-by metadata). Supports create() factory and withParent() for forking.

  • IConversationCheckpointStore (interface) — DB-agnostic CRUD + pruning + GDPR erasure for checkpoints.

  • MongoConversationCheckpointStore — MongoDB implementation with compound index on (conversationId, createdAt).

  • PostgresConversationCheckpointStore — PostgreSQL implementation with JSONB storage and indexed columns.

  • MemorySnapshotService — Creates/restores checkpoints with auto-pruning, type-aware property restoration, and Micrometer metrics.

  • SessionManagement config — Inner class in AgentConfiguration with AutoSnapshot, forkingEnabled, maxForksPerConversation, maxCheckpointsPerConversation.

Design Decisions

  • DB-agnostic from day one — Both MongoDB and PostgreSQL implementations created simultaneously, wired via DataStoreProducers.

  • Type-aware property restore — Properties restored using Java pattern matching (instanceof) to route to correct Property constructor (String, Map, List, Integer, Float, Boolean).

  • JBoss Logger debugf ambiguity — Cast numeric args to (Object) to resolve overloaded method ambiguity with int/long parameter variants.

  • No ConversationForkService yet — Deep-copy logic deferred to integration wave when ToolExecutionService is wired.

Tests (22 new)

  • MemoryCheckpointTest — 7 tests: create/immutability/withParent/uniqueIds/equality

  • MemorySnapshotServiceTest — 10 tests: create/rollback/CRUD/null-safety/metrics

  • SessionManagementTest — 5 tests: defaults/AutoSnapshot/getters/integration

Files Modified

  • AgentConfiguration.java — Added SessionManagement field and inner class

  • DataStoreProducers.java — Added IConversationCheckpointStore producer

🔧 Wave 2 — MCP Governance & Token-Efficient Tool Loading (2026-05-07)

Repo: EDDI (feature/agentic-wave3-capabilities) What changed: Implemented Wave 2 of the agentic improvements — paginated tool responses, lazy/dynamic tool loading, and enhanced truncation strategies.

New Components

  • PaginatedResponseStore — Caffeine-backed store for paginated tool responses (15min TTL). Splits oversized tool output into retrievable pages.

  • FetchToolResponsePageTool — Built-in LLM tool (fetch_tool_response_page) that retrieves pages from the store by responseId.

  • DiscoverToolsTool — Meta-tool for lazy/dynamic tool loading. LLM discovers available tools by category/keyword instead of receiving all tool schemas upfront.

  • ToolLoadingStrategy config class — Controls tool presentation: eager (all upfront), lazy (only discover_tools first), dynamic (action-filtered).

Enhanced Components

  • ToolResponseTruncator — Now supports three strategies via truncationStrategy config:

    • truncate (default) — hard cut with original behavior

    • paginate — stores pages in PaginatedResponseStore, returns first page + responseId

    • summarize — routes through cheap model (summarizerModel config), falls back to truncate on failure or cost ceiling (>200k chars)

  • ToolResponseLimits — Added truncationStrategy and summarizerModel fields

  • AgentOrchestrator — Added FetchToolResponsePageTool as built-in tool

Design Decisions

  • Paginate as opt-intruncate remains default for backward compatibility

  • Summarize stub — Summarizer model integration is stubbed with proper fallback chain; actual model call wiring deferred until ChatModelRegistry supports secondary model lookups

  • DiscoverToolsTool not CDI-managed — Constructed per-invocation with available tool specs since it needs runtime context

  • FetchToolResponsePageTool is CDI — Singleton since it only reads from PaginatedResponseStore

Tests (42 new)

  • PaginatedResponseStoreTest — 10 tests: store/page/count/edge cases

  • FetchToolResponsePageToolTest — 7 tests: validation/expired/success/escaping

  • DiscoverToolsToolTest — 12 tests: category/keyword/cap/edge cases

  • ToolResponseTruncatorExtendedTest — 13 tests: all strategies/fallbacks/selection

Files Modified

  • LlmConfiguration.java — Added ToolLoadingStrategy, enhanced ToolResponseLimits

  • ToolResponseTruncator.java — Three strategies with fallback chain

  • AgentOrchestrator.java — FetchToolResponsePageTool wiring

  • LlmTask.java — Constructor updated for FetchToolResponsePageTool

  • AgentOrchestratorTest.java — Updated for new constructor parameter

  • LlmTaskTest.java — Updated for new constructor parameter

🛡️ Wave 1 — Behavioral Counterweights & Identity Masking (2026-05-07)

Repo: EDDI (feature/agentic-wave3-capabilities) What changed: Implemented Wave 1 of the agentic improvements plan — config-driven behavioral counterweights and identity masking.

New Components

  • CounterweightService — Engine-level safety injection into LLM system prompts. Level presets: normal (no-op), cautious, strict. Strict auto-downgrades to cautious for scheduled agents. Custom instructions override presets.

  • IdentityMaskingService — Prepends identity concealment rules to system prompts. Independent of counterweights; agent-level config.

  • DeploymentContextCondition — Behavior rule condition matching on EDDI_DEPLOYMENT_ENV and agent tags. Enables environment-aware routing (e.g., force cautious in production).

  • CounterweightConfig — Inner class in LlmConfiguration.Task for per-task counterweight configuration (level, placement, customInstructions).

  • IdentityMaskingConfig — Inner class in AgentConfiguration for identity masking rules.

Modified Files

  • LlmTask.java — Injected both services; calls identity masking → counterweight after system prompt compilation, before message building.

  • LlmConfiguration.java — Added CounterweightConfig inner class and field to Task.

  • AgentConfiguration.java — Added IdentityMaskingConfig inner class and field.

  • RuleDeserialization.java — Registered DeploymentContextCondition in condition factory.

  • IConversationMemory.java / ConversationMemory.java — Added getIdentityMaskingConfig() / setIdentityMaskingConfig().

  • Agent.java / AgentStoreClientLibrary.java — Threading identity masking config from agent factory to conversation memory.

Design Decisions

  • Counterweights are system prompt injections, not a separate pipeline task — they are a pre-LLM-call concern.

  • Identity masking is prepended before counterweight injection. Order: masking (agent-level) → counterweight (task-level).

  • Channel tag "scheduled" triggers strict→cautious downgrade to prevent one-step-at-a-time being destructive for batch agents.

  • All new config fields are @JsonInclude(NON_NULL) with safe defaults for backward compatibility.

Tests (33 new)

  • CounterweightServiceTest (14 tests) — all levels, placements, custom instructions, scheduled downgrade, metrics, case sensitivity.

  • IdentityMaskingServiceTest (7 tests) — enabled/disabled, empty/null rules, metrics, formatting.

  • DeploymentContextConditionTest (12 tests) — env matching, tag matching, case insensitivity, null configs, clone.

  • LlmTaskTest (55 existing tests) — all pass, no regressions.

Metrics

  • eddi.counterweight.activation.count{level} — counter per activation level

  • eddi.counterweight.strict.downgraded — counter for strict→cautious downgrades

  • eddi.identity.masking.applied — counter for masking activations


🔧 Wave 3 — A2A Capability Registry Gap Closure (2026-05-07)

Repo: EDDI (feature/agentic-wave3-capabilities) What changed: Closed all five outstanding gaps from Wave 3 of the agentic improvements plan.

Changes

  1. Fix round_robin strategy bug (CapabilityRegistryService.java): Replaced Collections.shuffle() with deterministic AtomicInteger-based per-skill rotation. Added explicit "random" strategy for when shuffling is actually desired. Counters reset on agent register/unregister to avoid drift on topology changes.

  2. Reject inert security flags (RestAgentStore.java): Agent create/update now returns HTTP 400 if signInterAgentMessages, signMcpInvocations, or requirePeerVerification is set to true. These cryptographic identity features are not yet implemented (Wave 6). Prevents silent misconfiguration.

  3. Public capability discovery endpoint (RestA2AEndpoint.java):

    • GET /.well-known/capabilities?skill=X&strategy=highest_confidence — queries registry, returns sanitized matches

    • GET /.well-known/capabilities/skills — lists all registered skill names

    • Gated behind eddi.a2a.capabilities.public config property (default false)

    • Same auth model as /.well-known/agent.json

  4. Audit capability selections (CapabilityMatchCondition.java): After a successful match, emits CAPABILITY_SELECTION audit event via memory.getAuditCollector() with skill, strategy, candidateAgentIds, and selectedAgentId. Provides immutable audit trail for compliance.

  5. Missing metrics (CapabilityRegistryService.java):

    • eddi.capability.miss.count (tagged by skill) — counts queries with no results

    • eddi.capability.strategy.applied (tagged by strategy) — tracks which strategy is used

Design Decisions

  • No new abstractions: The existing Capability model on AgentConfiguration is sufficient. Workflows are already the implementation unit; capabilities are the declaration layer. No "Skill Pack" resource type needed.

  • Backward compatible: All changes are additive. Existing configs work unchanged.

  • Public endpoint defaults to off: eddi.a2a.capabilities.public=false — admin must explicitly opt in.

🐛 Fix: Windows PowerShell install command (2026-05-06)

Repo: EDDI (docs/windows-install-command)

What changed: Replaced the broken scriptblock::Create one-liner (and its predecessor iwr | iex) with a download-and-execute approach that works on PowerShell 5.1+ and avoids expression-parser limitations.

Root Cause

install.ps1 uses <# #> block comments with &, [CmdletBinding()], and param() — syntax only valid in the script-file parser. Both iex and [scriptblock]::Create() use the expression parser, which rejects these constructs. Behavior was also environment-dependent (passed on some PS 5.1 builds, failed on others).

Fix

Download-and-execute — the only pattern that uses the script-file parser:

Files: install.ps1 (.EXAMPLE comment), README.md, docs/getting-started.md, HANDOFF.md


🐛 Improved Template Error Messages (2026-05-06)

Repo: EDDI (fix/template-error-message)

What changed: Made template rendering error messages actionable instead of generic.

Problem

When a system prompt or output template referenced a missing variable (e.g., {context.language} when no context is set), the error message was:

This gave no indication of which variable was missing or which template failed, making it hard for agent designers to debug their configurations.

Fix

  • TemplatingEngine: Error now includes the Qute engine's specific cause (which expression failed) and a preview of the first 200 chars of the failing template

  • LlmTask: Error now includes the parameter key (e.g., systemMessage, prompt) so designers know which LLM config parameter has the broken reference

  • OutputTemplateTask: Error now includes the output key or quick reply value for the failing template

Example (before vs after)

Before:

After:

Files: TemplatingEngine.java, LlmTask.java, OutputTemplateTask.java


🐛 Fix: install.ps1 fails when invoked via iwr | iex (2026-05-06)

Repo: EDDI (fix/install-ps1-iwr-iex-compat)

What changed: Replaced the documented iwr -useb ... | iex one-liner with & ([scriptblock]::Create((iwr -useb ...).Content)) across all docs.

Root Cause

When PowerShell pipes content to Invoke-Expression (iex), it processes the text as a raw expression string — not as a script file. This means:

  • <# ... #> block comments containing & are parsed as code (the & call operator is reserved)

  • [CmdletBinding()] and param() blocks are only valid at the top of a script file, not inside an expression

  • The [ValidateSet] workaround from the previous fix (2026-04-01) addressed one symptom but the fundamental parsing issue remained

The German error message confirms this: "Das kaufmännische Und-Zeichen (&) ist nicht zulässig" — the & in the ASCII art comment One-Command Install & Onboarding Wizard is being treated as a PowerShell operator.

Fix

The [scriptblock]::Create() pattern downloads the entire script text via .Content, parses it as a complete script block (honoring block comments, param(), etc.), then executes it. This is the standard pattern used by major installers (Scoop, Chocolatey) for scripts with advanced PowerShell syntax.

Files Changed

File
Change

install.ps1

Updated .EXAMPLE comment to use new pattern

README.md

Updated Quick Start PowerShell command

docs/getting-started.md

Updated Option 0 PowerShell command

HANDOFF.md

Updated installer reference


🔧 PR #470 Review Remediation (2026-05-05)

Repo: EDDI (feat/global-variables)

What changed: Addressed all Copilot and CodeRabbit review feedback on the Global Variables PR.

Code Fixes

File
Issue
Fix

GlobalVariableCrudIT

Invalid key test accepted 500 (should only accept 400)

Assert statusCode(400) only — IllegalArgumentExceptionMapper guarantees 400

DataStoreProducers

Missing CDI producer for IGlobalVariableStore (ambiguous bean)

Added globalVariableStore() producer following established pattern

RestGlobalVariableStore

Null variable body → NPE → 500

Added null guard throwing BadRequestException + unit test

GlobalVariableResolver

getTemplateData(null) didn't normalize to DEFAULT_TENANT

Added null → "default" fallback, matching resolveValue()

PostgresGlobalVariableStore

All SQLExceptions silently swallowed, returning empty results

Re-throw as RuntimeException — DB outage now surfaces properly

A2AToolProviderManager

warnIfRawKey() false-positive on ${vars:...} references

Added ${vars:} prefix recognition alongside vault prefixes

McpSetupTools

API key @ToolArg description incorrectly said "required for cloud providers"

Clarified: bedrock uses IAM, oracle-genai uses OCI auth — not all cloud providers need apiKey

Test Updates

File
Change

PostgresGlobalVariableStoreTest

4 error-handling tests now assert RuntimeException propagation instead of silent empty returns

RestGlobalVariableStoreTest

+1 test: upsertVariableNullBody verifies BadRequestException on missing body

Documentation Fixes

File
Fix

changelog.md

Section title eddivarvars; resolution order eddivar/eddivaultvars/vault; A2A Security typo (duplicate ${vault:}); test count 48 → 75; composite key descriptions

global-variables.md

Added text language tags to 4 bare fenced code blocks (MD040)

secrets-vault.md

Added text language tags to 4 bare fenced code blocks (MD040)

Files: DataStoreProducers.java, RestGlobalVariableStore.java, GlobalVariableResolver.java, PostgresGlobalVariableStore.java, A2AToolProviderManager.java, McpSetupTools.java, GlobalVariableCrudIT.java, PostgresGlobalVariableStoreTest.java, RestGlobalVariableStoreTest.java, changelog.md, global-variables.md, secrets-vault.md


🔒 CVE-2026-42198 — PostgreSQL JDBC DoS Fix (2026-05-02)

Repo: EDDI (fix/cve-2026-42198-postgresql)

What changed: Pinned org.postgresql:postgresql to 42.7.11 in <dependencyManagement> to fix CVE-2026-42198 (CVSS 7.5 High — client-side DoS via SCRAM-SHA-256 iteration count abuse).

Vulnerability

A malicious or compromised PostgreSQL server can send an excessively large PBKDF2 iteration count during SCRAM authentication. The JDBC driver (42.2.0–42.7.10) performs the computation without limits, exhausting client CPU and potentially wedging connection pools. loginTimeout does not mitigate it because the worker thread continues computing after timeout.

Investigation

  • langchain4j-pgvector hardcodes postgresql.version=42.7.7 in its source POM (even on main / 1.15.0-SNAPSHOT). The 1.14.0-beta24 release ships 42.7.7 — older than our previous 42.7.10.

  • Quarkus BOM (3.34.6) manages postgresql via quarkus-jdbc-postgresql at 42.7.10 — also vulnerable.

  • Neither upstream has released a fix. The <dependencyManagement> override is the correct remediation.

Files

  • pom.xml — Added <dependencyManagement> override for org.postgresql:postgresql:42.7.11

Verification: mvnw compile BUILD SUCCESS. dependency:tree confirms single resolved version 42.7.11.


🔔 Slack Notification — Digest Improvements (2026-05-01)

Repo: EDDI (fix/slack-notification-deltas)

What changed: Fixed bogus Slack daily/weekly deltas, added views/clones delta tracking, rescheduled digests, and made daily/weekly baselines independent.

Root Cause (bogus deltas)

The Validate baselines step only checked whether the required day/week baseline fields existed in the cached metrics.json, not whether they contained sensible values. When the GitHub Actions cache was evicted or rebuilt, those baseline fields were present, but the Docker baseline could still be set to 0 (from the initial seeding), making day_valid=true. The daily digest then computed delta = current - 0 = current, producing absurd deltas (e.g., +391490 pulls).

Changes

  1. Strengthened baseline validation — baselines are now rejected if the Docker pulls baseline is 0. Docker pulls only increase, so a zero baseline is always a cold-start artifact for an established project.

  2. Added sanity guards in digest steps — both daily and weekly digest steps detect when delta == current (baseline was 0) and skip the notification.

  3. Views/clones delta tracking — added day_views, day_clones, week_views, week_clones baselines to the metrics cache. Digests now show deltas for all 5 stats. Deltas are conditionally suppressed when the baseline field is missing from an older cache (avoids one-time bogus delta on first deploy).

  4. Rescheduled digests — daily moved from 6pm UTC (8pm CEST) to 7am UTC (9am CEST); weekly moved from Sunday 9am UTC to Monday 7am UTC (9am CEST).

  5. Independent day/week baselines — weekly runs no longer reset daily baselines. On Mondays both digests fire independently: daily shows yesterday's change, weekly shows the full week.

Decision

  • Views/clones baselines are NOT included in the field-presence check (DAY_PRESENT/WEEK_PRESENT). Adding them would skip the entire digest on first deploy (old cache lacks the new fields). Instead, view/clone deltas are conditionally hidden when the baseline is 0.

Files: .github/workflows/docker-pull-notify.yml, docs/changelog.md


🏷️ Late-Binding Prefix Rename (2026-05-01)

Repo: EDDI (feat/global-variables)

What changed: Unified late-binding reference syntax to use short, clean prefixes:

  • ${eddivar:...}${vars:...} (clean rename — never shipped)

  • ${eddivault:...}${vault:...} (backward-compat alias retained via dual-pattern regex)

Why: The eddi prefix was redundant (you're already inside the EDDI platform) and inconsistent with the template layer which already uses {{vars.x}}. Short names improve DX for agent designers.

Backward Compatibility

  • ${eddivault:...} is still accepted everywhere (regex alternation: (?:vault|eddivault))

  • Agent import auto-migrates: ${eddivault:...}${vault:...} on import

  • toReferenceString() now outputs the new canonical form ${vault:...}

Files Changed

Area
Files
Change

Vault core

SecretReference.java

Dual-pattern regex, new canonical output

Vault sanitization

SecretScrubber.java, SecretRedactionFilter.java

Dual-prefix detection

Variable resolver

GlobalVariableResolver.java

EDDIVAR_PATTERNVARS_PATTERN

Callsites

ApiCallExecutor, ChatModelRegistry, A2AToolProviderManager, VaultStartupBanner

Dual-prefix checks, new log messages

Import

AbstractBackupService, RestImportService

Auto-migrate eddivaultvault on import

Javadoc

10+ source files

Updated syntax examples

Tests

18 test files

Updated string literals + 4 backward-compat tests

Docs

12 markdown files + AGENTS.md

Updated all examples


⚙️ Global Variable Store — vars (2026-05-01)

Repo: EDDI (feat/global-variables)

What changed: Added a non-encrypted, deployment-wide Global Variable Store for runtime configuration parameters. Enables changing operational values (LLM models, API endpoints, temperatures, feature flags) across all agents simultaneously without redeployment.

New Components

Component
Purpose

GlobalVariable

Record model: key, value, description, exportable

IGlobalVariableStore

Persistence interface (non-versioned, flat key-value)

GlobalVariableStore

MongoDB adapter (globalvariables collection, composite _id = tenantId/key)

PostgresGlobalVariableStore

PostgreSQL adapter (global_variables table, PK = (tenant_id, key))

GlobalVariableResolver

Regex-based ${vars:<key>} resolution with Caffeine cache + invalidation listeners

IRestGlobalVariableStore

JAX-RS REST API (/variablestore/variables)

RestGlobalVariableStore

REST implementation with key validation and write-through cache invalidation

Pipeline Integration (8 callsites)

Resolution order: Jinja2/Qute templates → vars → vault. Integrated into:

  1. LlmTask{{vars.<key>}} template injection + ${vars:...} in type field (provider late-binding)

  2. ChatModelRegistryresolveAll before resolveSecrets, registers invalidation listener

  3. ApiCallExecutor — URL, body, headers, query params

  4. McpToolProviderManager — API key/URL resolution

  5. A2AToolProviderManager — API key/URL resolution

  6. EmbeddingModelFactory — config params before model creation

  7. EmbeddingStoreFactory — config params before store creation

  8. SlackChannelRouter — channel config values

Design Decisions

  • Two syntaxes: {{vars.<key>}} for template layer (system prompts), ${vars:<key>} for late-binding layer (everywhere). Same data, two access patterns for different resolution stages.

  • Non-versioned: Unlike agent configs, global variables are operational deployment config. No version history — upsert semantics with PUT.

  • Non-encrypted: Fully visible in UI and logs. Use the vault (${vault:...}) for sensitive values.

  • Invalidation listeners: Downstream caches (ChatModelRegistry) register Runnable callbacks. When variables change, all cached model instances are evicted so agents pick up new config on next request.

  • exportable flag: Variables marked exportable: false are excluded from agent exports (e.g., environment-specific URLs).

Bug Fixes

  • BUG-1 (LlmTask): task.getType() was used raw (unresolved) in tokenCounterFactory.getEstimator() (line 288) and chatModelRegistry.getOrCreateStreaming() (line 411). Hoisted resolvedType above both branches so ${vars:default-provider} works correctly for token-aware windowing and streaming mode.

  • BUG-2 (Resolver Null Safety): Added guard in GlobalVariableResolver.resolveValue(value, tenantId) to default a null tenantId to "default" before hitting the store, preventing potential undefined behavior.

  • BUG-3 (MongoDB Filter Parity): Extracted compositeId() helper and updated MongoDB get() and delete() methods to filter by _id (consistent with upsert()) instead of by fields. Added Sorts.ascending to MongoDB getAll/listAll for parity with Postgres.

  • BUG-4 (Postgres Reserved Words): Quoted SQL reserved words "key" and "value" across all DDL and DML in PostgresGlobalVariableStore and updated the corresponding test matchers.

Documentation

  • New: docs/global-variables.md — comprehensive public docs with architecture, syntax, REST API, use cases, and comparison table

  • Updated: AGENTS.md — added snippets and vars to both template data model tables (sections 4.2 and 5.1)

Tests (75 total)

Test Class
Tests
Type

GlobalVariableTest

7

Unit — model, defaults, JSON

GlobalVariableResolverTest

14

Unit — resolution, cache, invalidation

RestGlobalVariableStoreTest

11

Unit — CRUD, validation

GlobalVariableCrudIT

8

Integration — MongoDB CRUD lifecycle

PostgresGlobalVariableCrudIT

8

Integration — PostgreSQL CRUD lifecycle

GlobalVariableStoreTest

10

Unit — MongoDB adapter with mocked MongoCollection

PostgresGlobalVariableStoreTest

15

Unit — PostgreSQL adapter with mocked JDBC

9 modified test suites

Constructor dependency updates

Files: src/main/java/ai/labs/eddi/configs/variables/ (6 new files), src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java, src/main/java/ai/labs/eddi/modules/llm/impl/ChatModelRegistry.java, docs/global-variables.md, AGENTS.md


Last updated

Was this helpful?