How to run Dizzy locally.
Goal: simplest setup first, with optional failure-isolated add-ons.
Terminal A:
npm start(ornode .\agent_server.mjs)
Terminal B:
Invoke-RestMethod http://127.0.0.1:3000/healthInvoke-RestMethod http://127.0.0.1:3000/prompt
This starts the local API and exposes governance and prompt inspection endpoints. It does not require Telegram, Redis, or an external model backend. The Guided Trust Cockpit dashboard is opt-in; set DIZZY_DASHBOARD_ENABLED=1 before starting the server to serve http://localhost:3000/dashboard. The /memory/graph HTTP inspection endpoint is also opt-in; set DIZZY_MEMORY_GRAPH_ENABLED=1 before starting the server, then run Invoke-RestMethod http://127.0.0.1:3000/memory/graph.
The Guided Trust Cockpit dashboard read-only routes are isolated in lib/dashboard.mjs and its local-only renderer lives at dashboard/index.html. Asset failure leaves core routes such as /health available.
Dashboard browser access uses a loopback-only temporary operator session. Set DIZZY_DASHBOARD_ENABLED=1 before running npm start, and set a strong DIZZY_AUTH_TOKEN if authenticating non-loopback clients. Open /dashboard/login or /dashboard. The server exchanges tokens for random in-memory HttpOnly; SameSite=Strict cookies. The cookie is accepted only on dashboard routes and never authorizes general API routes. Do not put credentials in query strings or browser storage.
Dashboard API responses use the minimal-v1 projection: repository-relative paths are replaced by opaque stable IDs. Executable dashboard JavaScript is served from guarded local assets, and the HTML CSP allows same-origin scripts without inline script execution.
Terminal A:
node .\agent_server.mjs
Terminal B:
$env:TELEGRAM_BOT_TOKEN="..." ; $env:TELEGRAM_CHAT_ID="..." ; $env:DIZZY_BASE_URL="http://127.0.0.1:3000" ; node .\scripts\telegram_relay.mjs
This gives inbound Telegram -> /dispatch/incoming -> replies. Tool jobs will fail cleanly unless you also run Redis + worker.
- Find your chat id (once):
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\get_telegram_chat_id.ps1
- Set persistent USER env vars (once per machine/user):
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\set_user_env_telegram.ps1
- Launch server + relay (every time):
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\launch_telegram.ps1
Doctor (sanity check: server health + Telegram token/chat id):
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\doctor_telegram.ps1
Add-ons (optional):
$env:DIZZY_ENABLE_WORKER="1"to also open the Redis-backed worker window$env:DIZZY_ENABLE_NOTIFY_DRAIN="1"to also open Telegram "job dead" alerts$env:TELEGRAM_POLL_JOB_RESULTS="1"to have the relay poll/jobs/:idand post completions$env:TELEGRAM_SEND_STARTUP_MESSAGE="1"to send a startup Telegram message after launch
Notes:
- The launcher waits for
GET /healthbefore starting the relay (prevents "Dispatch error: fetch failed" when the server is not up yet). - By default, the launcher computes
DIZZY_BASE_URLfromPORTand ignores any pre-setDIZZY_BASE_URL. To override intentionally, setDIZZY_BASE_URL_OVERRIDE=1. - By default, the relay does not send an unsolicited Telegram startup message. Set
TELEGRAM_SEND_STARTUP_MESSAGE=1if you want that behavior. TELEGRAM_ALLOW_AUTO_BIND=1prints a one-time nonce; the first private chat must send/bind <nonce>before it is accepted.- In Telegram, use
/help,/governance,/health.
In one terminal:
node .\agent_server.mjs
Defaults:
- binds to
127.0.0.1(override withDIZZY_BIND_HOST) - defaults to
DIZZY_DEPLOYMENT_MODE=direct_local; useproxiedbehind a reverse proxy or tunnel, andhostedfor direct non-loopback exposure - defaults to
DIZZY_PUBLIC_SURFACES=closed; usediscoveryonly when profile, services, portfolio, logo, and governance should remain anonymously readable - structural transparency doc:
GET /governance
Optional auth (recommended if you ever bind beyond loopback):
- set
DIZZY_AUTH_TOKENon the server process and also on any client/relay processes proxiedandhostedmodes requireDIZZY_AUTH_TOKEN, even when the runtime itself receives the connection on loopbackdirect_localrejects forwarding headers because their presence contradicts the declared deployment boundary- preserve standard forwarding headers for auditability;
proxiedmode still requires authentication if a proxy strips them - set
DIZZY_ENFORCE_IDENTITY_HEADERS=1inproxiedmode to ignore body-suppliedclient_idandservice_idand read them fromX-Dizzy-Client-IdandX-Dizzy-Service-Id - identity-header enforcement fails startup unless
DIZZY_TRUSTED_PROXIESnames the proxy socket IPs allowed to supply those headers (for example127.0.0.1,10.0.0.1) - set
DIZZY_EXECUTE_TOKENand/orDIZZY_NOTIFY_TOKENalongsideDIZZY_AUTH_TOKENto restrict API callers to/agent/executeor/notify; scoped-token configuration fails startup without the master token - set
DIZZY_VERIFIED_HTTPS=1inproxiedorhostedmode when the deployment is served over verified HTTPS. This enables Strict-Transport-Security (HSTS) headers. Enabling verified-HTTPS validation prevents HSTS from interfering with local HTTP development.
Tool invocations (tool:http_get ..., tool:cheerio_extract ...) require Redis.
Ways to run Redis on Windows (pick one):
- Memurai (Redis-compatible, easiest on Windows): install/start Memurai Developer so it listens on
127.0.0.1:6379. - WSL (Ubuntu): install Redis inside WSL and expose it on
127.0.0.1:6379. - Docker Desktop: run a Redis container publishing
6379.
Then (with Redis running):
- set
REDIS_URL(example:redis://127.0.0.1:6379) node .\worker.mjs
Note:
- The server process must also have
REDIS_URLset (thelaunch_telegram.ps1script does this automatically whenDIZZY_ENABLE_WORKER=1).
If you do not want Redis right now:
- Tool calls can also run inline (no worker) by setting
DIZZY_TOOL_MODE=auto(default) orDIZZY_TOOL_MODE=inline. - Localhost/private-network fetches are denied by default. Only opt in with
DIZZY_TOOL_ALLOW_LOCALHOST=1and/orDIZZY_TOOL_ALLOW_PRIVATE_NET=1when you explicitly need them. - Redirects are manually validated and capped (
DIZZY_TOOL_MAX_REDIRECTS, default3) so an external URL cannot silently bounce into your local network. - File-mutating chat commands from Telegram (
/remember,/memory_review,/improve,/apply) are denied by default. SetDIZZY_ALLOW_REMOTE_MUTATIONS=1only if you intentionally want Telegram to be allowed to write local state.
Stop the server, worker, and relay before backup or restore so the snapshot has no active writers.
node .\scripts\backup_restore.mjs backup [destination-directory]copiesruntime/into a new directory and refuses to overwrite an existing destination.- Backups include a
manifest.jsonwith SHA-256 hashes for every copied file. node .\scripts\backup_restore.mjs restore <snapshot-directory>verifies the manifest, then renames the current runtime intobackups/before copying the snapshot. If copying fails, it restores the original runtime.node .\scripts\backup_restore.mjs repair [file-or-directory]repairs only a malformed final JSONL record and preserves the original beside it as a timestamped.bakfile. It refuses corruption found earlier in a file.
Snapshots can contain private conversation and execution data. Protect them like the live runtime directory.
SQLite remains an experimental, non-authoritative prototype. The snapshot command checkpoints a prototype database when present, but DIZZY_OPERATIONAL_STORE=sqlite is not a supported runtime mode.
In a third terminal:
$env:TELEGRAM_BOT_TOKEN="..." ; $env:TELEGRAM_CHAT_ID="..." ; $env:DIZZY_BASE_URL="http://127.0.0.1:3000" ; node .\scripts\telegram_relay.mjs
Optional:
$env:TELEGRAM_POLL_JOB_RESULTS="1"to post tool job results when they complete$env:DIZZY_AUTH_TOKEN="..."if the server requires auth
Telegram commands:
/help/governance/health/prompt(shows which local prompt files are loaded)/reset(clears Gemini chat memory for this Telegram chat, when enabled)/remember(writes a compact session summary tomemory/for durable recall via RAG)/memory_review(proposes curated updates toMEMORY.md+memory/topics/*.md; apply with/apply <id> CONFIRM)
Duplicate replies:
- If you see two similar replies to one message, you almost certainly have multiple
telegram_relayprocesses running. - The relay now enforces a single-instance lock by default (
runtime/telegram_relay.lock). Close extra "Dizzy Telegram Relay" windows and restart. - To intentionally run multiple relays (not recommended), set
TELEGRAM_ALLOW_MULTI=1.
In a fourth terminal:
$env:TELEGRAM_BOT_TOKEN="..." ; $env:TELEGRAM_CHAT_ID="..." ; $env:DIZZY_BASE_URL="http://127.0.0.1:3000" ; node .\scripts\telegram_notify_drain.mjs
Notes:
- This surfaces
/notify/:channelmessages (currently terminal failures:kind=job_dead). - Set
DIZZY_AUTH_TOKENorDIZZY_NOTIFY_TOKENhere too if auth is enabled. - Redis atomically enqueues each dead-job notification with its recovery marker, so an ambiguous Redis response can be retried without adding a second queue item.
- DLQ enqueue and its recovery marker use the same atomic pattern. Retained claims are retried periodically with bounded backoff while the worker remains running; pending counts and recovery errors are logged.
- End-to-end delivery remains at-least-once: Telegram may accept a message before the drain can acknowledge its exact queue receipt, so downstream duplicates remain possible after a drain crash.
- The queue currently supports standalone Redis-compatible servers only. Redis Cluster is not supported because multi-key Lua operations do not use a shared hash slot; ACLs must permit
EVALand the commands used by the queue scripts.
If you want Telegram plain-text messages to get a real model response (instead of Ack: ...), set:
DIZZY_CHAT_BACKEND=geminiGEMINI_API_KEY=...GEMINI_MODEL=...(default isgemini-1.5-flashif omitted)- Optional:
DIZZY_PROMPT_PACK=core|creative|ops|full(seePROMPT_PACKS.md) - Optional style defaults:
DIZZY_BREVITY_MODE=lite(default)DIZZY_AFFECT_MODE=attuned(default)DIZZY_REINFORCEMENT_MODE=gold_star(default)
Relevant-note injection (uses local .md files to reduce genericness without stuffing everything into the prompt):
- Enabled by default:
DIZZY_RAG_ENABLED=1 - Control:
DIZZY_RAG_TOP_K=4,DIZZY_RAG_CACHE_MS=10000,DIZZY_RAG_MAX_FILE_BYTES=200000 - Structural memory graph:
- Internal graph context is available by default; the HTTP inspection route is disabled by default and requires
DIZZY_MEMORY_GRAPH_ENABLED=1before server start. - Control:
DIZZY_MEMORY_GRAPH_TOP_K=3,DIZZY_MEMORY_GRAPH_CACHE_MS=10000 - Inspect live summary:
GET /memory/graph - Query graph context:
GET /memory/graph?q=wikimedia - Write a readable artifact:
node .\scripts\sync_memory_graph.mjs
- Internal graph context is available by default; the HTTP inspection route is disabled by default and requires
Optional knobs:
DIZZY_CHAT_MAX_TURNS=16(short memory window)DIZZY_CHAT_TIMEOUT_MS=20000DIZZY_CHAT_TEMPERATURE=0.7
Notes on memory:
- Working chat context is stored as JSONL in
runtime/conversations/telegram_<chat_id>.jsonl(used as the short history window). - Durable recall should be written to markdown under
memory/(use/remember). - Long-term memory is indexed in
MEMORY.md(keep it small; details live inmemory/topics/*.md). - Validate memory index caps:
node scripts/memory_validate.mjs - Memory quality rule of thumb: prefer delta over duplicate recap. A good memory captures what changed, why it matters, and what should be reused later.
- Auto-memory is enabled by default when chat is enabled. It writes only on higher-signal turns, with cooldown and dedupe gates to avoid summarizing every exchange.
- Trust-zone note:
/agent/executepaid/public requests are ephemeral by default. Enable continuity explicitly per client/task withcontinuity_mode=clientif you intentionally want retained client-specific chat history. - Client continuity requires
client_idandservice_id;/agent/executederives the conversation key from those fields and ignores caller-provided keys. - Export retained client continuity as JSON with
GET /agent/continuity/export?client_id=...&service_id=...orGET /agent/continuity/export?conversation_key=.... WhenDIZZY_AUTH_TOKENis configured, use the same bearer auth as other/agent/*routes. SeePORTABILITY.mdfor schema, exclusions, and deletion behavior. - Ephemeral
/agent/executerequests do not write execution-history entries. - Markdown auto-retrieval is scoped to trusted root doctrine docs plus
memory/by default. Imported markdown under_ext/and_external/is excluded unless you explicitly widen the allowlist.
Optional: automatic chat fallback (keep Gemini as primary)
DIZZY_CHAT_FALLBACK_BACKEND=openai_compat(only triggers on transient Gemini failures like 429/5xx/timeout)OPENAI_COMPAT_BASE_URL=...(example: Groq OpenAI-compatible base URL, or a local Ollama/vLLM base URL)OPENAI_COMPAT_API_KEY=...(if required by the endpoint; local may be blank)OPENAI_COMPAT_MODEL=...DIZZY_FALLBACK_MAX_CALLS_PER_HOUR=10(optional global safety cap across all conversations)DIZZY_FALLBACK_MAX_CALLS_PER_CONVERSATION_HOUR=3(optional fairness cap for one conversation)- DLQ JSONL is a redacted diagnostic record, not a directly replayable job archive; replays must reconstruct credentials from approved runtime configuration.
- Optional: reduce fallback token burn (useful for free tiers):
OPENAI_COMPAT_MAX_TOKENS=500(caps fallback completion length)DIZZY_FALLBACK_MAX_TURNS=6(sends fewer recent turns on fallback)DIZZY_FALLBACK_SYSTEM_PROMPT_MAX_CHARS=3500(shrinks fallback system prompt)DIZZY_FALLBACK_USE_RAG=0(default; set 1 to include RAG snippets on fallback)
Helper script (sets User env vars + current session):
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\set_user_env_fallback_openai_compat.ps1
Helper script (sets User env vars + current session):
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\set_user_env_gemini.ps1
Minimal routing roles:
chat: normal replies and final judgment useDIZZY_CHAT_BACKEND.utility: janitorial work (/remember, auto-remember,/memory_review,/improve) usesDIZZY_UTILITY_BACKENDwhen set, otherwise the chat backend.fallback: transient primary failures can useDIZZY_CHAT_FALLBACK_BACKEND=openai_compat.
Keep this small. Start with one primary chat backend, one utility backend, and one fallback. Add providers only when a real task cannot be served through Gemini or an OpenAI-compatible endpoint.
Utility override examples:
DIZZY_UTILITY_BACKEND=openai_compatDIZZY_UTILITY_OPENAI_COMPAT_MODEL=<cheap-or-local-model-id>DIZZY_UTILITY_OPENAI_COMPAT_MAX_TOKENS=500
Routing choices are recorded in conversation JSONL entries as model_route for later review.
Groq helper (lists available model IDs using your Groq API key):
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\groq_list_models.ps1
If you get a Gemini HTTP 404 for the model id:
- This runtime will try to auto-pick a working model via
ListModels. - You can also list models yourself:
node .\scripts\gemini_list_models.mjs
The order reconciler no longer uses a shared placeholder image.
Before generate_qc or manual_delivery can progress for an order, stage a real prepared asset under:
runtime/orders/<order_id>/candidate.pngruntime/orders/<order_id>/candidate.json
Minimum metadata example:
{
"model": "manual_prepared_asset",
"refined_prompt": "final prompt used for the prepared asset"
}If the asset or metadata is missing, stale, or marked as placeholder-grade, the reconciler writes a diagnostic artifact and blocks upload/delivery.
If you want to use OpenRouter (including :free models), set:
DIZZY_CHAT_BACKEND=openai_compatOPENAI_COMPAT_BASE_URL=https://openrouter.ai/api/v1OPENAI_COMPAT_API_KEY=...OPENAI_COMPAT_MODEL=...(example:openrouter/freeorqwen/qwen3.6-plus-preview:free)- Optional:
OPENAI_COMPAT_MAX_TOKENS=200(caps response length)
Helper script (sets User env vars + current session):
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\set_user_env_openrouter.ps1