EDDI 6.2.0 is the largest release since 6.0. Its headline is the Human-in-the-Loop (HITL) framework: two independent human-approval gates — turn-level and per-tool-call — with Slack, REST and MCP approval surfaces, crash recovery, timeout policies and full audit trails. Alongside it, multimodal attachments become a complete pipeline, the multi-model cascade gets an enterprise pass, group conversations gain a real lifecycle (follow-up → continue → close), and a broad remediation campaign closes a cross-user tool-cache leak, three audit-ledger integrity bugs, as well as quota, serialization and cost-accounting defects.
The Manager UI ships Workforce — a second, agent-first workspace alongside the admin dashboard — plus HITL approvals, cascade editors, an analytics dashboard and a schedules overhaul.
🚀 What's New
🧑⚖️ Human-in-the-Loop — Two Approval Gates, Four Surfaces
EDDI can now pause an agent mid-flight and wait for a human. Two independent gates share one pause/timeout/audit machinery:
| Gate | Trigger | Pause state | Blocks |
|---|---|---|---|
| Turn-level | PAUSE_CONVERSATION action emitted by a behavior rule |
AWAITING_HUMAN |
The remaining lifecycle pipeline for that turn |
| Tool-call | hitlConfig.toolApprovals pattern match |
hitlPauseType: TOOL_CALL |
The individual tool call, before it executes |
| Group phase | requiresApproval: true on a phase |
AWAITING_APPROVAL |
Phase advance (PHASE) or each task result (TASK) |
- 🔌 Tool-call gating across all 7 tool sources —
builtin,http,mcp,a2a,dynamic,memory,recall. Per-tool-call verdicts, and the approver can amend the arguments before the call runs. - 📓 Write-ahead journal — approved tool executions are journaled before they run, with transcript replay so resume re-enters the LLM loop at the same tool index. MongoDB and PostgreSQL journal stores, TTL retention, GDPR erasure.
- 💬 Slack approval surface — approval buttons with tool-pause detail blocks, an interactivity endpoint bound to the owning integration, and continuation pushed back into the conversation.
- 🤖 MCP HITL tools —
list/status/resume/cancelfor both regular conversations and groups, aneddi.mcp.hitl.mutations.enabledkill-switch, structured error codes, andresume_conversationnamed in thePAUSED_FOR_APPROVALpayload for client chaining. - 📥 Cross-group approval inbox —
GET /groups/pending-approvals, owner-filtered and bounded. - ⏱️ Timeout & no-progress policies —
WAIT/AUTO_APPROVE/AUTO_REJECT/ABORT, scoped per task with an agent-level default, armed as one-shot schedules and honored across pods. - 🔁 Crash recovery — a startup observer repairs paused conversations after an unclean shutdown, asynchronously so it never stalls boot.
- 🛡️ Save-time linting — reserved-action near-misses (
PAUSE_CONVERSATON) and inerthitlConfigblocks are flagged at deploy, not discovered in production.
Full reference: docs/hitl.md.
📎 Multimodal Attachments
- 🧩 One unified forwarder turns attachments into model content. Bytes resolve from a stored blob, a URL or base64 under uniform per-file (10 MB) and aggregate (20 MB) caps across all sources, then gate on the target model's real capabilities.
- 📄 Hybrid PDF — native document content when the model supports it, PDFBox text extraction inlined when it doesn't. Text-like files (
text/*, JSON, XML, CSV, YAML) always inline, no capability required. - 🗄️ Deletion actually deletes — conversation delete and GDPR erasure previously cascaded through a different store than uploads wrote to, so uploaded blobs were never removed.
- 🔑 Grants + owner-or-grant authorization — unguessable UUID storage refs, server-validated metadata reads, single-item download and delete, and cross-conversation grants that let group members read a shared blob without opening general access.
- 🔁 A file stays usable for the whole conversation, not just the turn it arrives on. The document itself is inlined only on the upload turn — re-sending it every turn would burn the context window — but the model keeps a way back to it.
readAttachmentis installed whenever the conversation has files from any turn, and when the current turn carries none the model gets a one-line note naming the earlier files and pointing at the tool. Without that note the tool is present but never looked at. - 🔓
readAttachmentdoes not requireenableBuiltInTools. That switch gates outbound, billable capabilities — web search, scraping, HTTP. Reading a blob already stored under this conversation and authorized by ownership is neither, and gating it there meant the tool was never registered for any agent the wizards create (the flag defaults tofalse). An explicitbuiltInToolsWhitelistis still honoured. - 👥 Group parity — attachments fan out to members and stay reachable across later phases, follow-up rounds and per-member follow-ups. Attachments and content-type behavior rules also survive a HITL pause/resume, which re-enters the same step from a reloaded conversation.
- 📖 Text extraction cap aligned to the tool-response limit — 50,000 characters (
eddi.attachments.text-extraction.max-chars), up from 10,000. The old cap clamped a document to roughly three pages before the layer that actually guards the context window got a say, so a follow-up question saw far less of a PDF than the upload turn did. Larger documents stay reachable throughreadAttachment's page parameter and the truncator's pagination cursors. - 🧯 Never silent — every cap hit, store failure or capability gate is recorded to
attachments:errorsand leaves a note the model can relay to the user. - 📊 Forwarder metrics, GDPR portability metadata, and per-conversation quotas (50 attachments / 100 MB, configurable).
🎚️ Multi-Model Cascade — Enterprise Hardening
- ⚖️ The audit records the real model —
audit:cascade_modelis written from the cascade-selected step (provider/model (step N)), not the task default, so an auditor can reconstruct which model produced an answer.audit:cascade_costandaudit:cascade_token_usageadded. - 🧑⚖️
judgeModel— a real config block, built through the model registry with vault and global-variable resolution. - 📡 SSE cascade events —
cascade_step_startandcascade_escalationare live end-to-end. - 💰 Ceilings —
maxTotalDurationMs(wall-clock) andmaxCostPerRun(dollars, from per-stepinputPricePer1M/outputPricePer1M) stop escalation and return the best response so far. - 📈 Metrics under
eddi.llm.cascade.*— executions, escalations by reason, accepted step, step latency, confidence distribution, tokens, cost, ceiling exceeded. - 🌊 The final step streams live token-by-token instead of buffering, and is no longer cancelled mid-stream.
- 🌍 Config-driven heuristics — confidence phrases and thresholds are configurable, and the no-match fallback is language-agnostic.
- ✅ Configure-time validation hard-fails only on invalid pricing/ceiling values; legacy conditions warn at deploy, so upgrading can never stop a previously-loading agent.
💬 Group Conversations — A Real Lifecycle
discuss → COMPLETED → [followup | continue]* → close → CLOSED
- 🎯 Follow up with any member — ask one agent (or the moderator) a question; both sides append to the transcript as
FOLLOW_UPentries. Accepts an agent ID or a display name, case-insensitive, with available members listed on a miss. - 🔄 Continue the full group — re-runs all phases with a new question; agents retain memory from prior rounds through reused private conversations, and a
roundcounter increments. - 🔒 Explicit close — ends member conversations, cleans up ephemeral agents, and locks the conversation permanently.
- 🧭
availableActionsis computed into every response andmemberDisplayNamesmaps agent IDs to names, so clients discover what they can do without transcript scanning. - 🚦 Honest HTTP statuses — follow-up and continue failures are split by cause; a mid-round member timeout is a 504, not a 502.
⚡ Error Handling & Recovery
- 🔁 Shared retry configuration — exponential backoff and retryable-error classification, configurable per subsystem.
- 🧮 Pipeline error classification — failure audit entries,
task_failedSSE events, and metrics tagged byerror.type. - 🩺 Admin state reset —
PATCH /{conversationId}/staterecovers stuck conversations. - 🔌 MCP
continueOnError+ retry + circuit breaker, config-driven per MCP call. - ✅ LLM response validation — config-driven policies for empty, truncated and filtered responses. Zero-token streaming failures are retried, partial responses are returned with metadata, and an interrupted stream now counts as a failure instead of a success.
- 📥 HTTP 4xx/5xx response bodies are stored in memory so an agent can react to them.
🧰 Tool Cost, Context & Cache Controls
- 🧱
maxToolContextTokens(default 60,000) — the in-turn tool context finally has a ceiling. The oldest tool exchanges are dropped, with a log line naming the knob to raise. - 💸
maxBudgetPerConversationcan bind — built-in tools are priced and rate-limited by their canonical slug, so a configured ceiling is no longer inert. A ceiling that cannot take effect now warns. - ⏳ Cache entries expire — per-entry TTLs are honoured; previously the lifespan argument was discarded and size eviction was the only mechanism.
- 🧹 Removed config that never did anything:
enableParallelExecution/parallelExecutionTimeoutMs, RAGinjectionStrategy/contextTemplate, andeddi.audit.retentionDays.
🔌 MCP Server — 77 Tools
Up from 65. New: 9 HITL tools and the group follow-up / continue / close tools. The memory and GDPR tools are now reachable — a tool missing from the server whitelist is dead code, and CI now auto-discovers every tool and fails the build on any omission.
🖥️ New Entry Points
/ smart-redirects based on a stored preference, /welcome is a workspace chooser, and /workforce serves the new agent-first workspace.
📡 Observability
- 💓 SSE heartbeat on the log-stream endpoint (15s comment events with idle tracking) so proxies don't drop idle log tails.
- 🧾 Log events serialize as JSON, and printf-style
%s/%dtemplates are resolved instead of being emitted raw. - 🔁 Fixed an equality bug in deployment tracking that caused a 10-second re-deployment loop.
- 📊 New meter
eddi_tool_cache_bypassed_total{tool}.
⚠️ Breaking Changes & Upgrade Notes
| Change | Impact |
|---|---|
🕐 REST: every java.time.Instant changes from a number to an ISO-8601 string |
On every endpoint. 1719964800.123 becomes "2026-07-02T12:00:00.123Z". Any external consumer of the REST API must be updated. The old encoding was fractional epoch seconds while clients call new Date(value), which expects millis — so nextFire, lastFired, pausedAt, createdAt, updatedAt and transcript timestamps all rendered as 1970 dates. Persistence stays numeric (a separate mapper), so on-disk formats and server-side sorts are untouched, and stored numeric values still parse. java.util.Date deliberately still emits epoch millis — the wire carries two encodings by design. Manager 6.2.0 ships the companion fix. |
🗑️ REST: DELETE /administration/orphans defaults changed |
includeDeleted now defaults to false (was true), so the bare call purges live-but-unreferenced resources only. includeDeleted also changed meaning: it was an equality filter, so true matched only soft-deleted descriptors instead of adding them to the live ones — the shipped Manager scanned with false and purged with true, making the set shown and the set deleted disjoint. true now means live and soft-deleted. Clients relying on the old default now purge less; pass includeDeleted=true to also purge soft-deleted resources. Purge additionally refuses to run on an incomplete scan rather than deleting from a truncated page walk. |
| 🔒 Tool-result cache is scoped per identity | Closes a cross-user data leak — see the Security section below. Cross-user tool-result sharing stops: cache hit rate will drop and outbound tool cost will rise, proportional to how much cross-user reuse the agent was silently relying on. Opt a specific tool back in with "toolCacheScopes": {"<tool>": "global"} — only where the result genuinely does not depend on the caller. No config migration needed; stored configs default to user scope. Existing cache entries are dropped on upgrade. |
📛 readDescriptors(limit = 0) |
Now means unlimited; it previously returned exactly 20 descriptors silently. |
🛡️ Security
🚨 Cross-user tool-result leak — please read
The tool-result cache keyed on toolName + ":" + arguments and nothing else — a single global namespace with no identity in it. If user A called getAccountBalance with {"account":"main"} and user B later made the byte-identical call, B was served A's cached result verbatim, with no execution and no authorization check in between. Every agent with enableToolCaching (default true) and a caller-dependent tool was affected, on every release that shipped the tool cache.
Keys now carry a scope tag: u:<hashed userId> (default), c:<conversationId>, or g (opt-in global), resolved per call from toolCacheScopes[<tool>] → defaultToolCacheScope → user. Identity handling is fail-closed — no usable user ID degrades to the narrower conversation partition, and if neither a user nor a conversation ID is available the cache is bypassed entirely rather than falling into a shared bucket. The old unscoped key methods were removed outright rather than kept as overloads, so the global key cannot be reintroduced by a future caller.
A sustained non-zero eddi_tool_cache_bypassed_total means calls are running with no identity at all and paying full tool cost every time — fix the caller rather than widening the scope. See Breaking Changes above for the cost and hit-rate impact of the upgrade.
Other security fixes
- 🔐 Conversation ownership on listing endpoints — the conversation-store listing and the MCP conversation tools had no ownership check, so any authenticated user could enumerate others' conversations. Now owner-filtered with a bounded, paged scan.
- 🕵️
read_agent_logsrequireseddi-adminfor unscoped or agent-only reads. - 🌐 Opt-in SSRF protection for httpcalls and A2A (
eddi.security.ssrf-protection.enabled, default off) — validates resolved URLs and disables redirect-following on API calls; A2A peer card fetches andtasks/sendURLs are validated. - 🔗 OpenAPI spec locations restricted to
http(s)schemes. - 🙈 Secrets are redacted from the tool trace before it reaches SSE — the live trace was streaming unredacted tool payloads to clients.
- 📄 Raw conversation-read endpoints no longer leak tool arguments and transcripts.
- ⏱️ Constant-time audit HMAC comparison.
- 🧨 ReDoS eliminated in the secret-redaction filter.
- 🧼 Log-injection remediation across HITL code paths; HITL REST error responses hardened against XSS and information exposure.
🧾 Audit Ledger & Cost Accounting
- 🔢 Real numbers, not zeros — the ledger recorded null or zero token usage, tool calls and cost. It now records actual token usage, tool-call counts and per-call tool cost.
- ➗ Stopped triple-billing the turn — LLM cost was duplicated while tool spend was dropped entirely.
- 🧬 The canonical string is now injective, versioned as v2, and recurses into nested maps and lists when canonicalizing for HMAC. Distinct payloads could previously produce identical canonical forms.
🎫 Tenancy & Quotas
- ✅
maxAgentsPerTenantis enforced on the agent deploy path — it was persisted and round-tripped through the REST API, but nothing ever read it. The count unions persisted deployment rows with live agents, closing a durable bypass whereautoDeploy=falseagents were live but uncounted. Fails open on store outage; denies with 429{"error":"quota_exceeded"}. - 🚦 429, not 500, when the API-call quota denies a turn.
- 🗃️ All quota counters live in one
tenant_usagedocument — they were previously split across documents and could disagree. - ⚖️ Deny at the limit on the PostgreSQL store (an off-by-one let one request through), with the at-limit cost comparison and
costMonthJSON shape aligned across stores.
📦 Dependency Updates
| Dependency | 6.1.2 | 6.2.0 |
|---|---|---|
| ☕ Quarkus Platform | 3.37.0 | 3.37.4 |
| 🧠 langchain4j | 1.17.0 | 1.18.0 |
| 🧠 langchain4j-libs | 1.17.0 | 1.18.0 |
| 🧪 langchain4j-beta | 1.17.0-beta27 | 1.18.0-beta28 |
| 🌐 langchain4j-community | 1.17.0-beta27 | 1.18.0-beta28 |
| 🔌 quarkus-mcp-server | 1.13.0 | 1.13.1 |
| 🐘 PostgreSQL JDBC | 42.7.11 | 42.7.13 ¹ |
| 📄 PDFBox | 3.0.7 | 3.0.8 |
| 📡 jnats | 2.25.3 | 2.26.0 |
| 📘 swagger-annotations | 2.2.50 | 2.2.52 |
| 📘 swagger-parser | 2.1.44 | 2.1.45 |
| 📊 JaCoCo | 0.8.14 | 0.8.15 |
| 🔍 Jandex | 3.5.3 | 3.6.0 |
| 🧪 Surefire / Failsafe | 3.5.5 | 3.5.6 |
¹ Covers CVE-2026-54291 (authentication downgrade, ≤42.7.11) in addition to the existing CVE-2026-42198 override.
Red Hat UBI9 OpenJDK 25 runtime bumped to the latest digest.
🐛 Other Bug Fixes
- 🧵 Model registry race — a secret rotation landing mid-build could re-cache a stale model.
timeout,logRequestsandlogResponsesare now part of a cached model's identity and are honoured when streaming; two models differing only in timeout previously shared one cache entry. - 🗑️ Orphaned conversation descriptors left behind on permanent conversation deletion.
- 🧮
convertToObjectreaches agent mode and streaming — API-level JSON mode was only wired to the legacy path. - 📉 Agent-mode token usage was discarded entirely; now accumulated across tool-loop iterations.
- 🧵 Tool execution traces are thread-safe.
- 📤 Output extraction — removed a
toString()fallback that produced garbage; extraction now handles text output items, plainString/Mapoutput andreplykeys explicitly. - ⏲️ Group agent timeout default raised 60s → 180s; empty task results,
maxTokensdefaults and verification display fixed. - 🐘 PostgreSQL
?|operator escaped in prepared statements for group conversation queries. - 📅 Schedule
Instantparsing hardened;JavaTimeModuleregistered for JSONB serialization. - 🔁 Cron
dom/dowOR semantics corrected to standard cron when both fields are restricted; API-call retry backoff is now truly exponential rather than linear. - 🌐 Channel routing generalized for multi-platform signing secrets; the coordinator dead-letter cap is configurable.
- 📇 MongoDB driver handshake metadata added — thanks @alexbevi.
- 🧊 Two cache keys that discarded the information they encoded, and a nonce cache sized from the wrong TTL.
🧪 Testing
11,000+ tests across 724 test files, gated at >90% instruction / >80% branch coverage.
🖥️ EDDI Manager 6.2.0 (Admin Dashboard)
The Manager UI ships bundled with EDDI 6.2.0 — 4,000+ tests, all strings translated across 11 locales.
🏢 Workforce — A Second Workspace
Workforce is an agent-first workspace sitting alongside the technical admin dashboard, with a root landing page that lets you pick which one you want and remembers the choice.
- 🎛️ Workspace switcher between Manager and Workforce, with full state isolation between the two.
- 💬 1:1 agent chat — streaming, markdown rendering, file attachments, HITL, history, smart auto-scroll, and an agent details panel that resolves the real LLM config from the workflow pipeline.
- 🧑🤝🧑 Group boards — wizard-driven creation, a live task board for Task Force discussions, structured plan and verification rendering, follow-up / continue / close controls, pinned groups and templates. Discussions stay visible and keep running when you navigate away and back.
- 🔄 Chat resumes the last conversation instead of minting a new one on every visit.
- 📊 Analytics dashboard — interactive filters driving all charts, KPIs and the leaderboard, plus performance and comparison sheets.
- ⚙️ Settings parity with the backend — Protocol & Resilience, Human Oversight (HITL), Dynamic Agents and Task Definitions, with the group member-timeout default raised and the previously-missing built-in tools exposed.
- 🧠 Knowledge Health card and Quick Actions surfaced above the workspace list.
- 📱 Embedded mode for the studio sidebar; fully responsive; RTL-safe throughout.
🧑⚖️ HITL Approvals
- 📥 Approvals inbox wired across the audit, channel, conversation and schedule pages.
- 🧩 Per-tool-call decisions and argument amendments, with a pause-details loading state.
- 🎚️ Tool-approval gating editors at both agent and per-task level, plus a group-level Human Approval editor.
- 🔄 Pause state is re-established and surfaced across chat send, stream and load paths.
🎚️ Cascade
- 📝 LLM config editor expanded for judge model, ceilings, pricing and heuristic configuration.
- 📡 Live escalation surfaced in chat activity and the debug trace.
🛠️ Editors, Operations & Reliability
- 📅 Schedules — one-time schedules, editing, timezone support, cron preview and a dead-letter dashboard.
nextFirehandling updated for the backend's ISO-8601 change. - 💬 Conversations — pagination, agent and state filters, active-conversation monitoring, an honest soft-vs-permanent delete choice, and an operator Attachments panel (browse / download / delete).
- 🔐 Secrets vault danger zone — rotate DEK, rotate KEK, tenant reset.
- ✅ Response Validation & Recovery editor section for the new LLM validation policies.
- 🧱 Backend field gaps filled in the MCP, API-call, property-setter and output editors; group execution strategy and condition types aligned with the backend.
- 📜 Live logs flipped to tail-style UX — newest at bottom with auto-scroll, REST seeding on first connect, SSE resilience with an inactivity timeout, plus level filter, text search and a stats bar.
- 🐞 Debug panel — pipeline waterfall with expandable task detail, cost dashboard with a per-turn table, searchable memory inspector and a role-labeled prompt viewer.
- 🧯 Confirmations and feedback on irreversible actions; the orphan-purge dialog now states plainly that all orphans are purged.
- 🎨 Light-mode sidebar with theme-aware logo and accent; onboarding with animated hero and template cards.
- ♿ Accessibility — focus traps, skip links, ARIA live regions, keyboard navigation, and
prefers-reduced-motionon all animations. - 📦 monaco-editor 0.56.0, dompurify 3.4.12, TypeScript 5.9, Playwright 1.62.
📋 Full Changelog
6.1.2...6.2.0 — see docs/changelog.md for per-change detail.