neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
재현·결정론 규칙 없는 견고한 워크플로 엔진, 워크플로는 데이터, JAR 하나와 DB에 셀룰러 샤딩 내장
A durable workflow engine with no replay and no determinism rules: the workflow is data, not code. One JAR plus a database; cellular sharding built in.
개발 도구Java · PythonGitHub @hadielmougy온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Cellular durable workflow engine for the JVM
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Durable workflows.
Describe a process as a graph. Wiggle runs it as a durable state machine that survives
crashes, waits for humans, and retries failures — with a server you can embed, and workers in
the language you already use.
That's a complete, durable, parallel workflow. No YAML, no DSL files, no determinism rules
to memorize — a compiled graph the server owns, and plain Java methods (or Go, or Python) that
serve its steps.README 전문 보기 ↗Describe a process as a graph. Wiggle runs it as a durable state machine that survives crashes, waits for humans, and retries failures — with a server you can embed, and workers in the language you already use.
interface OrderSteps { // the steps, as a contract
Order validate(Order o);
boolean inStock(Order o);
Order authorise(Order o);
...
}
FlowSpec orders = FlowSpec.define("order-fulfilment", 1, Order.class, OrderSteps.class, (f, s) -> {
var validated = f.thenApply(s::validate).thenFilter(s::inStock);
var payment = validated.thenApply(s::authorise).thenApply(s::capture);
var shipping = validated.thenApply(s::reserveStock).thenApply(s::printLabel);
return Wiggle.allOf(payment, shipping) // both arms run, on isolated context copies
.combine(s::merge) // and rejoin explicitly
.thenApply(s::notify);
});
That's a complete, durable, parallel workflow. No YAML, no DSL files, no determinism rules to memorize — a compiled graph the server owns, and plain Java methods (or Go, or Python) that serve its steps.
A spec names its steps; it never runs them — the code is bound by name on a worker. So the steps
are declared as an interface and named through it, which is why the compiler can check that each step
consumes what the one before it produced. A handler that implements OrderSteps is then checked
against the same contract, so the two halves cannot drift. Continuing validated twice is what makes
the two parallel arms.
Contents · What is Wiggle? · Deployment options · Java example · Architecture · Performance · Configuration · Roadmap · Docs & links
Wiggle is a durable workflow engine: a JAR and a database. You define a business process as pure topology (named steps and how they chain, branch, and rejoin); Wiggle persists every instance as tokens moving over that graph, so a process survives restarts, retries, and worker death and resumes exactly where it left off. Steps are executed by pull-based workers over gRPC — your services, in your processes, in your language.
Its distinctive move is that the workflow is data: a compiled graph the server walks, not replayed code. So there is no determinism discipline to get wrong, and a running process can be inspected, traced and versioned like any other row in your database.
Why teams pick it:
step, gate, choose, fork, sleep, signals,
timers, sub-flows, repeatWhile, thenForEach — a compiled graph, published at a version you declare.
No workflow-code determinism to get wrong, because the workflow is data, not replayed code./healthz probes, memory admission control.In one picture — a single orders instance whose steps run on different microservices,
routed by each step's queue. The server keeps the durable state; each service just pulls the
steps it serves:
How queue routing works end to end → docs/queues.md
One codebase, three postures — start embedded, grow into a cluster, without rewriting your workflows.
| Mode | What it is |
|---|---|
| Embedded | WiggleServer inside your JVM, in-memory or DB store |
| Standalone server | one node, gRPC :8080, in-memory or a database |
| Cluster | several nodes on one database — shared queue, leader runs timers/recovery |
The server is a library. No database configured means an in-memory store — perfect for tests:
try (WiggleServer server = new WiggleServer(ServerConfig.fromEnvironment()).start();
WiggleClient client = new WiggleClient(server.baseUrl())) {
// register flow specs, run workers, start instances — all in-process
}
./gradlew :dist:run # single node, in-memory, gRPC on :8080
Or download the pre-built distribution from the GitHub release
and run it directly — no build, no Maven, no registry, just a JRE 21 (ideal for airgapped or
locked-down environments). Each release attaches wiggle-server-<version>.tar/.zip plus a signed
SHA-256SUMS:
tar xf wiggle-server-0.0.10.tar
sha256sum -c SHA-256SUMS # optional: verify the download
WIGGLE_JDBC_URL=jdbc:postgresql://db:5432/wiggle \
WIGGLE_JDBC_USER=wiggle WIGGLE_JDBC_PASSWORD=wiggle \
./wiggle-server-0.0.10/bin/wiggle
As a container — one image bundles every storage backend; the JDBC URL scheme picks one at runtime, so you never build a per-database image. The signed, multi-arch (amd64 + arm64) image is published to both Docker Hub and GitHub Container Registry — pull from whichever your environment prefers:
docker run --rm -p 8080:8080 \
-e WIGGLE_JDBC_URL=jdbc:postgresql://db:5432/wiggle \
-e WIGGLE_JDBC_USER=wiggle -e WIGGLE_JDBC_PASSWORD=wiggle \
hadielmougy/wiggle:0.0.10 # Docker Hub
# ghcr.io/hadielmougy/wiggle:0.0.10 # …or GHCR (same image)
Clustering is just a shared database. Point several nodes at one PostgreSQL and they form a cluster: every node serves the API and hands out work; exactly one is elected leader for clock-driven duties (timers, lease recovery, schedules). Kill any node — including the leader — and the rest carry on. The schema creates and migrates itself on startup (versioned, forward-only migrations under a cross-node advisory lock).
docker compose up -d postgres
scripts/cluster.sh 20 # three server nodes, two workers, one Postgres
scripts/kind-up.sh 3 # or the same on Kubernetes (kind)
On a real cluster, the Helm chart deploys a hardened, non-root pod (distroless
image, read-only root filesystem, all capabilities dropped — passes a restricted PodSecurity
namespace unmodified). Point image.registry at your internal registry and you're done:
helm install wiggle deploy/helm/wiggle \
--set image.registry=artifactory.example.com \
--set storage.jdbc.url=jdbc:postgresql://postgres:5432/wiggle \
--set storage.jdbc.user=wiggle --set storage.jdbc.password=secret \
--set replicaCount=3
A web UI the server serves on its own port (WIGGLE_PORTAL_PORT, default 8070), reading the engine
in process — any node with it on serves the whole cluster. Every instance as a table of the steps it ran — click a step to expand its input, output,
retries and timing — plus cancel, deliver signals, schedules, and search by
instance id or correlation id. The first visit sets the admin password and stores it in the
database (or set WIGGLE_DASHBOARD_PASSWORD to skip that). Login: built-in admin and read-only viewer
accounts from the environment, plus accounts and roles built from permissions (scoped to a
workflow if you like) managed in the portal and kept in the database, with an audit of every
change. Any node serves any signed-in session. The /healthz probe for Kubernetes stays on its own port.
The Performance tab reads the same timings for every execution mode: each step's p50/p95 by the handler's own clock and how long it waited to be claimed, slowest first.
./gradlew :example:seedDashboard # a seeded server with the portal → http://localhost:8070
The fastest end-to-end: an embedded server, one worker, one instance — one JVM.
import com.wiggle.client.flow.*;
import com.wiggle.client.worker.*;
import com.wiggle.core.InstanceView;
import com.wiggle.server.*;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
// 1. The logic lives in a @ForFlow class. The signature defines the step.
@ForFlow("greet")
class GreetHandlers implements GreetSteps {
public Map<String, Object> sayHello(Map<String, Object> ctx) {
Map<String, Object> next = new HashMap<>(ctx);
next.put("greeting", "hello, " + ctx.get("name"));
return next;
}
}
// 2. The workflow names its steps through a contract. Nothing runs here -- the chain is walked
// once and compiled to a graph; the code that runs each step is bound by name on the worker.
interface GreetSteps { Map<String, Object> sayHello(Map<String, Object> ctx); }
FlowSpec greet = FlowSpec.define("greet", 1, Map.class, GreetSteps.class, (f, s) -> f.thenApply(s::sayHello));
// 3. Embedded server + worker + one instance.
try (WiggleServer server = new WiggleServer(ServerConfig.fromEnvironment()).start();
WiggleClient client = new WiggleClient(server.baseUrl())) {
try (Worker worker = new Worker(client, "worker-1")
.registerHandler(new GreetHandlers())) {
worker.start();
String id = client.start(greet, Map.of("name", "ada"));
InstanceView result = client.awaitCompletion(id, Duration.ofSeconds(10));
System.out.println(result.status()); // COMPLETED
System.out.println(result.context()); // {name=ada, greeting=hello, ada}
}
}
A real one — parallel branches, a guard, a retry policy, a server-side timer:
FlowSpec orders = FlowSpec.define("order-fulfilment", 1, Order.class, OrderSteps.class, (f, s) -> {
var validated = f.thenApply(s::validate)
.thenFilter(s::inStock); // false ⇒ the instance ends cleanly, not an error
var payment = validated
.thenApply(s::authorise, RetryPolicy.exponential(5, Duration.ofMillis(100)))
.thenApply(s::capture);
var shipping = validated
.thenApply(s::reserveStock)
.thenSleep("await-warehouse", Duration.ofMillis(300)) // no worker held while waiting
.thenApply(s::printLabel);
return Wiggle.allOf(payment, shipping) // arms ran on isolated context copies...
.combine(s::merge) // ...so rejoining them is explicit, never implicit
.thenApply(s::notify);
});
Sometimes the graph is written where its handlers are not — an author registering it with no handler classes on its classpath, or several independent workers that each bind a subset of the steps by name. Nothing changes: a spec never holds a handler, only the step's name, so the interface it names them through is a declaration you need not implement.
public interface OrderSteps { // declared here, implemented elsewhere
Order validate(Order o);
boolean inStock(Order o);
Order authorise(Order o);
// ...
}
// the author registers the topology without implementing a single step
FlowSpec orders = FlowSpec.define("order-fulfilment", 1, Order.class, OrderSteps.class, (f, s) -> { … });
That is what lets one workflow be served by workers in Java, Go and Python without any of them redefining it. A worker that does have the handlers can implement the interface and let the compiler check that every step matches.
Handlers are plain methods — typed records or raw maps, your choice per step. The signature
defines the step kind: a boolean return is a gate, void is an effect, anything else is a
task whose return value becomes the new context:
@ForFlow("order-fulfilment")
class OrderHandlers {
public Order validate(Order o) { return o.withStatus("VALIDATED"); }
public boolean inStock(Order o) { return o.quantity() > 0; } // gate: "in-stock"
public Order authorise(Order o) { return o.withPaymentRef("auth-" + o.orderId()); }
public Order reserveStock(Order o) { return o.withShipmentRef("shp-" + o.orderId()); }
public Order printLabel(Order o) { return o.withTrackingLabel("DHL-" + o.orderId()); }
public Order capture(Order o) { return o.log("captured"); }
// The combine is mandatory and explicit: fold what each branch produced onto the pre-fork
// order and return the COMPLETE post-join context — nothing merges implicitly.
public Order merge(Order payment, Order shipping) { // found by type; the pre-fork order is Step.base()
return Step.base(Order.class).withPaymentRef(pay.paymentRef())
.withShipmentRef(ship.shipmentRef()).withTrackingLabel(ship.trackingLabel());
}
public Order notify(Order o) { return o.withStatus("FULFILLED"); }
}
Dynamic fan-out is just as explicit — the element is the item's context (forEach maps
elements the way fork transforms contexts):
.thenForEach(Order::items, item -> item.thenApply(s::price)) // one isolated branch per element
.combine(s::collect)
Priced price(LineItem line) { // the parameter IS the element
Order base = Step.base(Order.class); // frozen pre-forEach context, read-only
return new Priced(line.sku(), base.rate() * line.amount());
}
Order collect(List<Priced> priced, Order base) { /* any order; you decide what lands */ }
When each element needs a chain of its own, let a step build the branches at run time instead —
thenApply(s::plan).combine(s::collect), with Step.create(item).thenApply(this::price) in the
handler (cookbook recipe 9).
Run it from any process — different teams can serve different steps of the same flow, each
with its own @ForFlow class and its own deploy, matched by name:
try (DirectConnection wiggle = WiggleConnection.direct("localhost:8080")) {
Worker worker = new Worker(wiggle.client(), "worker-1",
WorkerOptions.defaults().withConcurrency(16))
.register(orders)
.registerHandler(new OrderHandlers())
.start();
String id = wiggle.client().start(orders, Order.of("A-1001", "ada", 3, new BigDecimal("249.90")));
InstanceView v = wiggle.client().awaitCompletion(id, Duration.ofSeconds(30));
}
A team that only starts workflows needs none of that — no FlowSpec, no shared jar. The graph is data the server owns, so a submitter's whole contract is the workflow name plus the agreed context shape (exactly the coupling of calling an HTTP API). Registration ships with the worker artifact — the handlers and the graph they serve deploy as one atomic act:
// a separate submitting service: name + context, nothing else
String id = client.start("order-fulfilment", Map.of("orderId", "A-1001", "quantity", 3L));
// pin a version to be immune to mid-deploy definition changes (unpinned = latest)
String id2 = client.start("order-fulfilment", ctx, 302800684, "corr-42");
And the parts long-running processes actually need are first-class:
// Human / external input — the instance parks (no worker held), a deadline can escalate:
FlowSpec.define("expense", 1, Expense.class, ExpenseSteps.class, (f, s) -> f
.thenApply(s::submit)
.thenAwait("manager-approval", Duration.ofHours(48), b -> b.thenApply(s::autoEscalate))
.thenApply(s::payOut));
client.signal(instanceId, "manager-approval", Map.of("decision", "approved"));
// Cron & interval schedules — exactly-once firing, even across leader failover:
client.createCronSchedule("nightly-report", "0 3 * * *", null);
// Sub-workflows, cancellation, retries with attempt introspection:
client.cancel(id, "customer changed their mind");
More runnable code: ./gradlew :example:run (full order demo, one JVM) · the
cookbook — eight workflows exercising every operator, runnable:
./gradlew :example:runCookbook
(source).
The same eight graphs either way — worth reading side by side, since the typed one is a single
class per recipe where the other is a topology file plus a handlers file.
| Component | Module | What it does |
|---|---|---|
| Engine (server) | server | The durable state machine: compiles graphs, moves tokens, leases steps to workers, runs timers/signals/schedules, recovers dead workers. Clusters over a shared DB; leader-elected housekeeping. Serves gRPC :8080 and a /healthz probe. |
| Storage | jdbc, postgres | One HikariCP-pooled JDBC store behind an explicit StorageFactory: PostgreSQL to deploy on, H2 for tests and local runs. No DB configured ⇒ in-memory. |
| Client & worker | client | Workflow authoring (FlowSpec.define), @ForFlow binding, WiggleClient, pull-based Worker, WiggleConnection. |
| Portal | console | The web UI (embedded Tomcat) a server serves on WIGGLE_PORTAL_PORT, over its own engine. Trace, cancel, signal, schedules, search; accounts and permission-set roles on the auth shard. |
| Distribution | dist | The one runnable image: the server with the portal, every storage backend bundled. |
The mechanics that make it hold together:
define("orders", 2, …)). Re-registering the same graph is a no-op; re-registering a changed
one under a published version is refused, so a forgotten bump is a deploy-time error rather than a
graph swapped under running instances. In-flight instances keep the version they started on.step("render", "gpu")); worker pools
subscribe to queues, so one flow's steps spread across many services with no broker.LOCAL_SYNC / LOCAL_ASYNC execution modes let a worker run
consecutive same-queue steps back-to-back, cutting server round-trips for step-heavy flows
(see docs/local-execution.md).9,600 durable step executions/sec (1,200 workflow instances/sec), sustained by one server node on Cloud SQL for PostgreSQL. Every step is committed to the database, and each instance completes in about 1 to 1.5 seconds end to end. At lighter load the end-to-end time is about 260ms: 8,000 steps/sec (1,000 instances/sec) holds flat with no backlog.
Each instance is the 8-step order-fulfilment fork/join workflow: validate, a stock gate, two
parallel branches, an explicit combine, notify and audit. It runs in LOCAL_ASYNC mode. The bench
ramps the offered start rate and measures probe sojourn: the time a fresh instance takes from
start() to COMPLETED. The ceiling is the highest load at which sojourn stays bounded.
| Cloud SQL | sustained load | window | end-to-end latency |
|---|---|---|---|
| 8 vCPU | 5,600 steps/s (700/s) | 30s | flat ≈260ms |
| 8 vCPU | 7,200 steps/s (900/s), the ceiling | 2 × 60s | ≈2–4s |
| 16 vCPU | 7,200 steps/s (900/s) | 2 × 60s | flat ≈260ms |
| 16 vCPU | 8,000 steps/s (1,000/s) | 30s | flat ≈260ms |
| 16 vCPU | 9,600 steps/s (1,200/s), the ceiling | 30s | ≈0.9–1.4s |
The database sets the ceiling, not the server. At the ceiling the server node stayed under 50% CPU and the workers had headroom. On 8 vCPU, Cloud SQL ran at ≈80% CPU. On 16 vCPU it peaked at only 63%: there the limit is time spent waiting on instance locks, because the two branches of a fork finish together and the second one's report waits for the first one's commit. Doubling the database's vCPUs raised the ceiling 1.33×. Adding server nodes on the same database would not raise it.
Size the connection pool to the load. With WIGGLE_JDBC_POOL_SIZE=32 (the default is 10),
starts queued for a connection at ≈561/s while the server and database CPUs still had headroom.
One instance makes ≈45 SQL statements, so at 600/s about 25 connections are busy at once.
Raising the pool to 128 removed the limit.
Environment:
| Region | GCP us-central1-a. Everything is in one zone, on a private VPC. |
| Database | Cloud SQL Enterprise, PostgreSQL 16, zonal (no HA standby), 250 GB SSD, db-custom-8-32768 and db-custom-16-65536 |
| Server | 1 node, c3-standard-8, WIGGLE_JDBC_POOL_SIZE=128, default settings otherwise |
| Workers | own c3-standard-8: 4 processes × 256 slots, LOCAL_ASYNC batch 64 |
| Load generator | own c3-standard-8: RateCeilingBench, 256 submitter threads |
| Build | revision 0dcff87, OpenJDK 21; one database, resized from 8 to 16 vCPU between runs |
A regional (HA) Cloud SQL instance adds a synchronous standby to every commit, so expect a lower ceiling there.
Reproduce it end to end. The script provisions the environment, deploys a revision, runs the
ladder, and collects pg_stat_statements, per-VM CPU and a server JFR:
export GCP_PROJECT=<sandbox-project>
deploy/gcp/ceiling.sh up && deploy/gcp/ceiling.sh deploy && deploy/gcp/ceiling.sh run
deploy/gcp/ceiling.sh down
See deploy/gcp/README.md for every knob.
Everything defaults sensibly; override by environment variable (or the same-named system
property). The tables below are the ones you'll actually touch — the complete reference,
including programmatic WorkerOptions, lives in docs/onboarding.md.
| Variable | Default | Meaning |
|---|---|---|
WIGGLE_PORT | 8080 | gRPC port (0 picks a free one) |
WIGGLE_JDBC_URL | (unset) | unset = in-memory, single node; set to cluster on a DB. Scheme picks the backend: jdbc:postgresql: to deploy on, jdbc:h2: for tests and local runs |
WIGGLE_JDBC_USER / WIGGLE_JDBC_PASSWORD | database credentials | |
WIGGLE_SCHEMA_MODE | apply | apply runs pending migrations on startup; verify applies nothing and fails fast if the schema is behind or has drifted (DBA/CI-owned schema) |
WIGGLE_MIGRATE_ONLY | false | true = apply migrations and exit (a one-shot job; then run the app with WIGGLE_SCHEMA_MODE=verify) |
WIGGLE_JDBC_POOL_SIZE | 10 | HikariCP max pool size |
WIGGLE_JDBC_TX_ATTEMPTS | 3 | replays for a transaction that rolled back on a momentary database failure; 1 disables |
WIGGLE_PG_RANDOM_PAGE_COST | 1.1 | PostgreSQL random_page_cost on each connection; server keeps the server's own |
WIGGLE_LEASE_MILLIS | 30000 | task lease before a stalled step is reclaimed |
WIGGLE_RECORD_STEP_IO | true | record each step's input and output for the portal (false turns it off) |
WIGGLE_STEP_IO_MAX_CHARS | 65536 | cap per recorded input/output; longer ones keep their first 4096 characters |
WIGGLE_LONGPOLL_MAX_MILLIS | 20000 | max server-side block of a worker poll |
WIGGLE_POLL_INTERVAL_MILLIS | 1000 | housekeeping / dispatch loop cadence |
| Variable | Default | Meaning |
|---|---|---|
WIGGLE_PORTAL_PORT | 8070 | HTTP port; 0 turns it off |
WIGGLE_DASHBOARD_USER / WIGGLE_DASHBOARD_PASSWORD | admin / (unset) | admin login from the environment; unset = the first visit sets the admin password, kept in the database |
WIGGLE_DASHBOARD_VIEWER_USER / WIGGLE_DASHBOARD_VIEWER_PASSWORD | viewer / (unset) | optional read-only account — sees everything, can't cancel/signal/schedule |
WIGGLE_AUTH_CACHE_MILLIS | 30000 | how long a node serves a cached account or session; accounts, roles and sessions live on the auth shard |
WIGGLE_CONSOLE_USERS_FILE | wiggle-users.json | an old console users file, imported once and then no longer read |
WIGGLE_TLS_* | (unset) | the same keystore serves the portal over HTTPS |
Security posture in one line: TLS everywhere is a keystore away; a truststore on the server upgrades it to mTLS;
WIGGLE_GRPC_AUTH=enforcechecks every gRPC call against the caller's role, by API key or certificate; the portal has accounts with the same permission-set roles.
Where it's going — the honest list:
WIGGLE_GRPC_AUTH).Step.emit on the event log, committed with the step that
emitted it.Suggestions and PRs welcome — open an issue.
| 🚀 Onboarding + full configuration reference | everything, one page |
| 🧑🍳 Cookbook | every operator in runnable code — ./gradlew :example:runCookbook |
| 🤖 Multi-agent research team | LLM agents as durable steps: a supervisor's branches, a critic loop, a human sign-off — ./gradlew :example:runAgents |
| 🧵 Queues | one flow's steps across many microservices |
| ⚡ Local execution | LOCAL_SYNC / LOCAL_ASYNC step chaining |
| 📨 Event log | durable lifecycle and handler-emitted events, pulled and acknowledged by named consumers |
| 📽 Slide deck | the 5-minute tour |
| 🐍 wiggle-python · 🐹 wiggle-go | idiomatic clients, same control plane |
Install (Maven Central, sh.wiggle):
implementation("sh.wiggle:wiggle-client:0.0.10") // DSL + worker + client
implementation("sh.wiggle:wiggle-server:0.0.10") // only to embed the server
implementation("sh.wiggle:wiggle-postgres:0.0.10") // + your storage module
Prefer the BOM so every wiggle module (and the shared gRPC/protobuf stack) stays version-aligned with no per-dependency pins:
implementation(platform("sh.wiggle:wiggle-bom:0.0.10"))
implementation("sh.wiggle:wiggle-client") // versions come from the BOM
Behind a locked-down internal Artifactory? Use the shaded client — one self-contained jar with
gRPC, protobuf and Guava relocated under com.wiggle.shaded, so it has zero transitive
dependencies and cannot clash with anything already on the app's classpath:
implementation("sh.wiggle:wiggle-client-all:0.0.10") // author flows + run workers, nothing else
Build from source — JDK 21+, wrapper included:
./gradlew build # full build + tests
./gradlew :example:run # see it work
WIGGLE_HOUSEKEEPING_BATCH | 100 | timers/signals/reclaims swept per pass |
WIGGLE_DISPATCH_LINGER_MILLIS | 5 | wake-on-produce batch linger (0 = claim immediately) |
WIGGLE_FALLBACK_POLL_MILLIS | 100 | long-poll fallback re-claim interval |
WIGGLE_SCOPED_FALLBACK_MAX_MILLIS | 2000 | the most a narrowed poll's fallback interval (version-scoped, or with criteria) grows to while its claims find nothing |
WIGGLE_HEARTBEAT_INTERVAL_MILLIS | 5000 | node heartbeat cadence |
WIGGLE_MISSED_HEARTBEATS | 3 | missed beats before a node is considered dead |
WIGGLE_RETENTION_MILLIS | 86400000 | how long finished instances are kept |
WIGGLE_NODE_NAME | hostname | name in cluster membership |
WIGGLE_DASHBOARD_PORT | 0 (off) | port for the /healthz probe endpoint |
WIGGLE_PORTAL_PORT | 8070 | port for the web portal (below); 0 turns it off |
WIGGLE_ALLOW_GRAPH_REPLACE | false | development only — honour register(spec, force) and replace the graph of an already published version instead of rejecting it |
WIGGLE_MEMORY_SHEDDING_ENABLED | false | memory admission control — under heap pressure, reject a fraction of polls (WIGGLE_MEMORY_THRESHOLD 0.90, WIGGLE_MEMORY_REJECT_RATIO 0.10, WIGGLE_MEMORY_RETRY_MILLIS 2000, WIGGLE_MEMORY_RETRY_JITTER_MILLIS 1000) |
WIGGLE_TLS_KEYSTORE (+_PASSWORD) | (unset) | keystore ⇒ TLS on; unset = plaintext |
WIGGLE_TLS_TRUSTSTORE (+_PASSWORD) | (unset) | truststore on a server ⇒ require client certs (mTLS) |
WIGGLE_SEARCH_ENABLED | false | full-text search over instances on the one database; a topology gets it from a search shard (WIGGLE_SEARCH_RETENTION_MILLIS, WIGGLE_SEARCH_WORKFLOWS) |
WIGGLE_EMBEDDER | none | semantic search by meaning: http (any OpenAI-compatible /embeddings API, with WIGGLE_EMBEDDER_URL/_MODEL/_DIMENSION/_API_KEY) or hashing; pgvector gives it an HNSW index |
WIGGLE_GRPC_AUTH | off | per-RPC authorization: log reports what would be refused, enforce refuses; callers present an API key (WIGGLE_API_KEY) or a client certificate |
WIGGLE_LOG_FILE / WIGGLE_LOG_LEVEL | (unset) / INFO | rotating file log (JDK System.Logger — zero logging deps) |