Skip to content

Releases

Nexora v0.5.0

Choose a tag to compare

@github-actions github-actions released this 09 Nov 19:45
ff92b40

Added

Deployment

  • Kubernetes Helm chart and manifests - deploy/helm/nexora runs nexora observe on Kubernetes: Deployment, Service (HTTP 9464 and the dashboard's WebSocket on 9465), ConfigMap (nexora.json from config), ServiceAccount, and optionally a Secret (for inline values), PersistentVolumeClaim, HorizontalPodAutoscaler, PodDisruptionBudget and Prometheus Operator ServiceMonitor. Startup and liveness probes hit /health/live, readiness /health/ready; resources default to requests 250m/512Mi and limits 1/1Gi. Values cover the image, replicas, the API key Secret, persistence (h2 on a PVC, jdbc from a Secret, or none), the OTLP endpoint, a kubernetes.io/tls Secret for HTTPS, multi-tenancy and a users-file Secret. The pod runs as uid 10001 with a read-only root filesystem and no capabilities, and meets the restricted Pod Security standard. The chart refuses settings that would deploy but not work, such as H2 with more than one replica, jdbc without a URL, or TLS without a certificate. deploy/k8s holds the default rendering as plain YAML, generated by deploy/k8s/generate.sh (#47)
  • Container image - A Dockerfile packages nexora observe on the Temurin 21 JRE, non-root, with the heap sized to the container's memory limit. The new Container image workflow builds it for linux/amd64 and linux/arm64 on every v* tag and pushes ghcr.io/iamvirul/nexora:<version>, :<major>.<minor> and :latest (#47)
  • NEXORA_JDBC_URL - The CLI and nexora observe can use a database server: set NEXORA_JDBC_URL to a JDBC URL, credentials included. PostgreSQL 15+ (the driver now ships in the CLI jar) and H2 are accepted. The URL is read only from the environment, so it can come from a secret, and is never logged; setting it together with executionStore fails at startup (#47)

Distribution

  • Published to Maven Central - The library modules (nexora-core, nexora-plugin-spi, nexora-registry, nexora-tracing, nexora-tracing-otel, nexora-retry, nexora-event, nexora-executor, nexora-plugin-loader, nexora-planner, nexora-saga, nexora-persistence, nexora-runtime, nexora-api) are published to Maven Central with sources and javadoc JARs, all signed, so an application can depend on them with no extra repository. The Publish workflow builds, tests and uploads them as one bundle through the Central Publisher Portal on every v* tag (mvn -P release deploy); Central publishes it only if every module passes validation. README and Getting Started show the Maven and Gradle snippets. The CLI and the LLM planner plugin JAR stay on GitHub Releases (#50)

Planner

  • LLM planner plugin - New module nexora-planner-llm plans goals with an LLM through the official SDKs: Anthropic (anthropic-java, default model claude-opus-5-5), OpenAI (openai-java), Gemini (Google's google-genai, on the Gemini Developer API), or any model on OpenRouter (the OpenAI SDK pointed at OpenRouter). It sends the goal, the capabilities the tenant may use and the names (not the values) of the intent context, and asks for the plan as structured JSON output. Every answer is validated before it runs: known capabilities, unique step ids, dependencies that exist and form no cycle, context names the request carries, output fields the producing capability declares, every required input bound. An invalid answer, a refusal, a truncated answer or a failed call, including an SDK error no client recognised, logs a WARN with the reason and falls back to the rule-based planner. Valid plans are cached by sha256 of the goal, the sorted capability ids and the context names, with a TTL (default 10 minutes) and a size bound. Anthropic requests on supported models opt into server-side refusal fallback. Configured with NEXORA_LLM_* environment variables or nexora.llm.* system properties (provider, model, max tokens, effort for Anthropic, temperature for OpenAI, OpenRouter and Gemini, timeout, retries, cache TTL, base URL, priority); keys come from ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, GEMINI_API_KEY or a key file, never from system properties. With nothing configured the plugin activates without a planner. Ships as a plugin JAR (-plugin classifier, about 140 MB because of the three SDKs). The tests/llm-planner-demo sample runs a goal through both planners side by side, with --offline for a run without a key (#44)

Plugins

  • Plugin hot reload - NexoraEngine.reloadPlugin(pluginId, jar) (PluginManager.reloadPlugin) swaps an active JAR plugin for a new version without stopping the engine. The new JAR is loaded under a fresh PluginClassLoader and fully initialised before anything changes; then its capabilities and planners replace the old ones and new executions use them at once. Steps already running on the old version finish on it: every capability and planner from a JAR plugin is registered behind a ref-counted lease on its version, and the old version's shutdown() and class loader close run when the last lease is returned. If the new JAR cannot be read, declares another id, needs an inactive plugin, fails to initialise or takes another plugin's capability id, nothing is swapped, the old version keeps serving, PluginReloadFailedEvent is published and PluginInitializationException is thrown; on success PluginReloadedEvent carries the old and new versions. Reload requires an ExecutionStore (IllegalStateException otherwise) and is refused for plugins registered in code. nexora plugins reload <pluginId> --jar <path> calls the new POST /api/plugins/{pluginId}/reload route on a running observe server, which accepts only the server API key and only a .jar in the same directory as the plugin's current JAR. plugins in nexora.json lists JARs to load at startup. NexoraEngine.pluginJar(pluginId) returns the JAR a plugin's active version came from (#43)

Documentation

  • HTTP API reference on GitHub Pages - The docs site publishes the observe server's openapi.yaml at https://nexora.virul.me/api/openapi.yaml with a rendered reference at /api/, copied from nexora-cli on every site build (website/scripts/copy-openapi.mjs), so the server and the site never drift. The new HTTP API page holds the versioning rules and an API changelog. The site redeploys when openapi.yaml changes.

Changed

  • Maven group ID is now me.virul.nexora - All modules moved from com.nexora to me.virul.nexora, the namespace verified on Maven Central. Update the groupId in your build; artifact IDs and the com.nexora.* Java packages are unchanged (#50)
  • nexora-all is a BOM - nexora-all was a shaded JAR bundling every module. It is now a bill of materials pinning every published module to one version: import it (<type>pom</type><scope>import</scope>, Gradle platform(...)) and depend on nexora-api. The shaded JAR repeated classes that are also published as separate JARs (#50)
  • No more GitHub Packages - Releases no longer publish the Maven modules to GitHub Packages, which needs a token even to read; Maven Central replaces it. Versions up to 0.4.0 stay there under com.nexora (#50)
  • Docs moved to nexora.virul.me - The documentation site is served from https://nexora.virul.me. Links to iamvirul.github.io/nexora/ redirect there
  • CLI startup errors are one line - When the engine cannot start (a store that cannot be opened, both NEXORA_JDBC_URL and executionStore set, an invalid pool size), the CLI prints Error: <reason> and exits with code 2, instead of a stack trace and exit code 1. The reason keeps the root cause ("Connection refused"), with any JDBC URL cut to jdbc:<subprotocol> (#47)
  • CI validates the Helm chart - The Helm chart workflow runs on changes to deploy/** or the Dockerfile: helm lint --strict with the defaults and each ci/ value set, checks that invalid settings are refused and that deploy/k8s is regenerated, then on a kind cluster runs kubectl apply --dry-run=client and --dry-run=server for every value set and installs the chart with an image built from the commit (#47)
  • Planners can decline after accepting an intent - A planner whose plan() throws the new PlanningDeclinedException (in nexora-plugin-spi) is skipped and CompositePlanner tries the next planner, ending with the rule-based one, instead of failing the execution. Other PlanningExceptions still fail planning (#44)
  • The OpenAPI document has its own version - info.version in openapi.yaml is now the version of the API contract, separate from the Nexora release version, and changes only when the API does (it had stayed at 0.3.0 through the 0.4.0 release). CI fails when the current info.version has no section in the API changelog. The version field of GET /health is still the Nexora release.
  • Plugin planners are read live - CompositePlanner takes a Supplier<List<Planner>> (new constructor) and the engine passes the active plugins' planners through it, so plugins activated or reloaded after build() plan new intents. PluginManager.registeredPlanners() now returns an immutable snapshot that is replaced on every change. A JAR plugin's capabilities are registered wrapped, so CapabilityRegistry.find returns a wrapper rather than the plugin's own instance. A capability or plan...
Read more

Nexora v0.4.0

Choose a tag to compare

@github-actions github-actions released this 01 Nov 18:07
62de0d2

Added

Persistence

  • Schema migrations with Flyway - JdbcExecutionStore applies versioned migrations from db/nexora/migration during construction, before any query, and records them in nexora_schema_history. The location and table are namespaced so an application's own Flyway on the same database never picks them up. V1__initial_schema.sql holds the previous DDL. A failed or edited migration makes construction throw IllegalStateException naming the script. Stores created by earlier releases are baselined at version 0 and adopted in place, keeping their data. Migrations run over separate connections when the store owns its pool; a caller-supplied DataSource must allow at least two connections. Naming and authoring rules are in CONTRIBUTING.md (#29)
  • Step outputs are stored - Each completed step's output is saved with its step record and returned by ExecutionStore.findById() as StepRecord.output(), a Map<String, Object> that is empty when the step returned nothing. A map output is kept as its JSON form and any other value goes under value; java.time values become ISO-8601 strings. Migration V2__add_step_output.sql adds output_json and output_truncated to nexora_steps. Outputs larger than persistence.stepOutputMaxBytes in nexora.json (NexoraEngine.Builder.withStepOutputMaxBytes, default 64 KB) are kept as a truncated preview, flagged by StepRecord.outputTruncated() and logged at WARN, never rejected, and later steps still get the whole value. 0 stores no outputs. An output that cannot be written as JSON is logged and stored as empty, and the step still completes. GET /api/executions/{id}, the /api/process snapshot (4 KB per step) and both dashboards show the outputs (#32)
  • Connection pooling for JdbcExecutionStore - The store now borrows a connection per call from a DataSource instead of serialising every call on one synchronized connection, so persistence calls run in parallel up to the pool size. New JdbcExecutionStore(DataSource) constructor for caller-managed pools (left open on close()), JdbcExecutionStore(String, PoolConfig) and h2AutoServer(String, PoolConfig) for a store-owned HikariCP pool, and HikariCPDataSourceFactory. The CLI reads persistence.pool.maximumPoolSize, minimumIdle, connectionTimeoutMs and idleTimeoutMs from nexora.json (#28)
  • PostgreSQL integration tests - JdbcExecutionStorePostgresIT runs the store against PostgreSQL 15 in a Testcontainers container: Flyway migrations on a fresh database, execution and step CRUD, findById, findRecent, history filtering with cursor paging, webhook deliveries, dead letters, schedules, 10 concurrent writers, and racing upserts on a new step. Tests tagged @Tag("integration") run under failsafe in mvn verify and are excluded from mvn test; -DskipITs and -DskipUTs pick one side. CI runs them as a separate step limited to 3 minutes. They need Docker (#45)

Security

  • Rate limiting for the observe server - Each client gets 60 requests a minute across the authenticated /api routes and 10 a minute to POST /api/execute on top (token buckets, Bucket4j). More get 429 with {"error":{"code":"RATE_LIMIT_EXCEEDED","message":"...","retryAfterSeconds":N}} and an accurate Retry-After, are logged at WARN with client IP, method, route and limit, and counted in nexora_rate_limit_violations_total{endpoint,client_ip}. Limits apply after authentication, so unauthenticated requests get 401 without spending tokens, and never to health probes, /metrics, the dashboard or the docs. Configured under observe.rateLimit in nexora.json (enabled, globalRpm, executeRpm, trustedProxies); enabled: false applies no limit at all. X-Forwarded-For is honoured only from listed proxies, taking the rightmost untrusted hop; each listed proxy must append the peer address it saw, or a client could write that hop itself. Idle buckets are dropped and at most 100,000 clients are tracked per limit. RequestRateLimiter, RateLimitConfig and ClientAddress live in com.nexora.api.ratelimit for other embedded servers; ObserveServer.Config.withRateLimit sets the limits (#49)

  • Multi-tenancy - Every execution belongs to a tenant: Intent.withTenantId("acme") (or the new constructor), "default" when none is given, exposed to capabilities as CapabilityRequest.tenantId() and to the engine as ExecutionContext.tenantId(). Ids are 1 to 64 characters of a-z, 0-9, -, _ (TenantId). Migration V3__add_tenant_id.sql adds tenant_id (default 'default') to executions, steps, webhook deliveries, dead letters and schedules, with existing rows in default; steps and deliveries copy their execution's tenant. Dead letters, schedules and replays keep the original tenant. Tenant-scoped API: NexoraEngine.replay(tenantId, id), cancel(tenantId, id), listSchedules(tenantId), cancelSchedule(tenantId, id), listCapabilities(tenantId), ExecutionStore.findById(tenantId, id), findDeadLetters(tenantId, ...), ExecutionFilter.forTenant; another tenant's work answers like an unknown id. NexoraEngine.Builder.withTenantId binds an engine to one tenant: intents without a tenant run for it, others are refused, and its cron scheduler only runs that tenant's schedules. CapabilityDescriptor.withTenantIds restricts a capability to some tenants; others neither plan with it nor invoke it (CAPABILITY_NOT_FOUND). nexora observe --multi-tenant scopes every /api route and the live stream to one tenant taken from the credential: users are bound to a tenant (nexora user add --tenant, nexora user tenant), and a header naming another tenant gets 403 TENANT_FORBIDDEN; the server API key must send X-Tenant-ID (400 TENANT_REQUIRED), also when opening a dashboard session, which stays on that tenant. nexora cancel / replay take --tenant. Without the flag nothing changes and the header is ignored. Execution metrics carry a tenant label (nexora_active_executions is now per tenant; sum it for the total), and nexora dlq / nexora schedule take --tenant. Schedule firings are claimed in the store before they run (ExecutionStore.claimScheduleFire, a compare-and-set on next_fire_at), so engines sharing a store, bound, unbound or replicas, fire each slot exactly once. PostgreSQL row-level security for direct database readers is documented in website/docs/concepts/multi-tenancy.md (#48)

  • Authentication for the observe server - Every /api/* route and the live WebSocket stream require an API key, sent as Authorization: Bearer <key>. A missing or wrong key gets 401 with {"error":{"code":"UNAUTHORIZED","message":"..."}} and WWW-Authenticate: Bearer, before any other check, so unknown /api paths do not reveal what exists. /metrics, /health/*, the dashboard page, /openapi.yaml and /docs stay public. On first start nexora observe generates a key, prints it once and saves only its SHA-256 digest (owner-only, ~/.nexora/observe-api-key.sha256, override with --api-key-file); NEXORA_API_KEY or --api-key (at least 32 characters) supplies one instead, with nothing printed or written. --rotate-api-key replaces it. Keys are compared as SHA-256 digests with MessageDigest.isEqual, so timing reveals nothing. The dashboard trades the key once for an HttpOnly; SameSite=Strict session cookie (POST /api/session, DELETE /api/session to log out), and cookie-authenticated state changes and WebSocket handshakes must also carry the dashboard's own Origin. nexora cancel and nexora replay send the key from NEXORA_API_KEY or --api-key and refuse to send a key or password over plain HTTP to another machine unless --allow-insecure-http is passed. The payment pipeline demo uses the same auth with a key printed per run (nothing saved), and keeps its /api/mock-webhook receiver open for signed webhook deliveries. The auth classes live in nexora-api (com.nexora.api.auth: ApiAuth, ApiKeyStore, UserStore, PasswordHasher, SessionEndpoints) so any embedded HTTP server can reuse them (#30)

  • Username and password login with user API keys - nexora user add/passwd/remove/list manages dashboard accounts in an owner-only ~/.nexora/observe-users (--users-file), hashed with PBKDF2-HMAC-SHA256 at 600,000 iterations and a per-user salt; passwords are 12 to 128 characters. A correct password mints a user API key (nxu_..., 12 hours) that works on every route the server's key does (except opening a dashboard session) and is logged under the user's name. POST /api/keys returns it in the body for nexora login, which saves it to ~/.nexora/credentials for nexora cancel / nexora replay; POST /api/session gives the dashboard the same key as the HttpOnly cookie instead. GET /api/session reports who is signed in, and DELETE /api/session (nexora logout) revokes it. Removing a user or changing a password revokes their keys on the running server, with no restart. Five failures lock a username for 15 minutes and each client is limited to 20 attempts a minute, checked before any hashing; unknown usernames get the same treatment and timing, so nothing reveals whether an account exists. Login must be application/json and same-origin. The dashboard signs in with a username and password by default, with the API key as an option; the payment pipeline demo stays key-only (#30)

  • Login demo - New tests/login-demo sample runs the real observe server on localhost:9470 with demo users alice and bob, a server...

Read more

Nexora v0.3.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 17:51
e988694

Added

Core Engine

  • Execution cancellation - NexoraEngine.cancel(executionId) returns CompletableFuture<Boolean> (true if cancelled, false if already finished; fails with ExecutionNotFoundException for unknown ids). Pending steps are never started, running steps are interrupted, the execution is persisted as CANCELLED, and PlanCancelledEvent fires. Saga compensation runs for completed steps when enabled. Cancelled executions are not dead-lettered. New ExecutionStatus.CANCELLED and ExecutionState.CANCELLED (#33)

Observability

  • NexoraEngine.submit(intent) - Starts an execution and returns an ExecutionHandle (executionId + result future) immediately, so the id is known before the run finishes (#33)
  • OpenAPI specification for the observe server - A hand-maintained OpenAPI 3.1 document covering every endpoint, with schemas and success and error examples, served at GET /openapi.yaml. Swagger UI is served at GET /docs from the bundled org.webjars:swagger-ui assets under a strict Content-Security-Policy, with no CDN or validator calls. CI lints the document with Redocly CLI, and OpenApiSpecTest fails the build when a route is added without documenting it or the document describes a route the server does not serve (#46)
  • Execution replay - NexoraEngine.replay(executionId) re-runs a FAILED, TIMED_OUT, CANCELLED or COMPENSATED execution with the same goal and context under a new execution id; the original id is in the replay's context as _replayOf. Steps that completed reuse their original idempotency key so idempotent capabilities skip repeated side effects; failed steps, and every step of a compensated execution, get fresh keys. Exposed as POST /api/executions/{id}/replay (202, 404, 409 with the current state), nexora replay <executionId>, and a Replay button in the history panel. Dead letter replay now delegates to it (#37)
  • Execution history REST API - GET /api/executions lists executions newest first with keyset (cursor) pagination and status, goal, from / to, limit filters, responding {"data": [...], "cursor": "...", "total": N}. GET /api/executions/{id} returns one execution with its step records. ExecutionStore gains findByFilter(ExecutionFilter) and count(ExecutionFilter); the JDBC store builds both with bound parameters only and adds an index on (started_at, execution_id). The observe UI and the payment pipeline demo get an Execution History panel with status and goal filters, cursor-based Load more, and click-to-expand step detail (#34)
  • Cancel REST API, CLI, and UI - POST /api/execute and DLQ replay now return the new executionId. The observe UI has a Cancel button on running executions, and the payment pipeline demo adds a cancellation scenario and a "Slow payment" preset. DELETE /api/executions/{id} on the observe server (200 cancelled, 404 unknown, 409 already finished) and nexora cancel <executionId> [--server URL]. New nexora_plan_cancelled_total counter; cancels are not counted as failures (#33)
  • Health check endpoints - GET /health/live (liveness), GET /health/ready (readiness: persistence, plugins, executor; 503 with a per-check breakdown when any is DOWN), and GET /health (summary with version, checks, and capability circuit states). All three are unauthenticated. New nexora_ready gauge and NexoraNotReady alert (#31)

Changed

  • Code formatting enforced - All Java code is formatted with palantir-java-format via the Spotless Maven plugin. mvn verify fails on unformatted code; run mvn spotless:apply to fix

  • /health/ready semantics - Readiness now reflects the engine's own dependencies instead of capability circuit state. An OPEN circuit no longer returns 503; circuit states moved to the capabilities field of GET /health (#31)

  • PluginManager.loadPlugin(Path) returns the plugin id - Previously void. Source compatible; code compiled against an older nexora-plugin-loader must be recompiled (#140)

  • DagStepScheduler.schedule(Plan, ExecutionContext, AtomicBoolean) replaced by schedule(Plan, ExecutionContext, HaltSignal) - HaltSignal carries why the execution stopped (DEADLINE or CANCELLED) so the scheduler can report TIMED_OUT or CANCELLED. Deadline behaviour is unchanged (#33)

  • Default webhook events include CANCELLED - Intents without explicit webhookEvents are now notified when cancelled (#33)

Fixed

  • Step records could be persisted out of order - Step persistence ran as asynchronous event-bus handlers with no ordering, so a step's completion could be written before its start (leaving it RUNNING), and step rows could still be missing when the execution's result completed. Step persistence now uses the new ExecutionEventBus.subscribeSync, which runs inline in publish order; the step-start row, with its idempotency key, is written before the capability runs (#37)
  • Execution state write failures were silent - JdbcExecutionStore.updateExecution swallowed SQL errors, so a failed terminal write could leave a record stuck at COMPENSATING (blocking replay) or at FAILED after compensation. It now throws, and the engine retries state writes up to three times. The engine also keeps an execution tracked until saga compensation has finished, so replay and cancel never act on a half-written record (#37)
  • Step idempotency keys were erased on completion - JdbcExecutionStore.upsertStep replaced the whole row, and completion and failure updates carry no key, so the key recorded at step start was lost as soon as the step finished. Updates without a key now keep the recorded one (#37)
  • findById reported database errors as "not found" - JdbcExecutionStore.findById swallowed query failures and returned empty, so a database outage surfaced as 404 from the detail endpoint and as NOT_FOUND from cancel. It now throws IllegalStateException; both endpoints answer 500 (#34)
  • Observe server failed to serialize java.time values - nexora-cli lacked jackson-datatype-jsr310, so any response containing an Instant or Duration threw instead of responding: GET /api/dead-letters with entries, webhook delivery records, and GET /health once capabilities were registered. Instants serialize as ISO-8601 timestamps and durations as ISO-8601 durations (PT0.25S) (#34)
  • Timed-out capabilities kept running - TimeoutInterceptor used CompletableFuture.cancel(true), which does not interrupt the worker, so a capability that exceeded its timeout carried on in the background. It now runs on a FutureTask and is interrupted on timeout (#33)
  • Retries after interruption - RetryInterceptor no longer starts another attempt once the step thread has been interrupted (#33)
  • Plugin jars never activated - NexoraEngine.Builder.withPluginJar() loaded the jar but never activated the plugin, so its capabilities and planners were never registered and executions failed with CAPABILITY_NOT_FOUND. build() now activates jar plugins after inline plugins, in the order added, and fails with PluginInitializationException if activation fails (#140)

Nexora 0.2.1

Choose a tag to compare

@github-actions github-actions released this 03 Jul 04:31
5b1d3f1

Update

  • Dependency Update - Bumps ch.qos.logback:logback-classic from 1.5.35 to 1.5.37.

Nexora v0.2.0

Choose a tag to compare

@github-actions github-actions released this 30 Jun 16:48
ce7d2ce

Added

Core Engine

  • Cron Scheduling - Recurring intent executions driven by 5-field UNIX cron expressions. NexoraEngine.schedule(cron, intent) persists the schedule to a nexora_schedules table and returns a ScheduledExecution handle with id(), nextFireTime(), and cancel(). Three missed-fire policies control restart behaviour: SKIP (drop missed windows), FIRE_ONCE (fire once to catch up, default), and FIRE_ALL (fire once per missed window). ScheduledExecutionFiredEvent and ScheduledExecutionMissedEvent are published on the event bus. Requires a persistence store (#38)
  • Dead Letter Queue - Permanently failed executions (after all retries are exhausted) are now captured in a nexora_dead_letters table. Each dead letter record stores executionId, goal, context, failureCode, failureMessage, failedAt, and a reviewState (PENDING / RESOLVED / REPLAYED). An ExecutionDeadLetteredEvent fires on the event bus for external alerting (#42)
  • Conditional Branching - Allows defining a StepCondition (e.g. ContextValueEquals, StepOutputEquals) on any step. The scheduler dynamically evaluates these before step execution and skips the step if the condition resolves to false (#41)
  • Plan-level execution deadline / timeout - wall-clock deadline that cancels an entire plan execution when the limit is exceeded, returning TIMED_OUT execution status and triggering full saga compensation; overridable per Intent or globally via NexoraEngine.Builder.withDefaultPlanDeadline() (#35)

Executor

  • Capability contracts enhancements - Added openDuration and probeInterval properties to CapabilityContract to support stateful circuit breaking (#36)
  • Capability circuit breaker - CapabilityContractMonitor now implements a robust circuit breaker state machine (CLOSED, OPEN, HALF_OPEN) and routes unhealthy capabilities to their fallback transparently (#36)

Observability

  • Schedule REST API - Three new endpoints on the observability server: GET /api/schedules (list all schedules), POST /api/schedules (register a schedule with cronExpression, goal, and optional missedFirePolicy), DELETE /api/schedules/{id} (cancel immediately). The observe dashboard includes a live Cron Schedules panel with an add-form and auto-refresh every 30 seconds (#38)
  • DLQ REST API - Three new endpoints on the observability server: GET /api/dead-letters (paginated, filterable by state; defaults to PENDING), POST /api/dead-letters/{id}/replay (creates a new execution and marks the entry as REPLAYED), POST /api/dead-letters/{id}/resolve (accepts an optional reason body and marks the entry as RESOLVED). Authentication will be enforced once #30 lands (#42)
  • Circuit breaker monitoring - Added /health/ready endpoint to the observability server to monitor capabilities circuit statuses (#36)
  • Webhook Callbacks - Implemented asynchronous terminal event webhooks using Java 11 HttpClient with exponential backoff. Webhooks are signed with an HMAC-SHA256 signature and delivery attempts are persisted in nexora_webhook_deliveries and exposed via /api/webhook-deliveries/{executionId} (#39)

CLI

  • nexora schedule - New subcommand group for cron schedule management: nexora schedule add --goal "<goal>" --cron "<expr>" [--policy SKIP|FIRE_ONCE|FIRE_ALL], nexora schedule list [--active-only], nexora schedule remove <id>. Requires executionStore set in nexora.json (#38)
  • nexora dlq - New subcommand group for dead letter queue management: nexora dlq list [--state PENDING|RESOLVED|REPLAYED|ALL], nexora dlq replay <id>, nexora dlq resolve <id> [--reason "..."] (#42)

Fixed

  • Observability UI gets stuck on RUNNING for timed-out plans - NexoraObservability tracker missed the PlanTimedOutEvent, leaving the UI infinitely spinning on RUNNING. Fixed by adding the event listener and updating the dashboard UI to correctly render the orange TIMED_OUT badge.

Nexora v0.1.0

Choose a tag to compare

@github-actions github-actions released this 22 May 11:18
c214588

Added

Core Engine

  • Initial implementation of the Intent-Based Execution Engine (IBEE)
  • nexora-core - Intent, Plan, Step, InputBinding, CapabilityRequest, CapabilityResult, ExecutionContext, TraceContext
  • nexora-plugin-spi - NexoraPlugin, CapabilityProvider, Capability, CapabilityDescriptor, CapabilityContract, Planner interfaces
  • nexora-planner - RulePlanner matching step definitions to goals via predicate matchers; PlanRegistry for step definition storage
  • nexora-event - sealed ExecutionEvent hierarchy; InProcessEventBus with typed subscriptions
  • nexora-registry - DefaultCapabilityRegistry
  • nexora-retry - RetryPolicy, RetryPolicyRegistry, ExponentialBackoffPolicy
  • nexora-tracing - Tracer SPI, NoopTracer
  • nexora-runtime - ExecutionEngine orchestrating planner + scheduler + event bus
  • nexora-api - NexoraEngine public facade with fluent builder

Executor

  • DagStepScheduler executes plan steps in parallel respecting declared dependsOn relationships; cycle detection via DFS before execution starts
  • InterceptorPipeline and ExecutionInterceptor SPI for wrapping capability invocations
  • TracingInterceptor propagates TraceContext through all capability calls
  • CapabilityInvoker as the terminal pipeline node; resolves capabilities from the registry
  • RetryInterceptor and TimeoutInterceptor wired into the default interceptor pipeline

Plugins

  • Plugin loader - loads NexoraPlugin implementations from external JARs via URLClassLoader; manages plugin lifecycle (register → activate → deactivate → unload)
  • Pluggable planner SPI - plugins can supply custom Planner implementations; CompositePlanner tries plugin planners first and falls back to the rule-based planner
  • Reactive plan amendments - CapabilityResult can return PlanAmendment entries; DagStepScheduler applies AddStepAmendment, SkipStepAmendment, and ModifyInputAmendment atomically before dependent steps start
  • Capability contracts - CapabilityContract declares p99 latency SLA, max error rate, and fallback capability ID; CapabilityContractMonitor maintains a sliding window and routes unhealthy capabilities to their fallback transparently

CLI

  • nexora CLI - run, demo, observe, and help sub-commands via picocli
  • EngineFactory - builds NexoraEngine from a CliConfig YAML file

Observability

  • Observability server (nexora observe) - embedded HTTP server serving a live process dashboard, Prometheus /metrics endpoint, and /api/process snapshot endpoint
  • NexoraObservability - attaches to any NexoraEngine via event subscriptions; tracks execution lifecycle, step latencies, error rates, and amendment counts using Prometheus counters/gauges/histograms
  • ProcessSnapshot and ExecutionSnapshot DTOs for the process view API
  • WebSocket push - SnapshotListener SPI broadcasts live ProcessSnapshot JSON to connected browser clients over a dedicated port; ObserveCommand injects the port into the bundled HTML at serve time
  • Live observe UI - professional dark theme, Gantt-style timeline showing step parallelism, viewport-height layout with the execution list as the sole scrollable area, mobile responsive, live "Updated" counter
  • Grafana stack integration (docker-compose) with a pre-built dashboard

Production Readiness

  • Idempotency keys - CapabilityRequest carries a UUID per step-execution attempt; propagated through StepStartedEvent so capabilities can deduplicate side effects on retry
  • Durable execution state - ExecutionStore SPI with a JDBC/H2 default; persists execution lifecycle and per-step start/complete/fail transitions; opt in via NexoraEngine.builder().withExecutionStore(...)
  • Contract enforcement - capability p99 SLA used as the effective TimeoutInterceptor deadline when no explicit step timeout is set
  • Orchestration-based saga - SagaOrchestrator runs compensating capabilities in reverse topological order on partial execution failure; Step.compensateCapabilityId declares the rollback capability; opt in via .withSagaEnabled(true)
  • CompensationStartedEvent, CompensationCompletedEvent, CompensationFailedEvent added to the sealed ExecutionEvent hierarchy

Distribution

  • nexora-all - shaded bundle module aggregating all library modules (nexora-api, nexora-common, nexora-core, nexora-plugin-spi, nexora-registry, nexora-tracing, nexora-retry, nexora-event, nexora-executor, nexora-plugin-loader, nexora-planner, nexora-capabilities, nexora-saga, nexora-runtime, nexora-persistence) into a single deployable artifact; external transitive dependencies are promoted; individual modules are published separately alongside the bundle

Samples

  • tests/payment-pipeline - standalone 8-step fraud-detection DAG (validate_request, enrich_user_data, check_velocity, screen_sanctions → run_fraud_check → process_payment → send_confirmation + update_ledger) demonstrating plan amendments, capability contracts, fallback routing, and orchestration-based saga compensation
  • screen_sanctions step added between check_velocity and run_fraud_check; fails with SANCTIONS_HIT when forceBlockedUser=true; screen_sanctions_compensate included in the saga rollback chain
  • Four startup scenarios: happy path, high-risk fraud review, gateway failure with saga, and sanctions blocklist hit with partial compensation

CI/CD

  • ci.yml - builds and runs all tests on every push and pull request; uploads Surefire reports on failure
  • publish.yml - publishes each Maven module individually to GitHub Packages in dependency order, then creates a versioned GitHub Release on v* tag push; pre-release flag set automatically for tags containing -
  • bump-version.yml - triggered automatically after a release is published; computes the next SNAPSHOT version, updates all pom.xml files via mvn versions:set, and opens a pull request to merge the version bump back to the default branch

Fixed

  • CapabilityRequest rejects null inputs - optional context bindings (e.g. forceFailure, forceVelocityFail, forceBlockedUser) resolve to null when the caller omits them; Map.copyOf() throws NullPointerException on null values, causing steps to fail with an internal error and leaving dependent steps permanently pending. Null-valued entries are now stripped before the immutable copy is taken; capabilities should use getOrDefault() or Boolean.TRUE.equals() to test absent flags.
  • DagStepScheduler leaves dependent steps permanently pending on unexpected exceptions - when a step's thenApplyAsync threw an unchecked exception the future completed exceptionally, propagating through CompletableFuture.allOf and blocking all downstream steps indefinitely. A .handle() stage now intercepts any exception, publishes a StepFailedEvent with code INTERNAL_ERROR, and returns a failure StepResult so the pending counter decrements correctly and the execution terminates.
  • run_fraud_check embedded a null-prone forceFailure flag in its Map.of() output - removed; it was a control input, not a produced output.