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()withputIfAbsent()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 inGroupConversationService,MongoTenantQuotaStore,PostgresTenantQuotaStorewith centralizedLogSanitizer.sanitize(). Added Unicode line separator (U+2028/U+2029) handling per CodeQL feedback. Also wrappede.getMessage()in log calls.Fail-closed cost accounting:
PostgresTenantQuotaStore.tryAddCost()now returnsDENIEDon SQL failure instead ofOK— prevents budget bypass when database is unreachable.Key version validation:
AgentSigningService.generateKeyPairVersioned()androtateKey()now rejectversion <= 0.JacksonCanonicalizer strict duplicate detection: Enabled
StreamReadFeature.STRICT_DUPLICATE_DETECTIONto 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 redundantagentStorelookups.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
@Vetoedto 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), whileexecuteWithTools()initially presents onlydiscover_toolsspec. After the LLM callsdiscover_tools, matching built-in specs are activated viaactivateDiscoveredTools().PostgresTenantQuotaStore transactional delete:
deleteQuota()now wraps bothtenant_quotasandtenant_usagedeletes in a single transaction with rollback on failure.PostgresTenantQuotaStore schema auto-creation: Added
CREATE TABLE IF NOT EXISTSwithensureSchema()pattern (matchingPostgresGlobalVariableStore,PostgresSecretPersistence, etc.).MongoTenantQuotaStore unique index: Added unique ascending index on
tenantIdfor bothtenant_quotasandtenant_usagecollections to prevent duplicate rows from upsert races.DiscoverToolsTool JSON serialization: Replaced manual
StringBuilderJSON assembly with JacksonObjectMapperfor 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() - 1to avoid misleading-1in 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 (
verifyEnvelopereturns false) → discard signature, fall back to unsigned entryNonce 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,nonceCacheServiceall guarded for nullagentConfig.getIdentity()guarded beforegetKeyValidAt()call
S7 (LOW): NonceCacheService unused ttlMs variable
Removed computed
ttlMsthat was never passed to cache factoryAdded 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 sectionplanning/manager-ui-handoff.md: removedsignMcpInvocations,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:
signMcpInvocationsfromSecurityConfig(no MCP signing implementation exists)Removed:
forkingEnabled+maxForksPerConversationfromSessionManagement(no forking service exists)Rationale: "Configs without functionality" creates false confidence. Features are added alongside their implementation, not before.
Files:
AgentConfiguration.java,RestAgentStore.java(removedvalidateSessionFlags()), tests updated
TranscriptEntry — Full Envelope Storage
Added:
signatureNonce,signatureTimestampMs,signatureKeyVersionfields toTranscriptEntryrecordAdded:
hasEnvelopeData()convenience method for verification checksBackward-compatible: Two compact constructors for unsigned and signature-only entries
Files:
GroupConversation.java
GroupConversationService — End-to-End Crypto Wiring
Injected:
NonceCacheServicefor replay protectionSigning block: Now creates full
SignedEnvelopewith nonce, immediately self-verifies, registers nonce, and stores all envelope fields inTranscriptEntryAdded:
verifyPriorEntriesIfRequired()— when receiving agent hasrequirePeerVerification=true, reconstructs envelopes from stored fields and verifies each speaker's signature against their public keyDefense-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:
maxToolsInContextfield (default: 20) toLlmConfiguration.Taskfor LAZY tool loadingPreviously: Hardcoded
int maxToolsInContext = 20inAgentOrchestratorFiles:
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 fieldsUpdated
GroupConversationServiceTest— addedNonceCacheServiceconstructor parameterAll 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=trueaccepted silently but noConversationForkServiceexistsOriginal fix: Added
validateSessionFlags()inRestAgentStoreto reject the flag with a clear errorFinal state: Both
forkingEnabledandmaxForksPerConversationconfig 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:
signMcpInvocationsflag accepted silently but no MCP signing implementation existsOriginal fix: Split
validateSecurityFlags()to rejectsignMcpInvocationswhile allowingsignInterAgentMessagesandrequirePeerVerificationFinal state:
signMcpInvocationsfield was fully removed fromSecurityConfig. 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, addedToolLoadingStrategyenum (EAGER/LAZY) toLlmConfiguration.Task, wired LAZY branch intoAgentOrchestrator.collectEnabledTools()— when LAZY, onlydiscover_toolsmeta-tool is sent initially, LLM discovers available tools, specs injected mid-loopFiles:
DiscoverToolsTool.java(recovered),LlmConfiguration.java,AgentOrchestrator.java
Item 4: Cryptographic Infrastructure — Recovered + Wired
Problem:
SignedEnvelope,JacksonCanonicalizer,NonceCacheServicedeleted as dead code (commit4a717fa5)Fix: Recovered all 3 files + tests, re-added
signEnvelope()/verifyEnvelope()/rotateKey()/generateKeyPairVersioned()toAgentSigningService, upgradedGroupConversationServicesigning from simple string signing to fullSignedEnvelopewith nonce-based replay protectionFiles:
SignedEnvelope.java,JacksonCanonicalizer.java,NonceCacheService.java(all recovered),AgentSigningService.java,GroupConversationService.java
Item 5: Tenant Quota DB Persistence — Dual-Backend Stores
Problem:
ITenantQuotaStoreonly hadInMemoryTenantQuotaStore— restarts reset all quota counters, no cross-instance synchronizationFix: Created
MongoTenantQuotaStore(usesfindAndModifyfor atomicity) andPostgresTenantQuotaStore(usesUPDATE...WHERE...RETURNING), wired intoDataStoreProducersfollowing existing dual-backend patternFiles:
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-levelmessageevents, assumingapp_mentionhandles them. But Slack never firesapp_mentionin DMs — onlymessageevents withchannel_type: "im". DMs were silently dropped.Two-part fix:
SlackEventHandlernow detectschannel_type: "im"and lets DM messages through the filterChannelTargetRouter.resolveDefaultForDm()added — DM channels use dynamicD-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_STYLESincludes all 5 styles)Start message format changed to lowercase
Synthesis uses header+thread pattern (2
postMessagecalls)
Rewrote SlackGroupDiscussionListenerTest to match current behavior.
New Test Coverage (24 new tests)
SlackWebApiClientTest— 19 new tests forconvertMarkdownToSlackMrkdwnSlackGroupDiscussionListenerTest— 5 new tests: all styles, header+thread synthesis, start message format
Documentation Overhaul
slack-integration.md— Major rewrite:ChannelIntegrationConfigurationas primary config model, DM support section, unified header+thread UX, trigger keywords, Markdown→mrkdwn conversion, fixed component names, DM troubleshootinggroup-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:...}inChannelTargetRouter.deepCopyConfig()JavadocM6: Added SPDX headers to
IRestChannelIntegrationStore,RestChannelIntegrationStore(missed in first pass)L3: Applied
LogSanitizer.sanitize()to all Slack-sourced log parameters inSlackEventHandler(CodeQL compliance)L4:
ChannelTarget.getTriggers()now returns a defensive copy (consistent withgetTargets()/getPlatformConfig())L5: Added null guard to
postMessageChunked()to prevent NPE on null textL6: Added
ObserveConfigbounds 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 andThreadLocalare a known Loom footgun — carrier thread reuse can leak stale values. Replaced with explicitbotTokenparameter passing throughpostMessage(),postMessageChunked(), andpostHelp(). All callers now passbotToken(ornullfor router fallback) directly.C2 — Intent key format change documented: The conversation mapping intent key changed from
slack:<channelId>:<threadKey>tochannel: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 —
eddivault→vaultJavadoc: Updated stale${eddivault:key-name}reference inChannelIntegrationConfigurationto${vault:key-name}(prefix was renamed on main in1b884109).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.0headers 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 byChannelTargetRouter).
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
selfvisibility upgrade — When consolidating entries from multiple agents (preserveAgentProvenance=false), self-scoped visibility is upgraded toglobalso no agent loses its memoriesGroupIds preserved — Consolidated entries now inherit the union of all groupIds from originals, fixing group-scoped entries becoming unreachable after consolidation
summarizeTargetEntriesvalidation — Setter now rejects<1(was silently accepting0, 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 metrics —
entriesSummarizedcounter 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 fixed —
Collectors.groupingBynow uses null-safe lambda defaulting to "fact" (legacy Mongo entries may have null category)LLM output guardrails —
parseConsolidatedEntriesnow rejects blank keys/values and truncates toMAX_KEY_LENGTH=100/MAX_VALUE_LENGTH=1000(matches UserMemoryConfig guardrails)
Low Severity (1 — log level)
SummarizationService log level — Changed
warnf→errorfin both exception handlers (RuntimeException + checked) per coding guidelines
New Tests (11 added: 51 DreamService total)
summarize_multiAgentSelfScope_upgradesVisibility— visibility upgrade to globalsummarize_preservesGroupIds— merged groupIds on consolidated entriessummarize_nullCategory_defaultsToFact— null-safe groupingparseConsolidatedEntries_blankKeyFiltered— blank key rejectionparseConsolidatedEntries_longKeyTruncated— key length guardrailtruncate_shortString_unchanged,truncate_longString_truncated,truncate_null_returnsNull— truncate utilitysummarize_partialInsertFails_rollsBack— rollback on partial insert failuresetSummarizeTargetEntries_rejectsZero,setSummarizeTargetEntries_rejectsNegative— config validation
Verification
./mvnw clean test -Dtest=DreamServiceTest,ConversationSummarizerTest,SummarizationServiceTest→ 71 tests, 0 failuresJaCoCo: 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 eliminated —
process()was callinggetAllEntries()three times when pruning + contradiction + summarization were all enabled. Hoisted the post-prune reload so it's shared (contradiction detection is read-only)maxCostPerRundefault aligned — Java default changed from$5.00to$0.50to matchuser-memory.mdandscheduling.mddocumentation. Prevents a 10× cost surprise for operatorsscheduling.mdcontradiction claim fixed — Changed "Identifies and resolves" to "Identifies and logs for review"
Should-Fix (5)
Cost estimator input undercount fixed —
estimateCost()now takesinputContentLengthparameter and estimates from input+output chars when providers don't report tokens (was output-only, underestimating by 5-10×)Dead exception catch block fixed —
SummarizationService.summarizeWithUsage()now re-throws exceptions (was swallowing them, makingDreamService's catch block unreachable).summarize()wrapper retains swallow-and-return-empty behavior for backward compat withConversationSummarizercontradictionResolutionfield 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)
buildEntriesJsonnow uses injected ObjectMapper — Replaced hand-rolledStringBuilderJSON withobjectMapper.writerWithDefaultPrettyPrinter(), keeping manual fallback for resilienceStale Javadoc fixed —
SummarizationServiceclass doc: "future Dream consolidation" → "Dream memory consolidation"enableSummarization()test helper — Now also setsmaxCostPerRunto explicit value for clarity
New Tests (6 added: 40 DreamService + 8 SummarizationService)
estimateCost_withTokenUsage— token-based cost calculationestimateCost_withoutTokenUsage_fallsBackToCharEstimate— input+output char fallbacksummarize_costCeilingReached_stopsEarly— loop stops at cost ceilingsummarizeWithUsage_llmError_propagatesException— verifies re-throw (vssummarize()which swallows)summarizeWithUsage_returnsTokenCounts— token usage extraction from LLM responsesummarizeWithUsage_checkedExceptionWrappedInRuntime— checked exception wrapping
Verification
./mvnw clean test -Dtest=DreamServiceTest,ConversationSummarizerTest,SummarizationServiceTest→ 60 tests, 0 failuresJaCoCo 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=falseare unaffected
DreamService
Added
SummarizationServiceas constructor dependency (CDI injection)Added
entriesSummarizedCountermetricRefactored
process()to reload entries only after pruning (contradiction detection is read-only)Implemented
summarizeInteractions()with insert-before-delete safety patternLLM call wrapped in try-catch — failure skips the group, does not kill the dream cycle
escapeJson()now uses Jackson'sJsonStringEncoderfor complete RFC 8259 complianceHelpers:
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
summarizeTargetEntriesInsert 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 signature12 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, addeddream.entries.summarizedmetricdocs/scheduling.md— Dream config example updated with new fieldsHANDOFF.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
RestAttachmentUpload —
tenantIdquery param now sanitized (regex: alphanumeric + dash/underscore, max 64 chars). Invalid values silently discarded to null. Security note documents trust boundary.RestAttachmentUpload —
CompletableFuture.runAsync()now uses injectedManagedExecutor(matchesBaseRuntimepattern) instead of defaultForkJoinPool. Preserves request context (security, MDC).MultimodalMessageEnhancer — Added
MAX_MULTIMODAL_FORWARD_BYTES(10MB) guard on STORED image attachments. Files exceeding this limit get aTextContentplaceholder 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
AgentSigningService —
generateKeyPair()now evictsprivateKeyCacheentry 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 / GridFsAttachmentStore —
resolvedMimenow usesMimeValidator.normalize()(strip;params, trim, lowercase) before persisting. Prevents non-canonical values likeimage/png; charset=utf-8in the database.
Observability
RestAttachmentUpload — All upload log messages now include
conversationIdfor 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 forManagedExecutorconstructor. AddedshouldRejectInvalidTenantIdtest (SQL injection → sanitized to null).AgentSigningServiceTest— AddedgenerateKeyPair_evictsCacheOnRegeneration(sign-verify roundtrip proves new key is in use after re-gen).MultimodalMessageEnhancerExtendedTest— AddedoversizedStoredImageProducesTextFallback(10MB+1 byte → text placeholder).ToolResponseTruncatorExtendedTest— AddedtestPaginateCeilingFallback(500K+1 chars → truncation, store never called).MimeValidatorTest— Added 7NormalizeTests(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
GroupConversationService —
sign()was called withgc.getUserId()but private keys are stored under tenant ID. Fixed to usedefaultTenantId(fromeddi.tenant.default-idconfig property), matching theAuditLedgerServicepattern.RestAgentStore —
validateSecurityFlags()only checkedidentity.publicKeybut ignoredidentity.keyslist. Key-rotated configs were incorrectly rejected. Now accepts either legacy key or rotated keys list.ToolResponseTruncator —
SUMMARY_HEADERprepended to summary could push total output pastmaxChars. Guard 5 now checkssummary.length() + header.length() > maxChars.
Architecture Compliance
RestAttachmentUpload — All 3 endpoints (
upload,list,delete) converted from synchronousResponsetoAsyncResponsewithCompletableFuture.runAsync().RestAttachmentUpload — Added early file size guard (
Files.size()beforereadAllBytes) to prevent OOM. Configurable viaeddi.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 forAsyncResponsepattern withCountDownLatch-based capture helper. Added test for OOM size guard.GroupConversationServiceTest— Constructor calls updated for newdefaultTenantIdparameter.
🧠 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:
SummarizationServiceonly passesmodelNametoChatModelRegistry— no API key. This works forConversationSummarizeronly 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 includeapiKey,baseUrl, etc.) and callsChatModelRegistry.getOrCreate()directly. OnlymodelNameis overridden withsummarizerModel. This inherits the full provider context automatically.
Changes
ToolResponseTruncator.java— InjectedChatModelRegistry. ImplementedsummarizeResponse()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 totruncate. Response prefixed with[SUMMARY — original: N chars, tool: name]header.AgentOrchestrator.java— UpdatedtruncateIfNeeded()call to passtask.getType()andtask.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 callingDeepCopyUtil.deepCopy(), thenMemoryCheckpoint.create()deep-copied again — wasting CPU on every checkpointFix:
extractProperties()now returns a shallowLinkedHashMapcopy;MemoryCheckpoint.create()handles the single deep-copy viacopyProperties()
Bug Fix: Property Scope Loss on Rollback (Finding 4)
MemoryCheckpoint.propertiesCopywasMap<String, Object>(flattened values) — scope, visibility, and type metadata were stripped at checkpoint timerestoreProperties()reconstructed all properties with hardcodedScope.conversation, losinglongTerm/step/secretscopeFix: Changed
propertiesCopytoMap<String, Property>, which preserves the fullPropertyobject (scope, visibility, all value types).copyProperties()clones eachPropertyvia its all-args constructor.restoreProperties()now simply puts back the originalPropertyobjects
Dead Code Removed (Finding 2)
AgentSigningService— RemovedgenerateKeyPairVersioned()+vaultKeyNameVersioned()(38 lines). Only caller was deletedrotateKey(). Tests removed tooAgentSigningServiceTest— Removed 2 dead test methods exercising the deleted methods
Minor Cleanup (Finding 3)
DeepCopyUtil— Removed redundantimport java.util.Collections(already covered byimport java.util.*)
Test Improvements
MemoryCheckpointTest— Added 3 new tests: scope preservation, visibility preservation, deep-copy mutation isolationMemorySnapshotServiceTest— Updated rollback test to assert scope preservation (longTermproperties 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 —@ApplicationScopedbut never injected.MultimodalMessageEnhancerhandles the actual attachment→Content conversionNonceCacheService+ test — Caffeine-based replay protection, never injected by any endpointSignedEnvelope+ test — Envelope signing record, never used (basicsign()/verify()onAgentSigningServiceis the live API)JacksonCanonicalizer+ test — RFC 8785 canonicalization, only consumer was deadSignedEnvelopeDiscoverToolsTool+ test — Meta-tool for lazy tool loading, never instantiated byAgentOrchestrator
Dead Code Removed (from live files)
AgentSigningService— RemovedsignEnvelope(),verifyEnvelope(),rotateKey()(never called)LlmConfiguration— RemovedToolLoadingStrategyinner class + field + getter/setter (never read by any pipeline component)AgentSigningServiceTest— Removed 5 tests for deleted methods
Bug Fix
DeepCopyUtil.deepCopy()— Wrapped return value inCollections.unmodifiableMap().MemoryCheckpointproperties are contractually immutable; the test correctly asserted this but the implementation returned a mutableLinkedHashMapDeepCopyUtil.java— Was present in working tree but never committed to Git. Now tracked
Documentation
architecture.md— Replaced deletedAttachmentForwarderreference withMultimodalMessageEnhancerToolResponseTruncator— 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, immutabilityDeploymentContextConditionTest— 4 new edge case tests:setConditionsno-op,setContainingRuleSetno-op, uninitialized getConfigs, blankwhen
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— Addednull, nullforMemorySnapshotServiceandIAttachmentStoreparams.AgentOrchestratorTest— AddednullforMemorySnapshotServiceparam.MultimodalMessageEnhancerTest/MultimodalMessageEnhancerExtendedTest— AddednullforIAttachmentStoreparam.GroupConversationServiceTest— Addednull, nullforAgentSigningServiceandIAgentStoreparams at both constructor sites.RestAttachmentUploadTest— Complete rewrite fromIAttachmentStorage/Instance<>pattern to newIAttachmentStore-based API. Now tests upload (success, rejection, tenant ID, MIME defaulting), list, and delete endpoints (10 tests).
Production Code Fixes
RestAttachmentUpload.java— Fixedattachment.fileName()→attachment.filename()to matchAttachmentrecord field name.MultimodalMessageEnhancerExtendedTest— UpdatedstoredImageProducesTextFallbackassertion 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)tocanonicalizeObject(Object)to eliminate overload dispatch ambiguity (CodeQL finding).DiscoverToolsTool: Replaced partial
String.replace()escaping with fullescapeJson()utility for all interpolated fields (name, description). Prevents invalid JSON from tool names containing backslashes/newlines.FetchToolResponsePageTool: Applied
escapeJson()toerrorandtoolNamefields (previously unescaped).DeploymentContextCondition:
setConfigs(null)now explicitly clearswhenandtagMatchesfields 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 preventfindBySkillAndAttributes()from double-counting strategy metrics viafindBySkill("all").
Javadoc Accuracy
LlmConfiguration.summarizerModel: Removed phantom claim about
eddi.mcp.summarizer.modelconfig-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-existentagentNamefield fromidentityMaskingexamples and parameter table (field was never implemented inIdentityMaskingConfig).langchain.md: Corrected placement values fromappend/prependtosuffix/prefixto match the actualCounterweightServicecode. Corrected default fromappendtosuffix.langchain.md: Added explicitenabled: trueto counterweight JSON example and added usability notes explaining that bothenabled: trueAND a non-normallevel (or at least one rule for masking) are required for activation.langchain.md: Addedcounterweight.enabledrow to parameter table (was missing), fixedcustomInstructionstype fromstringtostring[].
Migration Note
identityMaskingwas moved fromAgentConfiguration(agent-level) toLlmConfiguration.Task(task-level). Old agent configs withidentityMaskingat the agent level will have this field silently ignored (JacksonFAIL_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:
IAttachmentStore→PostgresAttachmentStoreuses correctengine.attachmentsimportIConversationCheckpointStore→PostgresConversationCheckpointStorehas full CRUD + pruneIPromptSnippetStore→AbstractResourceStoreviaPostgresResourceStorageFactory(no Postgres-specific snippet store needed)ISecretPersistence→PostgresSecretPersistence(forAgentSigningServicekey storage)DataStoreProducerscorrectly wires all stores for both backendsJackson
SerializationCustomizerapplies to both backends (sharedObjectMapper)
📊 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
isCompatibleedge 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 newengine.attachmentspackage 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:
CounterweightServicenow injectsPromptSnippetServiceand 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:
IdentityMaskingConfigwas onAgentConfigurationand smuggled throughIConversationMemoryvia bespoke getter/setter. This mixed configuration passthrough with conversational state.After:
IdentityMaskingConfigclass moved toLlmConfigurationalongsideCounterweightConfig. Config read fromtask.getIdentityMasking()— consistent withtask.getCounterweight().Impact: Removed 2 methods from
IConversationMemory, 1 transient field fromConversationMemory, wiring fromAgent.javaandAgentStoreClientLibrary. 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:
IAttachmentStoreandMimeValidatorinengine.memorypackage despite having nothing to do with conversation memory.After: Moved to new
ai.labs.eddi.engine.attachmentspackage. 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 withforSigning()/withSignature()factories andcanonicalForm()for deterministic signing.NonceCacheService— Caffeine-backed replay protection: freshness (5min default), clock-skew (30s), and duplicate detection with Micrometer counters.AgentPublicKey— Versioned key record withisValidAt(epochMs),createCurrent(), andwithExpiry()for rotation windows.
Modified Components
AgentIdentity— AddedList<AgentPublicKey> keyswithgetKeyForVersion(int)andgetKeyValidAt(long)for multi-key rotation.AgentSigningService— AddedsignEnvelope(),verifyEnvelope(),rotateKey(),generateKeyPairVersioned(). Versioned vault keys stored asagent-signing-key:{id}:v{n}.
Design Decisions
Pure Jackson canonicalization — No JCS library dep. Uses
TreeMap+ recursivesortKeys()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 flag —
eddi.a2a.signing.enabled(default false) guards all signing at call sites.
Tests (30 new)
JacksonCanonicalizerTest— 11 tests: key sorting, data types, determinism, error handlingSignedEnvelopeTest— 5 tests: forSigning, withSignature, canonicalFormNonceCacheServiceTest— 7 tests: freshness, clock skew, replay detectionAgentPublicKeyTest— 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 langchain4jImageContent(images) orTextContentmarkers (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 casesAttachmentForwarderTest— 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). Supportscreate()factory andwithParent()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.SessionManagementconfig — Inner class inAgentConfigurationwithAutoSnapshot,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 correctPropertyconstructor (String, Map, List, Integer, Float, Boolean).JBoss Logger debugf ambiguity — Cast numeric args to
(Object)to resolve overloaded method ambiguity withint/longparameter 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/equalityMemorySnapshotServiceTest— 10 tests: create/rollback/CRUD/null-safety/metricsSessionManagementTest— 5 tests: defaults/AutoSnapshot/getters/integration
Files Modified
AgentConfiguration.java— AddedSessionManagementfield and inner classDataStoreProducers.java— AddedIConversationCheckpointStoreproducer
🔧 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.ToolLoadingStrategyconfig class — Controls tool presentation:eager(all upfront),lazy(only discover_tools first),dynamic(action-filtered).
Enhanced Components
ToolResponseTruncator— Now supports three strategies viatruncationStrategyconfig:truncate(default) — hard cut with original behaviorpaginate— stores pages in PaginatedResponseStore, returns first page + responseIdsummarize— routes through cheap model (summarizerModelconfig), falls back to truncate on failure or cost ceiling (>200k chars)
ToolResponseLimits— AddedtruncationStrategyandsummarizerModelfieldsAgentOrchestrator— Added FetchToolResponsePageTool as built-in tool
Design Decisions
Paginate as opt-in —
truncateremains default for backward compatibilitySummarize 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 casesFetchToolResponsePageToolTest— 7 tests: validation/expired/success/escapingDiscoverToolsToolTest— 12 tests: category/keyword/cap/edge casesToolResponseTruncatorExtendedTest— 13 tests: all strategies/fallbacks/selection
Files Modified
LlmConfiguration.java— Added ToolLoadingStrategy, enhanced ToolResponseLimitsToolResponseTruncator.java— Three strategies with fallback chainAgentOrchestrator.java— FetchToolResponsePageTool wiringLlmTask.java— Constructor updated for FetchToolResponsePageToolAgentOrchestratorTest.java— Updated for new constructor parameterLlmTaskTest.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 onEDDI_DEPLOYMENT_ENVand agent tags. Enables environment-aware routing (e.g., force cautious in production).CounterweightConfig— Inner class inLlmConfiguration.Taskfor per-task counterweight configuration (level, placement, customInstructions).IdentityMaskingConfig— Inner class inAgentConfigurationfor identity masking rules.
Modified Files
LlmTask.java— Injected both services; calls identity masking → counterweight after system prompt compilation, before message building.LlmConfiguration.java— AddedCounterweightConfiginner class and field toTask.AgentConfiguration.java— AddedIdentityMaskingConfiginner class and field.RuleDeserialization.java— RegisteredDeploymentContextConditionin condition factory.IConversationMemory.java/ConversationMemory.java— AddedgetIdentityMaskingConfig()/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 leveleddi.counterweight.strict.downgraded— counter for strict→cautious downgradeseddi.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
Fix
round_robinstrategy bug (CapabilityRegistryService.java): ReplacedCollections.shuffle()with deterministicAtomicInteger-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.Reject inert security flags (
RestAgentStore.java): Agent create/update now returns HTTP 400 ifsignInterAgentMessages,signMcpInvocations, orrequirePeerVerificationis set totrue. These cryptographic identity features are not yet implemented (Wave 6). Prevents silent misconfiguration.Public capability discovery endpoint (
RestA2AEndpoint.java):GET /.well-known/capabilities?skill=X&strategy=highest_confidence— queries registry, returns sanitized matchesGET /.well-known/capabilities/skills— lists all registered skill namesGated behind
eddi.a2a.capabilities.publicconfig property (defaultfalse)Same auth model as
/.well-known/agent.json
Audit capability selections (
CapabilityMatchCondition.java): After a successful match, emitsCAPABILITY_SELECTIONaudit event viamemory.getAuditCollector()withskill,strategy,candidateAgentIds, andselectedAgentId. Provides immutable audit trail for compliance.Missing metrics (
CapabilityRegistryService.java):eddi.capability.miss.count(tagged by skill) — counts queries with no resultseddi.capability.strategy.applied(tagged by strategy) — tracks which strategy is used
Design Decisions
No new abstractions: The existing
Capabilitymodel onAgentConfigurationis 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 referenceOutputTemplateTask: 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()]andparam()blocks are only valid at the top of a script file, not inside an expressionThe
[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
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
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
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
changelog.md
Section title eddivar → vars; resolution order eddivar/eddivault → vars/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.7in its source POM (even onmain/ 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
postgresqlviaquarkus-jdbc-postgresqlat 42.7.10 — also vulnerable.Neither upstream has released a fix. The
<dependencyManagement>override is the correct remediation.
Files
pom.xml— Added<dependencyManagement>override fororg.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
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.Added sanity guards in digest steps — both daily and weekly digest steps detect when
delta == current(baseline was 0) and skip the notification.Views/clones delta tracking — added
day_views,day_clones,week_views,week_clonesbaselines 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).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).
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 importtoReferenceString()now outputs the new canonical form${vault:...}
Files Changed
Vault core
SecretReference.java
Dual-pattern regex, new canonical output
Vault sanitization
SecretScrubber.java, SecretRedactionFilter.java
Dual-prefix detection
Variable resolver
GlobalVariableResolver.java
EDDIVAR_PATTERN → VARS_PATTERN
Callsites
ApiCallExecutor, ChatModelRegistry, A2AToolProviderManager, VaultStartupBanner
Dual-prefix checks, new log messages
Import
AbstractBackupService, RestImportService
Auto-migrate eddivault → vault 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
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:
LlmTask —
{{vars.<key>}}template injection +${vars:...}intypefield (provider late-binding)ChatModelRegistry —
resolveAllbeforeresolveSecrets, registers invalidation listenerApiCallExecutor — URL, body, headers, query params
McpToolProviderManager — API key/URL resolution
A2AToolProviderManager — API key/URL resolution
EmbeddingModelFactory — config params before model creation
EmbeddingStoreFactory — config params before store creation
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
Runnablecallbacks. When variables change, all cached model instances are evicted so agents pick up new config on next request.exportableflag: Variables markedexportable: falseare excluded from agent exports (e.g., environment-specific URLs).
Bug Fixes
BUG-1 (LlmTask):
task.getType()was used raw (unresolved) intokenCounterFactory.getEstimator()(line 288) andchatModelRegistry.getOrCreateStreaming()(line 411). HoistedresolvedTypeabove 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 anulltenantId to"default"before hitting the store, preventing potential undefined behavior.BUG-3 (MongoDB Filter Parity): Extracted
compositeId()helper and updated MongoDBget()anddelete()methods to filter by_id(consistent withupsert()) instead of by fields. AddedSorts.ascendingto MongoDBgetAll/listAllfor parity with Postgres.BUG-4 (Postgres Reserved Words): Quoted SQL reserved words
"key"and"value"across all DDL and DML inPostgresGlobalVariableStoreand updated the corresponding test matchers.
Documentation
New:
docs/global-variables.md— comprehensive public docs with architecture, syntax, REST API, use cases, and comparison tableUpdated:
AGENTS.md— addedsnippetsandvarsto both template data model tables (sections 4.2 and 5.1)
Tests (75 total)
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?