> For the complete documentation index, see [llms.txt](https://docs.labs.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.labs.ai/reference/changelog/2026-05.md).

# May 2026

> Archived entries for May 2026, newest first. For recent work see [the live changelog](/reference/changelog.md).

***

## 🛠️ 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 — `eddivault` → `vault` 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 metrics** — `entriesSummarized` 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 fixed** — `Collectors.groupingBy` now uses null-safe lambda defaulting to "fact" (legacy Mongo entries may have null category)
* **LLM output guardrails** — `parseConsolidatedEntries` 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 `warnf` → `errorf` 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 eliminated** — `process()` 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 fixed** — `estimateCost()` 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 fixed** — `SummarizationService.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 fixed** — `SummarizationService` 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

* **RestAttachmentUpload** — `tenantId` query 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 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

* **AgentSigningService** — `generateKeyPair()` 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 / GridFsAttachmentStore** — `resolvedMime` 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

* **GroupConversationService** — `sign()` 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.
* **RestAgentStore** — `validateSecurityFlags()` only checked `identity.publicKey` but ignored `identity.keys` list. Key-rotated configs were incorrectly rejected. Now accepts either legacy key or rotated keys list.
* **ToolResponseTruncator** — `SUMMARY_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

```json
{
  "type": "openai",
  "parameters": { "apiKey": "${vault:openai-key}", "modelName": "gpt-4o" },
  "toolResponseLimits": {
    "defaultMaxChars": 5000,
    "truncationStrategy": "summarize",
    "summarizerModel": "gpt-4o-mini"
  }
}
```

### 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:
  * `IAttachmentStore` → `PostgresAttachmentStore` uses correct `engine.attachments` import
  * `IConversationCheckpointStore` → `PostgresConversationCheckpointStore` has full CRUD + prune
  * `IPromptSnippetStore` → `AbstractResourceStore` via `PostgresResourceStorageFactory` (no Postgres-specific snippet store needed)
  * `ISecretPersistence` → `PostgresSecretPersistence` (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 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 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-in** — `truncate` 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:

```powershell
Invoke-WebRequest -UseBasicParsing -Uri "https://...install.ps1" -OutFile "install.ps1"
Unblock-File .\install.ps1
.\install.ps1
```

**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:

```
Error trying to insert context information into template. Either context is missing or reference in template is wrong!
```

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:**

```
ERROR [LlmTask] Error trying to insert context information into template. Either context is missing or reference in template is wrong!
```

**After:**

```
ERROR [LlmTask] Template processing failed for LLM parameter 'systemMessage': Template rendering failed: Rendering error in template ... | Template preview: You are a helpful assistant. The user speaks {context.language}...
```

**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 `SQLException`s 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 `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.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_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

| 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. **ChatModelRegistry** — `resolveAll` 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`

***
