How to run context-intelligence-server and Neo4j as persistent services on
Linux (systemd) or macOS (launchd), with full authentication, and
integrated with the Amplifier CLI so sessions are automatically captured.
The fastest path to a local server (server v6.0.0): static API-key auth and
the runtime /admin/* identity-map API enabled. The server is headless
(API-only) — there is no browser dashboard; the OpenAPI docs at /docs are always
on. Every value below is a placeholder — substitute your own. The numbered
sections after this one explain each step in full.
1. Install uv and the server (one binary lands in ~/.local/bin):
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv tool install git+https://github.com/microsoft/amplifier-context-intelligence
# Pre-merge / a specific branch instead (note --force to overwrite an existing install):
# uv tool install --force "git+https://github.com/microsoft/amplifier-context-intelligence@feat/runtime-identity-map"
# A local checkout instead:
# uv tool install --force /path/to/amplifier-context-intelligence2. Start Neo4j (Docker) — APOC + GDS (Graph Data Science, Community edition), both required:
NEO4J_BOLT_PORT=37687 # bolt driver (standard would be 7687)
NEO4J_HTTP_PORT=37474 # browser UI (standard would be 7474)
NEO4J_PASSWORD="<your-strong-password>"
DATA_DIR="$HOME/amplifier-context-intelligence-server-data-store"
mkdir -p "${DATA_DIR}/neo4j"
docker run -d \
--name amplifier-context-intelligence-neo4j \
--restart unless-stopped \
--memory 24g --memory-swap 24g \
-p ${NEO4J_HTTP_PORT}:7474 -p ${NEO4J_BOLT_PORT}:7687 \
-e NEO4J_AUTH=neo4j/${NEO4J_PASSWORD} \
-e 'NEO4J_PLUGINS=["apoc","graph-data-science"]' \
-e 'NEO4J_dbms_security_procedures_unrestricted=apoc.*,gds.*' \
-e NEO4J_server_memory_heap_initial__size=10g \
-e NEO4J_server_memory_heap_max__size=10g \
-e NEO4J_server_memory_pagecache_size=10g \
-v "${DATA_DIR}/neo4j:/data" \
neo4j:5.26.22-communityMemory limits are not optional on a shared host. Without
--memoryand explicit heap/page-cache settings, Neo4j sizes itself from host RAM — on a 121 GB machine it grew to 22 GB and helped OOM the server. Keep--memory≥ heap + page cache + ~3–4 GB headroom, and scale all three to your box (the24g/10g/10gsplit above suits a host with ≥ 32 GB RAM). The double underscore inheap_max__sizeis required — misspell it and the setting is silently ignored. Rationale, verification commands, and the matching systemd guards: operational-hardening.md.
GDS (Graph Data Science): the startup plugin-installer resolves the GDS Community build matching this Neo4j release from the official compatibility matrix (2.13.x for 5.26.x) — https://neo4j.com/docs/graph-data-science/current/installation/supported-neo4j-versions/ This requires network egress at container start. Set only
NEO4J_dbms_security_procedures_unrestricted=apoc.*,gds.*(noallowlist, which would block built-indb.*/dbms.*procedures). Verify after start withdocker exec amplifier-context-intelligence-neo4j cypher-shell -u neo4j -p "$NEO4J_PASSWORD" "RETURN gds.version();". See local-development.md §1 for the local-dev variant.
3. Make two DISTINCT tokens — one for data capture, one for admin:
# Data token: clients/the hook send this RAW; the config stores only its digest.
DATA_TOKEN=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
DATA_DIGEST=$(python3 -c "import hashlib,sys; print(hashlib.sha256(sys.argv[1].encode()).hexdigest())" "$DATA_TOKEN")
# Admin token: the RAW value goes in admin_api_key and is the bearer for /admin/*.
# Keep it DISTINCT from the data token (see the created_by note in step 4).
ADMIN_TOKEN=$(openssl rand -hex 32)
echo "DATA token (use in the hook / clients): $DATA_TOKEN"
echo "DATA digest (goes into api_keys): $DATA_DIGEST"
echo "ADMIN token (bearer for /admin/* calls): $ADMIN_TOKEN"4. Write server-config.yaml — static mode, admin on, local
writable store paths. Create the dirs, then drop in the file:
mkdir -p "${DATA_DIR}/identity" "${DATA_DIR}/blobs" "${DATA_DIR}/logs" "${DATA_DIR}/queues"
mkdir -p ~/.config/context-intelligence# ~/.config/context-intelligence/server-config.yaml — LOCAL STATIC MODE
# --- Auth: static keystore (static is the default; shown for clarity) ---
auth_mode: static
api_keys:
"<DATA_DIGEST>": # sha256(DATA_TOKEN) from step 3 — the DIGEST, not the raw token
id: owner # the contributor id; surfaces as created_by on graph nodes
# --- Admin API (runtime identity-map management) ---
# A request bearing this RAW token may call /admin/*. Without it, /admin/* → 503.
# IMPORTANT: keep this DISTINCT from any data token in api_keys (see note below).
admin_api_key: "<ADMIN_TOKEN>"
# The server is headless (API-only). OpenAPI docs at http://localhost:8000/docs
# are always on and auth-exempt.
# --- Identity-map store files (LOCAL writable dir — default /data/... is not writable on a normal box) ---
api_keys_store_path: "/home/you/amplifier-context-intelligence-server-data-store/identity/api-keys.json"
entra_identities_store_path: "/home/you/amplifier-context-intelligence-server-data-store/identity/entra-identities.json"
# --- Neo4j (use bolt:// for Community Edition) ---
neo4j_url: "bolt://localhost:37687" # NEO4J_BOLT_PORT from step 2
neo4j_browser_url: "http://localhost:37474" # NEO4J_HTTP_PORT from step 2 (clickable link in the UI)
neo4j_user: neo4j
neo4j_password: "<your-strong-password>"
# --- Server bind + storage ---
server_host: 0.0.0.0
server_port: 8000
blob_path: "/home/you/amplifier-context-intelligence-server-data-store/blobs"
log_path: "/home/you/amplifier-context-intelligence-server-data-store/logs/server.jsonl"
queues_path: "/home/you/amplifier-context-intelligence-server-data-store/queues"
created_by="admin"caveat — why two tokens. The admin key is recognised by the middleware before the data-identity resolver, so any request bearing the admin token is attributedcreated_by="admin". If you reuse one token for both capture and/admin/*, your captured sessions are stampedadmininstead of your real contributor id. Use a distinctadmin_api_keyvs your dataapi_keysto preserve per-user attribution.
5. Run it — directly for a quick check (or install it as a service via §5/§6):
AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_CONFIG_FILE=$HOME/.config/context-intelligence/server-config.yaml \
context-intelligence-server6. Verify:
curl -sS http://localhost:8000/status | jq '.auth'
# → {"mode":"static","admin_api_enabled":true}
curl -sS http://localhost:8000/version
# → {"version":"6.0.0"}
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8000/events \
-H "Content-Type: application/json" -d '{}'
# → 401 (auth is enforced)Explore the API interactively at http://localhost:8000/docs (Swagger UI, always
on and unauthenticated); data endpoints still require the DATA token as
Authorization: Bearer <token>.
7. Use the admin API (static — the bearer is the ADMIN token):
# List current key entries (hash + contributor id; never raw keys):
curl -sS http://localhost:8000/admin/keys -H "Authorization: Bearer $ADMIN_TOKEN"
# Onboard a peer at runtime (register the HASH of their key, never the raw key):
PEER_HASH=$(printf '%s' "<peer-raw-key>" | sha256sum | cut -d' ' -f1)
curl -sS -X PUT "http://localhost:8000/admin/keys/$PEER_HASH" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"id": "<contributor>"}'Full onboarding/offboarding runbook and status-code matrix: identity-management.md.
curl -LsSf https://astral.sh/uv/install.sh | shEnsure ~/.local/bin is in your PATH:
export PATH="$HOME/.local/bin:$PATH" # add to ~/.bashrc or ~/.zshrcThe server needs Neo4j 5.x reachable over Bolt, with the APOC procedures available (and GDS for graph-analytics features). Install and run it locally per local-development.md §1. This service guide does not require Docker for Neo4j.
Install and start Neo4j 5.x locally with APOC (and GDS if you use graph-analytics features), following local-development.md §1 (Neo4j Desktop, or a package/tarball install with the plugin JARs). Authentication is always required — the server refuses to connect to an unauthenticated Neo4j instance, so set an initial password:
# Adjust these three values before running
NEO4J_HTTP_PORT=37474 # browser UI (standard would be 7474)
NEO4J_BOLT_PORT=37687 # bolt driver (standard would be 7687)
NEO4J_PASSWORD="<your-strong-password>"
DATA_DIR="$HOME/amplifier-context-intelligence-server-data-store"
mkdir -p "${DATA_DIR}/neo4j"
docker run -d \
--name amplifier-context-intelligence-neo4j \
--restart unless-stopped \
-p ${NEO4J_HTTP_PORT}:7474 \
-p ${NEO4J_BOLT_PORT}:7687 \
-e NEO4J_AUTH=neo4j/${NEO4J_PASSWORD} \
-e 'NEO4J_PLUGINS=["apoc","graph-data-science"]' \
-e 'NEO4J_dbms_security_procedures_unrestricted=apoc.*,gds.*' \
-v "${DATA_DIR}/neo4j:/data" \
neo4j:5.26.22-communityAPOC + GDS plugins:
-e 'NEO4J_PLUGINS=["apoc","graph-data-science"]'enables both. Neo4j 5.x auto-installs the bundledapoc-corejar at startup (from/var/lib/neo4j/labsinto/var/lib/neo4j/plugins) with no network fetch required — it re-installs on every container start. GDS is NOT bundled in the image, so the installer resolves and downloads the Community build matching this Neo4j release from the official compatibility matrix (2.13.x for 5.26.x) — this DOES require network egress at container start. Set onlyNEO4J_dbms_security_procedures_unrestricted=apoc.*,gds.*(do NOT set anallowlistofapoc.*,gds.*— it would block the built-indb.*/dbms.*procedures). Verify withdocker exec amplifier-context-intelligence-neo4j cypher-shell -u neo4j -p "${NEO4J_PASSWORD}" "RETURN apoc.version();"and"RETURN gds.version();". See local-development.md §1 for the local-dev variant. Hosted AuraDB has APOC Core preinstalled.Air-gapped hosts:
NEO4J_PLUGINS=["apoc","graph-data-science"]needs internet egress at startup for the GDS half (APOC alone works offline — it's bundled in the image at/var/lib/neo4j/labs/). For a host with no egress, build your own Neo4j image with the matching GDS jar baked into/var/lib/neo4j/plugins/at build time (the build step needs network; the resulting container runs egress-free), or serve the jar from an internal mirror. On a fully disconnected host, pre-load the base image too:docker save neo4j:5.26.22-community -o neo4j.taron a connected machine, thendocker load -i neo4j.taron the air-gapped host.
Wait for Neo4j to be ready (usually 15–30 seconds), then verify APOC:
cypher-shell -u neo4j -p '<your-strong-password>' "RETURN apoc.version();"Important: use
bolt://(notneo4j://) for the server connection URL. The routing protocol (neo4j://) fails on Community Edition single-node installs. Setneo4j_urltobolt://localhost:<NEO4J_BOLT_PORT>in your config.Neo4j exposes two ports: the bolt driver port used for all data operations, and the HTTP browser UI port used only for the Neo4j Browser web interface. Both must be configured separately —
neo4j_urlfor the driver connection,neo4j_browser_urlfor the Neo4j Browser link surfaced in the/statusresponse. Both are displayed verbatim from the config, so if Neo4j is on a remote machine, use that machine's hostname in both values.
uv tool install git+https://github.com/microsoft/amplifier-context-intelligenceOne binary is placed at ~/.local/bin:
| Binary | Purpose |
|---|---|
context-intelligence-server |
Runs the FastAPI server |
Upgrade later:
uv tool upgrade context-intelligence-serverThere is no init subcommand. Write server-config.yaml by hand from the
annotated template, then add authentication.
mkdir -p ~/.config/context-intelligence
curl -o ~/.config/context-intelligence/server-config.yaml \
https://raw.githubusercontent.com/microsoft/amplifier-context-intelligence/main/server-config.example.yamlEdit the file and set values for your machine — at minimum neo4j_url (use the
bolt:// scheme and your NEO4J_BOLT_PORT from Step 2), neo4j_browser_url
(your NEO4J_HTTP_PORT, surfaced in the /status response), and
neo4j_password. Configuration keys are grouped into three categories below.
Set up an API token so the server requires Authorization: Bearer <token>.
Generate a token, derive its SHA-256 digest, and add an api_keys entry (the
file stores the digest; clients send the raw token):
TOKEN=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
DIGEST=$(python3 -c "import hashlib,sys; print(hashlib.sha256(sys.argv[1].encode()).hexdigest())" "$TOKEN")
printf 'api_keys:\n "%s":\n id: owner\n' "$DIGEST" >> ~/.config/context-intelligence/server-config.yaml
echo "API token (use in Step 7): $TOKEN"Copy the API token — you need it in Step 7 (Amplifier settings), and it is not
recoverable from the config file afterward. The legacy single-key mode
(api_key: "<secret>") also still works. Full guide — adding/revoking/rotating
peers, the empty-{} hard-error rule, and the raw-token-vs-digest guardrail — in
managing-api-keys.md.
Prefer a one-shot bootstrap?
python scripts/prime-local-config.pygenerates theapi_keyskeystore and prints the token once, then writes a ready-to-useserver-config.yaml— see local-development.md §2.
Microsoft Entra JWT auth. Instead of pre-shared keys, the server also supports
auth_mode=entra, where clients authenticate with Azure AD bearer tokens and the server maps each token'soidto a contributor. Setauth_mode,azure_client_id,azure_tenant_id, andentra_identities(see the table below). Full guide: entra-auth-setup.md.
Runtime identity-map management (no restart). Adding or removing a key/identity by editing config requires a restart. To onboard/offboard at runtime, enable the admin API — set
admin_api_key(static mode) or theIdentityAdminApp Role (entra mode) — and use the/admin/*endpoints. Full runbook: identity-management.md.
These cover the three things a local static-mode run usually needs turned on or
pointed somewhere writable. All are plain server-config.yaml keys (or the
matching AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_* env var; env wins).
| Key | Example | Purpose |
|---|---|---|
admin_api_key |
(a raw admin token) | Enables the /admin/* runtime identity-map API in static mode. A request whose bearer token equals this value is recognised as admin — the middleware checks it before the data keystore. Unset/empty → every /admin/* call returns 503; a valid data key gets 403. The admin key cannot be deleted or shadowed via the API (409). Use a token DISTINCT from your data keys (see caveat). Env: …_ADMIN_API_KEY. |
api_keys_store_path |
(local writable path) | Durable JSON file backing the static sha256(key) → contributor map that /admin/keys edits. The default /data/identity/api-keys.json is not writable on a normal box — point it at your data store (e.g. …/identity/api-keys.json). It is seeded from api_keys on first boot, then this file is the source of truth for runtime edits. |
entra_identities_store_path |
(local writable path) | The entra equivalent (oid → contributor), used only in auth_mode=entra. Same default-not-writable caveat; set it to a writable path even in static mode if you want a clean layout. |
created_by="admin"caveat — use two distinct tokens. The admin key is recognised by the middleware before the data-identity resolver, so any request bearing the admin token is attributedcreated_by="admin". The session-capture hook uses a data token; if you reuse that same token as the admin key, your captured sessions are stampedadmininstead of your real contributor id. Keepadmin_api_keydistinct from your dataapi_keysto preserve per-user attribution.
Runtime onboarding/offboarding via /admin/keys (static) or /admin/identities
(entra), the full status-code matrix (401/403/409/422/503), and how the live map
stays fresh without a restart: identity-management.md.
| Key | Example | Purpose |
|---|---|---|
server_host |
0.0.0.0 |
Bind address. 0.0.0.0 = all interfaces; 127.0.0.1 = localhost only |
server_port |
8000 |
Listen port |
log_level |
INFO |
Verbosity (DEBUG / INFO / WARNING / ERROR) |
api_key |
(your secret) | Legacy single bearer token (folds to contributor id owner). All endpoints except /status and static routes require Authorization: Bearer <value>. The server verifies it as sha256(token). |
api_keys |
(map) | Per-contributor keystore: sha256_hex(token) -> {id: <contributor>}. The file holds digests; clients send raw tokens. api_keys: {} is a hard startup error (omit/null to disable auth). See managing-api-keys.md. |
| Key | Example | Purpose |
|---|---|---|
neo4j_url |
bolt://localhost:37687 |
Bolt/driver URL for all graph operations. Use bolt:// scheme. Port must match NEO4J_BOLT_PORT from Step 2. Surfaced verbatim in the /status response — use the address reachable by the server process, which may differ from what your browser can reach. |
neo4j_browser_url |
http://localhost:37474 |
Neo4j Browser HTTP UI URL. Port must match NEO4J_HTTP_PORT from Step 2. Surfaced verbatim in the /status response (neo4j_browser_url). Use the address reachable from your browser — if Neo4j is on a remote machine this will be that machine's hostname or IP, not localhost. Never used for driver connections. |
neo4j_user |
neo4j |
Auth username (legacy single-credential form). |
neo4j_password |
(your password) | Auth password. Always required — the server refuses an unauthenticated Neo4j. Must match the password you set on Neo4j (e.g. via neo4j-admin dbms set-initial-password). |
Optional — separate read/write credentials or URLs (two-client split). The four flat keys above are the legacy single-credential form and keep working unchanged. The
/cypherendpoint reads run through an internal read client; ingest and schema changes run through an admin/write client. To give them separate credentials (and optionally separate URLs, e.g. a read replica), replace the flat keys with a structuredneo4j:block:neo4j: admin: url: bolt://localhost:7687 username: neo4j password: "<write-password>" access_mode: WRITE # MUST be WRITE cypher_query: url: bolt://localhost:7687 # or a read replica URL username: reader password: "<read-password>" access_mode: READ # MUST be READ — default is WRITE, so this is requiredWhen the
neo4j:block is present both sub-clients are required, andaccess_modeis validated at startup (adminMUST beWRITE,cypher_queryMUST beREAD) — a wrong or missingaccess_modeis a hard boot failure. Full migration guide and the Community-vs-Enterprise enforcement caveat: auth-troubleshooting-and-upgrades.md § "Neo4j two-client split".
| Key | Example | Purpose |
|---|---|---|
blob_path |
~/amplifier-context-intelligence-server-data-store/blobs |
Event payload storage (binary blobs from tool outputs) |
log_path |
~/amplifier-context-intelligence-server-data-store/logs/server.jsonl |
Structured JSONL server log |
queues_path |
~/amplifier-context-intelligence-server-data-store/queues |
Durable per-session ingest queues (.log/.offset/.dead.jsonl) |
DATA_DIR="$HOME/amplifier-context-intelligence-server-data-store"
mkdir -p "${DATA_DIR}/blobs" "${DATA_DIR}/logs" "${DATA_DIR}/queues"
⚠️ Single-instance invariant — run the server ONLY via this service. Exactly onecontext-intelligence-serverprocess may serve a given(server_port, neo4j_url)at a time. Once it is installed as a service, start, stop, and restart it only throughsystemctl(or launchd on macOS). Do not also launch it by hand —uv run uvicorn …,python -m uvicorn …, or a binary from a stray venv (/opt/…, a leftover.venv). The--reloaddev command in the README is for throwaway local boxes only.Why it matters: extra copies silently bind-race the same port and share the same Neo4j + queue store. The kernel gives the socket to one; the rest keep running as orphans on possibly-older code, still draining events into the same graph. Real incidents this caused: an old orphan re-stamping
created_by="admin"after an auth fix was deployed (corrupting attribution), and the event hook getting spurious HTTP 401 floods from an orphan with a stale keystore. When behavior contradicts the code you just deployed, the question is "which running copy answered this?" —ss(who holds the socket) beatsps(who's merely alive).Restart safely and verify a single listener:
sudo systemctl restart context-intelligence-server.service # or: systemctl --user restart … ss -ltnp | grep ':8000' # expect ONE pid (+ its worker), nothing else ps -eo pid,args | grep 'context_intelligence_server.main:asgi_app' | grep -v grep # expect NONE outside the service # kill -TERM <pid> any orphan found, then re-check; SIGKILL only if it lingers curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8000/events -d '{}' # → 401 = up + auth enforcedIf orphans exist, fix whatever launched them (a manual
uv run, an old deploy script, a duplicate unit) so this service is the only entry point.
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/context-intelligence-server.service << 'EOF'
[Unit]
Description=Context Intelligence Server
After=network.target
# Crash-loop guard: refuse to start again after more than 5 starts within
# 10 minutes, instead of restarting forever. These two directives belong in
# [Unit], NOT [Service] -- systemd silently ignores them in the wrong section
# (they are documented in systemd.unit(5)). See docs/operational-hardening.md §3.
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
Type=simple
ExecStart=%h/.local/bin/context-intelligence-server
Environment=AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_CONFIG_FILE=%h/.config/context-intelligence/server-config.yaml
Restart=on-failure
RestartSec=5s
# Memory ceiling: MemoryHigh throttles/reclaims, MemoryMax is the hard wall.
# Without these the unit runs at MemoryMax=infinity and a crash-recovery boot
# over a large queue spool can OOM the whole host. Size to your machine --
# these values suit a host with >= 32 GB RAM. See docs/operational-hardening.md §3.
MemoryHigh=20G
MemoryMax=24G
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
EOF%h is systemd's specifier for the user home directory — no hardcoded paths.
Verify the guards actually took.
systemd-analyze verifyonly warns about a misplaced directive (it exits 0 either way), andMemoryMaxon a--userunit is enforced only if the memory controller is delegated to your user slice:systemd-analyze verify --user ~/.config/systemd/user/context-intelligence-server.service # silence = good; "Unknown key name 'StartLimitIntervalSec' in section 'Service'" = misplaced cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers # must include: memory systemctl --user show context-intelligence-server -p MemoryHigh -p MemoryMax # → MemoryHigh=21474836480 / MemoryMax=25769803776 (NOT "infinity")Full rationale, the incident that motivated these guards, and the Neo4j-side limits: operational-hardening.md.
systemctl --user daemon-reload
systemctl --user enable context-intelligence-server
systemctl --user start context-intelligence-serverloginctl enable-linger $USERsystemctl --user status context-intelligence-server
journalctl --user -u context-intelligence-server -fThe repository ships a plist template at
service/macos/com.context-intelligence.server.plist.template. Expand it
with sed (launchd does not expand ~ in paths):
mkdir -p ~/Library/LaunchAgents
sed "s|HOME_DIR|$HOME|g" \
/path/to/repo/service/macos/com.context-intelligence.server.plist.template \
> ~/Library/LaunchAgents/com.context-intelligence.server.plist
launchctl load ~/Library/LaunchAgents/com.context-intelligence.server.plist# Check status
launchctl list | grep context-intelligence
# Stop
launchctl unload ~/Library/LaunchAgents/com.context-intelligence.server.plist
# Logs
tail -f ~/.local/share/context-intelligence/logs/server.stdout.log
tail -f ~/.local/share/context-intelligence/logs/server.stderr.logThe amplifier-bundle-context-intelligence bundle hooks into the Amplifier
CLI and forwards all session events to the server in real time.
Add it to the app list in ~/.amplifier/settings.yaml:
bundle:
app:
- git+https://github.com/microsoft/amplifier-bundle-context-intelligence@mainOr use the CLI:
amplifier bundle add git+https://github.com/microsoft/amplifier-bundle-context-intelligence@mainAdd the server URL and the API token (the raw token you saved in Step 4) to
~/.amplifier/settings.yaml:
overrides:
hook-context-intelligence:
config:
context_intelligence_server_url: "http://localhost:8000"
context_intelligence_api_key: "<api-key-from-step-4>"| Key | What it does |
|---|---|
context_intelligence_server_url |
URL of the running server. The hook POSTs all session events here. |
context_intelligence_api_key |
Raw bearer token. The server verifies it as sha256(token) against its keystore (a legacy api_key, or an api_keys entry). If this token is missing or wrong, the hook logs a warning once and disables HTTP dispatch for the session. |
bundle:
active: <your-active-bundle>
app:
- git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main
# ... other bundles ...
overrides:
hook-context-intelligence:
config:
context_intelligence_server_url: "http://localhost:8000"
context_intelligence_api_key: "<api-key-from-step-4>"# Health check (always unauthenticated)
curl http://localhost:8000/status
# → {"status":"ok","neo4j_connected":true,"neo4j_query_connected":true,"neo4j_url":"bolt://localhost:37687","neo4j_browser_url":"http://localhost:37474",...}
#
# Both neo4j_url and neo4j_browser_url are read verbatim from server-config.yaml.
# If Neo4j is on a remote host the response will show those remote addresses.
# Confirm auth is enforced — must return 401
curl -s -o /dev/null -w "%{http_code}" \
-X POST http://localhost:8000/events \
-H "Content-Type: application/json" -d '{}'
# → 401If neo4j_connected is false, check:
- Neo4j is running (e.g.
neo4j status, or check Neo4j Desktop) - Bolt port in
server-config.yamlmatches the port Neo4j listens on (and the URL uses thebolt://scheme) - Neo4j password in config matches the password you set on Neo4j
API docs: the server is headless (no browser dashboard). Explore the API
interactively at http://localhost:8000/docs (Swagger UI) — always on and
unauthenticated. The raw spec is at http://localhost:8000/openapi.json. Data
endpoints still require the API token from Step 4 as Authorization: Bearer.
| Symptom | Cause | Fix |
|---|---|---|
command not found: context-intelligence-server |
~/.local/bin not in PATH |
Add export PATH="$HOME/.local/bin:$PATH" to your shell profile |
neo4j_connected: false |
Bolt port mismatch or wrong scheme | Set neo4j_url to bolt://localhost:<NEO4J_BOLT_PORT> in config — must use bolt:// not neo4j:// |
neo4j_connected: false (connection refused) |
Neo4j not running | Check with neo4j status (or Neo4j Desktop); start it with neo4j start |
neo4j_query_connected: false |
The read/cypher_query driver is not connected (ingest via the admin driver may still work) | Check the cypher_query URL/credentials and, if using a cluster/read-replica, its reachability |
neo4j_browser_url in /status points at the wrong host/port |
neo4j_browser_url misconfigured |
Update neo4j_browser_url in server-config.yaml to the address reachable from your browser — if Neo4j is remote, use the remote hostname, not localhost |
| Service starts then immediately stops | Config file missing or bad path | journalctl --user -u context-intelligence-server to see the error |
Permission denied on blob/log path |
Directories don't exist | mkdir -p the paths listed in your config |
| Port 8000 already in use | Conflict with another process | Change server_port in config and update the systemd unit |
| Linux: service doesn't start on boot | Lingering not enabled | loginctl enable-linger $USER |
| macOS: plist loaded but service not running | launchd silently failed | Check server.stderr.log for startup errors |
| Events stop dispatching, circuit breaker tripped | context_intelligence_api_key missing from ~/.amplifier/settings.yaml |
Add context_intelligence_api_key: "<key>" under overrides.hook-context-intelligence.config |
Data attributed to the wrong created_by, client gets HTTP 401 floods, or behavior contradicts the deployed code |
Multiple server instances running at once (an orphan from a manual uv run/old deploy racing the service on the same port + Neo4j — see the single-instance invariant at the top of §5) |
ss -ltnp | grep ':8000' and ps -eo pid,args | grep asgi_app | grep -v grep; kill -TERM every process not owned by the service manager, then sudo systemctl restart context-intelligence-server.service and re-verify a single listener. Fix whatever launched the extra copies so the service is the only entry point. |
Graph silently stops updating while clients still get 202 from POST /events |
The drainer is not draining — usually the server is crash-looping (OOM) or Neo4j is unreachable. 202 only means the event was durably appended, never that it reached the graph. |
curl -s http://localhost:8000/status and check metrics.in_queue_total (climbing?), metrics.degraded, orphaned_sessions, and each session's last_successful_flush; then du -sh <queues_path> and systemctl --user show context-intelligence-server -p NRestarts -p Result. Full symptom/command table: operational-hardening.md §6 |
| Service restarts every few minutes forever, serving nothing; host memory pressure | Unbounded crash loop. Restart=on-failure with no start limit + MemoryMax=infinity + a large queue spool: crash recovery OOMs mid-startup and restarts before it can ever finish. A real incident burned 9.5 CPU-hours over two days this way. |
Add StartLimitIntervalSec/StartLimitBurst (in [Unit]) and MemoryHigh/MemoryMax (in [Service]) — see §5 — cap Neo4j's memory, and archive the oversized spool out of the boot path. Full procedure: operational-hardening.md |
| Neo4j process grows to many GB / crowds out the server | No container memory limit and no explicit heap — Neo4j auto-sizes from host RAM | docker update --memory 24g --memory-swap 24g amplifier-context-intelligence-neo4j && docker restart amplifier-context-intelligence-neo4j (the JVM only re-sizes at startup), then set explicit heap/page-cache env vars — see Step 2 and operational-hardening.md §2 |
Server refuses to boot / crash-loops with RuntimeError: Neo4j graph has N node(s) lacking the :Node label (un-migrated). Cold start refuses to boot to avoid duplicating them on write. Run: context-intelligence-server doctor --fix (or an equivalent :Node constraint data-conflict error) |
Un-migrated graph. Cold start no longer self-heals untagged/duplicate legacy nodes, and it now FAILS LOUD on either shape (behavior change — see the README upgrade note): nothing has been written yet at boot, so refusing to start is the safest point to surface the problem. The mid-flight flush path is unaffected and still self-heals if a conflict is only discovered after the server was already serving. | Run context-intelligence-server doctor --fix to repair the graph, then restart the service |
The server speaks plain HTTP; terminate TLS in front of it. In production, the
Azure Container Apps platform edge handles HTTPS
automatically. For a self-hosted run that needs TLS, put your own reverse proxy
(nginx, Caddy, etc.) in front of the server and point it at
http://localhost:8000, then update settings.yaml with the HTTPS URL — same
pattern as the Azure deployment guide.