Notes | CaseHub - Engine
The Effects That Watch Themselves
The Effects That Watch Themselves
When the planner can't see ahead
The GOAP planning strategies had comprehensive unit tests — mock the CaseDefinition, mock the CaseContext, assert the planner returns the right binding. What they didn’t have was a single test exer...
GOAP in a Case Engine: What Six Integration Tests Teach About Planning Under Uncertainty
GOAP in a Case Engine: What Six Integration Tests Teach About Planning Under Uncertainty
The Authoring Surface
The Authoring Surface
The Contingency
The Contingency
The Judgment Layer
The Judgment Layer
Four Annotation Layers, One Interface
An agent that assesses aircraft defects needs to be cautious and methodical. Its inspector sign-off requires two independent approvals. Every step must produce an audit trail that satisfies EASA Pa...
When Coarse Fails, Decompose
When Coarse Fails, Decompose
Closing the Loop
Closing the Loop
The Middle Ground
The Middle Ground
When "Created At" Means "Deployed At"
CaseInstanceResponse had a createdAt field from day one. It was labelled “Case creation timestamp” in the OpenAPI schema. The value came from CaseMetaModel.getCreatedAt() — which records when the d...
When UNKNOWN Means Yes
When UNKNOWN Means Yes
The Store That Forgot to Persist
The orchestration workbench needed an SSE endpoint streaming execution state. The engine already had all the data — ExecutionStateSnapshot composing plan model, DAG plan, and DAG result into a sing...
The View That Wasn't Just a View
The issue said “no new logic — just a new view composing existing snapshots.” Six endpoints already existed on PlanResource serving decomposition trees, DAG plans, execution results, and live plan ...
Two gates before you observe
The engine already observes two compliance dimensions — latency and attestation rate — and feeds them to eidos for trust scoring. Both are straightforward: measure the clock, check the outcome. Add...
The bridge that wasn't — goal decomposition meets engine dispatch
casehub’s goal decomposition pipeline looked complete on paper. An LLM takes a
high-level goal, produces an ordered sequence of capability references, and the
engine materializes them as compound P...
The Budget That Wasn't a Number
Cost budgets looked simple at first glance. The issue asked for a costBudget field on PlanningConstraints — an integer, like resourceLimit. One number, one meaning.
When Your Routing Doesn't Route
The plan for engine#882 was straightforward: when an HTN step fails, feed the failure context back to the decomposition strategy and let it produce a revised plan. Three moving parts — ReplanContex...
When the Strategy Resolver Can't See Your Strategy
The final piece of the agent learning sub-epic landed today — goal formation. Agents can now discover new goals from accumulated experience, completing the lifecycle: goals are declared, revised ba...
When Your Timeout Doesn't Actually Time Out
The agentic planning epic has four phases. Phase 1 landed the execution bridge — blocks patterns running inside the engine’s worker boundary. Phase 2 is about giving those patterns resource awarene...
Crash Recovery for Pattern Execution
The agentic pattern infrastructure can now survive a JVM crash mid-execution. Before this, if the process died while a DEBATE or SEQUENCE pattern was running, the engine retried from scratch — ever...
Agents That Grow Their Own Goals
The landscape analysis I did in July surveyed every multi-agent project worth examining — Smallville, Emergence World, Concordia, the lot. One gap stood out from the rest: no project has agents tha...
Plan Adaptation — When Plans Meet Reality
The engine’s goal decomposition creates a plan at case start — but plans go stale the moment a worker returns with unexpected results. Plan adaptation closes that gap: after each worker completion,...
The Plan That Plans Itself
The Plan That Plans Itself
The Right Cache at the Wrong Layer
BGE-M3 runs at 4.5 seconds per entry on CPU. That’s the full ONNX forward pass — dense 1024-dim, sparse learned lexical, ColBERT multi-vector — all from one model. At 2590 garden entries, a full re...
Why Your Agent Forgets Everything
“Memory” in LLM agents has become one of those terms that means whatever the vendor needs it to mean. Most of the time, it means a chat log with summarisation bolted on. LangChain’s memory modules,...
Workspace Symlink Archaeology
The engine’s HANDOFF had work-repo issues in it. #329, #330, #298, #152, #328 — all from casehubio/work, not casehubio/engine. Branch detection was showing a work-repo branch as the active workspac...
A2A and MCP — Handler, Not Provisioner
The langchain4j parity audit flagged two gaps: we couldn’t invoke A2A agents or MCP tools as casehub workers. The obvious implementation path was WorkerProvisioner — it’s the SPI that provisions ex...
When the Spec Drifts and the Code Stays Still
The original epic (#84) was written when Stage was alive — containment hierarchies, parentStageId pointers, CMMN 5.4.4 alignment. A year later, Stage is retired. Compound replaced it. The epic’s vi...
Scope-Activated Triggers — Workers That Start When Their World Does
Most case workers are reactive. A context change satisfies a condition, the binding fires, the worker runs. This is the choreography model — bindings listen for data, and dispatch follows naturally.
Teaching the Engine Who Should Handle This
Most workflow engines assign human tasks the same way they did twenty years ago: a queue, a round-robin, maybe a role-based filter. The task appears in someone’s inbox. Whether that person is the r...
From Opaque Groups to Scored Individuals
Part of a series on #797 — HumanTask CBR routing. Previous: Teaching the Engine Who Should Handle This.
When your agent registry is already a planning domain
Part of a series on #60 — Unified execution model. Previous: Why your case needs decomposition.
Why Your Case Definition Needs a Decomposition Strategy
Part of a series on #60 — unified execution model. Previous: Delete the Duplicate, Promote the SPI.
Delete the Duplicate, Promote the SPI
Part of a series on #60 — unified execution model. Previous: Two Primitives, One Graph.
Two Primitives, One Graph
The unified execution model spec sat for eleven days after its adversarial review. Thirty-five issues raised, twenty-three verified, ten accepted. The spec was solid. The implementation plan existe...
The Shim That Wasn’t Dead
The Shim That Wasn’t Dead
The Invisible Breakage
I set out to fix four small issues on the engine — the kind of cleanup batch where you expect to be done in an hour. Two constructor mismatches from upstream SNAPSHOT changes, one checkstyle violat...
Sealing the cracks in goal composition
Sealing the cracks in goal composition
Three Models, One Vocabulary
CaseHub has three coordination models: blackboard (engine), workflow (engine-flow), and agentic patterns (blocks). They all answer the same question — how do I coordinate work across multiple agent...
Seven tasks, one session: ContextBridge implementation day
Seven of eight implementation tasks for the ContextBridge protocol landed today. The protocol was fully designed and adversarially reviewed yesterday — today was pure execution. The pipeline now ca...
Every Boundary Is the Same Boundary
CaseHub passes data through five different boundary types — worker input, work items, sub-case context, signals, connectors. Every one of them uses Map<String, Object>. Every one of them has ...
The Type That Was Three Things
Part of a series on #652 — add semantic labels/tags to CaseDefinition Java API. Previous: Two Canonicals, One Convention.
Two Canonicals, One Convention
Two Canonicals, One Convention
Six Issues, One Branch
The last session landed the universal routing strategy convention (#634) — NamedStrategy, StrategyResolver, five SPIs retrofitted. That work left a trail of follow-on issues: evaluation happening i...
One Convention, Not Seven
One Convention, Not Seven
Three Gaps, One Branch
The hybrid orchestration epic shipped last session. What it left behind were three follow-up issues — not defects exactly, more like threads that were cut short when the scope boundary was drawn.
Recovery Belongs to the Engine, Not the Scheduler
Recovery Belongs to the Engine, Not the Scheduler
The Review That Reversed the Design
The Review That Reversed the Design
Routing predicates and the art of not leaking
The composite WorkerExecutionManager (#461) solved CDI ambiguity when co-deploying multiple worker backends. But it left a question hanging: what happens when Quartz — the catch-all fallback — catc...
The Data That Disappeared
The Data That Disappeared
The Trampoline That Changed the Design
The Trampoline That Changed the Design
The Switch Statement Was the Problem
The Switch Statement Was the Problem
Where Does a Timeout Belong?
OutcomePolicy declared onExpired from the start. The YAML schema parsed it, the mapper wired it, the default constructor set it to REROUTE. But nothing in the engine ever used it. A worker that tim...
The Bug That Documented Itself Wrong
The failure cascade landed in the previous session — WorkerOutcome sealed type, OutcomePolicy, structured _outcomes state, agent exclusion. Four follow-up issues remained open. All four turned out ...
The Type Dispatch That Didn’t Belong
The Type Dispatch That Didn’t Belong
The Wrong Hypothesis and the JSON Document That Ate Every Binding
The Wrong Hypothesis and the JSON Document That Ate Every Binding
The bridge and the try/catch that lied
The foundation constraint is simple: casehub-qhorus and casehub-work are peer Foundation modules. Neither can depend on the other. But consumer apps often need both — a message arrives on a qhorus ...
The Batch That Paid for Itself
When the issue backlog accumulates fifteen XS/S items, the temptation is to knock them out one by one across separate branches. I tried the opposite — one branch, all fifteen, sequential execution.
The Registry That Ate the Scheduler's Reputation
Every symptom pointed at Quartz. Twelve tests timing out — cases start, workers never fire, ConditionTimeoutException after ten seconds of nothing. The contaminating class was CaseFaultedStateTest,...
The Qualifier Nobody Inherits
Four issues. Two were already done — one we’d fixed in the previous session and never closed the ticket, one adapted to a SNAPSHOT change without noticing the issue still existed on the board. Clos...
The Database That Wasn’t There Yesterday
The Database That Wasn’t There Yesterday
The factory that wasn't
CaseDefinitionYamlMapper had five call sites that all said the same wrong thing:
The Map Before You Dig
Four small issues in a single batch branch. Three were exactly what they looked like. One had a finding inside.
Breaking static routing in humanTask bindings
Until now a humanTask binding in a case definition had fixed routing. You’d write:
The gate that doesn't exist in the code until it fires
The idea behind ActionRiskClassifier is simple: a worker is about to do something consequential — file a SAR, freeze an account, push to production — and before the engine commits that output to th...
The Mixed-Pool Gap
The initial spec for the bootstrap guard had a gap I missed.
Wiring workflow steps to the engine
Engine#206 has been on the list for a while: when a Worker runs a Serverless Workflow, the steps inside it have no way to dispatch other casehub workers. The workflow executes in quarkus-flow’s exe...
Four issues, two architectural surprises
The tenancy enforcement batch took longer than the individual issue sizes suggested. Four issues on one branch: a migration (#411), a CDI qualifier pattern (#405), DB-level Row Level Security (#406...
What four CI failures found in the multi-tenancy test infrastructure
The PR had seven commits and clean local tests. CI disagreed — four times.
Fixes, a mystery, and three missing migrations
The S/XS backlog from the multi-tenancy session had nine open issues. I went through them all in a single branch today — eight commits, one PR. Most were straightforward. Two weren’t.
Tenancy Threading Gets Explicit
My first instinct for tenancy in the engine repositories was elegant: inject
CurrentPrincipal, read tenancyId() in each query, filter silently. Callers stay
clean. Protocol PP-20260520-e6a5f0 says ...
Hydration and Recovery: Teaching the Engine to Survive a Restart
When a Quarkus app restarts, every in-memory registry starts empty. For BlackboardRegistry, that meant any WorkItemLifecycleEvent arriving before the next CONTEXT_CHANGED — a human completing a del...
Six handlers and a miss
The batch we cleared this session was five issues — all small, all overdue,
all the kind of bugs that pass unit tests and quietly break in production.
Worker carries a definition, not an outcome
Worker carries a definition, not an outcome
Unblocking AML
The batch that closed today was supposed to be an afternoon.
Routing the Uncertain
Two routing issues had been sitting on the backlog since the AgentRoutingStrategy
SPI shipped. One was about what to do when all candidates are borderline on trust —
the four-phase model says escal...
The Wrong Abstraction
Seven null fields
REJECTED is not FAULTED
Four correctness fixes shipped today as a single PR. Three are mechanical — a PlanItem stuck in RUNNING when Quartz retries exhausted, an atomic counter race in the JPA repository, an OTel trace ID...
The Guard That Did Too Much
The signal gaps in the engine — Qhorus human messages that never reach cases,
WorkItem escalations that freeze WAITING cases, M-of-N group outcomes that vanish
— all traced to eleven lines in CaseC...
Scope and the Silent Guard
Two small fixes that close a larger gap — scope propagation for SLA preference routing and a silent when-field bug in contextChange bindings.
Waiting Is Not Running
The previous branch left the @QuarkusTest suites in casehub-blackboard
blocked — RoutingCursorStore unsatisfied dependency. The fix was
straightforward: add quarkus.arc.exclude-types=io.casehub.wor...
Clearing the Interim Address
When we moved JQEvaluator into casehub-engine-common as part of the JQ
consolidation a few days ago, we knew it was temporary. The comment in CLAUDE.md
said so explicitly: follow-on platform extrac...
The Wrapper That Earns Its Keep
Before touching any code I stopped and asked whether the casehub Agent class should exist at all. LangChain4j has its own agent model — UntypedAgent, AgenticScope, supervisor patterns. Was the case...
The Hand-Rolled Parser That Shouldn't Exist
The bug was simple: { humanApproval: { status: .decision } } in an outputMapping was producing a String literal in the case context instead of a nested Map. The fix should have been five lines. It ...
The List That Emptied Itself
The “What’s Next” table in my handover had nine items. I went to start the first one — engine#300, add the deadline field to the COMMAND message — and found the issue closed. The code was already t...
P1D Was Never Invalid
The issue description was confident: “Duration.parse() throws DateTimeParseException for any non-PT-prefixed format (e.g. P1D).” We were adding validation to CaseDefinitionYamlMapper.convertHumanTa...
The Pin Was Two Lines. The CDI Was Not.
work-adapter had a json-schema-validator pin sitting in the wrong place — it belonged in the root pom.xml alongside the other BOM overrides, not buried in the module that happened to need it first....
The Deadline Gets Through
Claudony is about to start tracking Commitments — Qhorus obligation records that bound how long an agent has to act on a COMMAND. To set an expiry, it needs the case budget deadline. The problem: c...
Giving the YAML a Human Concept
The casehub-engine-work-adapter has been implemented for weeks. HumanTaskScheduleHandler creates WorkItems. WorkItemLifecycleAdapter listens for completion and signals the engine to resume. The cod...
Testing the Handler, Not the Bus
Most of the day was cleanup — small things that had accumulated since the atomicity work. A two-arg arity mismatch when casehub-work extended SelectionContext and WorkItemCreateRequest without upda...
Atomic by Design
The bug in HumanTaskScheduleHandler was easy to describe: if WorkItem creation fails after item.markRunning(), the PlanItem ends up stuck RUNNING with no WorkItem to complete it. The engine won’t r...
Honouring the Contract
HumanTaskScheduleHandler has had a stub in template mode for a while. The comment was honest: // Template mode not yet implemented — leave PlanItem PENDING so binding stays eligible. What was less ...
SWF doesn't have a human
The previous entry covered the HumanTask binding design — the sealed
BindingTarget, the outbound HumanTaskScheduleHandler, the outputMapping
round-trip. Claude had built the implementation in a sep...
Config over CDI
The previous entry ended with a loose end: JsonPatchContextDiffStrategy still
using @Alternative while the no-op next door had just moved to @DefaultBean.
Before touching it I wanted to understand ...
Nine defaults, one wrong package
Nine SPI no-op defaults annotated with @DefaultBean to eliminate CDI ambiguity errors in consumer repos — plus a ninth bean found lurking in an import list, and a package that doesn't exist in the ...
BindingTarget and the sealed dispatch
Adding HumanTaskTarget as a sealed BindingTarget permit — replacing nullable fields with a type-safe sealed interface so every dispatch path is exhaustive and no new binding type can be introduced ...
Sub-case coordination — the race condition sequential tests miss
Implementing M-of-N sub-case coordination for multi-site clinical orchestration — the design question of when to use quarkus-flow vs a native counter, and the race condition that sequential tests c...
The half that was missing
The outbound path from engine binding to WorkItem creation was missing — tracing why WorkerProvisioner couldn't work, and designing HumanTaskScheduleHandler as the correct event-driven hook.
Optional Belongs on a Short Leash
When Optional belongs in an API — tracing Brian Goetz's original intent, identifying where casehub-engine's MapCaseFile.get() fell on the wrong side, and codifying the result as a platform rule.
Going Live — and the Two-Backup Mystery
Pushing the reconstructed engine history live surfaces a silent rebase failure and a detective story about two backup branches, a stale fork, and feature code that never made it to casehubio.
Cleaning House Before the Merge
Applying the squash policy to two live PRs collapses docs follow-on commits and surfaces a hitchhiker commit that snuck in under a different author's PR.
What Kind of Message Is This?
Adding MessageType to CaseChannelProvider.postToChannel forces a dependency graph decision: the engine takes a narrow compile dependency on casehub-qhorus-api rather than inventing a parallel vocab...
Workers can finally talk back
The CaseChannelProvider.postToChannel path existed but was never connected — WorkerContext.channels was always empty, and the buildContext() result was silently discarded before workers were schedu...
The Workaround That Wasn't
We submitted a PR to fix a null output bug in sdk-java. The maintainer closed it: it wasn't a bug.
Migration Gaps Closed
Idempotency window, DLQ replay, and SubCaseBinding close the last migration gaps, but a transitive quarkus-ledger dependency bundles JPA entities into every module that touches engine and breaks fo...
Worker registration as a speech act
WorkerRegistry becomes the single source of truth for all three worker entry paths — static, provisioned, self-registering — and Java 21 sealed classes provide the execution fork without an explici...
Connecting quarkus-work to the blackboard
CaseLifecycleEvent gets its missing worker execution call sites, and the casehub-work-adapter uses CDI choreography to route quarkus-work terminal states into PlanItem transitions via the callerRef...
Wiring the SPIs and adding lifecycle control
The Worker Provisioner SPIs get their call sites wired in, and resume adds a single CONTEXT_CHANGED publish so bindings re-evaluate immediately when a suspended case returns to RUNNING.
WorkBroker: wiring the shared SPI into casehub-engine
Wiring WorkBroker into casehub-engine reveals two undocumented quarkus-work-core behaviours — WorkerCandidate.of() silently drops all selections, and WorkItemStatus.EXPIRED.isTerminal() returns false.
casehub-resilience: conflict resolution, a timeout enforcer, and a Vert.x surprise
Building ConflictResolver and CaseTimeoutEnforcer surfaces a Vert.x event bus gotcha: publishing from a non-event-loop thread requires explicit context acquisition, or messages silently disappear.
Ecosystem Mapping, PRs F-J, ADR-0002
Gap closure via PRs F through J expands into a fifteen-issue ecosystem map, and the binding-gating decision — presence of stage.addBinding() as the opt-in — is validated as the only bounded and fai...
Closing Every Gap: Parity, Kogito, and ADR-0002
Four genuine gaps against the prior implementation — including strict PlanItem lifecycle and a CDI pre-registration SPI — are closed via PRs F-G and documented in ADR-0002.
QE Pass: 68 Tests to 99, Five PRs
A systematic comparison against the original implementation surfaces thread-safety gaps in PlanItem and Stage, missing end-to-end scenarios, and a null pointer hidden by the String constructor over...
Blackboard: Research, Analysis, and Implementation
Academic research into LLM-based blackboard architectures shapes one critical design decision — changing LoopControl.select() to return Uni — before the blackboard module ships its first implementa...
Cutting the JPA Wire
PR3 strips JPA from the engine module entirely — three domain objects become plain Java, twelve handlers route through three SPI interfaces, and no framework annotation remains in casehub-core.
The Dedup Wasn't Broken. The Test Was.
PR 2 shipped, Flyway is gone, and two CI failures that turned out to be different problems than they appeared.
Persistence PR 1: Container Chaos and a Quarkus Context Trick
PR 1 of the persistence decoupling plan exposes a Podman misconfiguration and a Quarkus Vert.x context requirement that blocks standard JUnit methods from calling reactive Panache.
Phase 2: Resilience, Diff Provenance, and a Persistence Rethink
Three new modules designed and shipped — resilience, EventLog enrichment, and a persistence decoupling spec — plus a conversation that changed the ORM approach entirely.
Phase 2: Standards, a Hidden Bug, and casehub-blackboard
Naming decisions resolved, CaseStatus aligned with CNCF standards, a silent bug found by the tests we wrote, and casehub-blackboard went from brainstorm to 390 tests in one session.
Phase 1: Into casehub-engine
The merge direction reversed before a single line of code was written. casehub-engine becomes the home; Phase 1 lays the extension points incrementally, one PR at a time.
Two CaseHubs, One Design
Discovering a parallel casehub-engine implementation and charting a 9-phase plan to unify both systems into one coherent design.
Session 3: Getting the Architecture Right
Collapsing the lineage graph, redesigning the persistence layer, and researching goal models across BDI, GOAP, CMMN, and HTN.
The Architecture Behind CaseHub: Blackboard Meets CMMN
Two patterns from very different traditions — Blackboard Architecture and CMMN — and why they belong together for agentic AI.
Wanted a Sketch, Got a Framework
One session, 73 files, 14,003 lines of code — what started as a request for a sketch became a working framework.