The fourth 1.0 release candidate: the 1.0.0rc3 code plus one BaseCacheStore behind everything the API shares between workers (run locks, caps, idempotency, token revocation, rate limiting), configured by one cache_store object in 10xgraph.json, a lighter 10xgraph init, and a startup check that no longer refuses to boot over a Redis nothing needs.
pip install -U --pre "10xgraph-api==1.0.0rc4"This pulls 10xgraph 1.0.0rc4. 10xgraph-client stays at 1.0.0-rc.2 (no client changes).
BaseCacheStorereplaces three extension points.ConcurrencyLimiter,TokenRevocationBackend(with its in-memory and Redis variants andbuild_token_revocation) andBaseRateLimitBackend(with its memory and Redis variants) are removed, with no aliases. If you wrote your own, implementBaseCacheStoreinstead.cache_storeis one object in10xgraph.json:{"type": "memory" | "redis" | "custom", "url": "${REDIS_URL}", "path": "module:attr", "rate_limit": {...}, "token_revocation": true}. Nocache_storemeans the in-memory store.- Top-level
redis,rate_limitandtoken_revocationkeys are config errors. The error says where each setting goes: insidecache_store. REDIS_URLno longer selects the store. Write"cache_store": {"type": "redis"}; with nourlit readsREDIS_URL. Typerediswithout theredispackage refuses to boot withpip install "10xgraph-api[redis]"./health/readyreportscache, notredis. Update monitors that readchecks.redis.- Rate-limit counters reset once. The Redis keys moved to the
10xgraph:ratelimit2namespace. Other Redis key layouts are unchanged. 10xgraph initasks only the agent name, template and AI provider. Production writes the recommended setup (jwt auth, ownership authorization, uuid ids, a Rediscache_storewith rate limiting and token revocation); Quick Start writes nocache_store. Flags such as--authand--cache-storeoverride parts of it for scripts.thread_name_generatortakes"random"(the default),"first_message","none"or"module:attr". With the key unset, new threads are now named and the name is saved.GraphConfig.thread_name_generator_pathis replaced byGraphConfig.thread_name_generator.- Requires
10xgraph1.0.0rc4. A custom checkpointer must implementaping().
Added
BaseCacheStore,RedisCacheStore(also Valkey, Dragonfly, KeyDB) andInMemoryCacheStore, exported fromtenxgraph_api, plus a contract suite for custom stores intenxgraph_api.testing.cache_store_contract.id_generatorin10xgraph.json(uuid,hex,short,int,bigint,timestamp,snowflakeor"module:attr"), used for new thread, message and memory ids.thread_name_generator: "first_message", and a token revocation section in10xgraph config.- One INFO line at boot names the cache store and the config key it came from.
Changed
- The startup check is fatal for the cache store only when a fail-closed feature uses it (token revocation with auth, or rate limiting with
fail_open: false). Otherwise an unreachable cache store logs one warning, the server starts, and/health/readyreportscache: failwith status 200. - Readiness and the startup check only call
aping()on the bound checkpointer, store and cache store. 10xgraph revokewrites to the cache storecache_storeselects and refuses when it is the in-memory store.- Shutdown closes the cache store once, after the graph and pending lease releases.
- The in-memory cache store keeps expiries on the monotonic clock, so a wall clock jump no longer expires a held thread lock early.
Fixed
- A leftover
REDIS_URLno longer blocks boot when nothing configured needs Redis (rc3 refused to start an in-memory server in production with a staleREDIS_URL). - A rate-limit denial stays a 429 when reading the cache store clock for
Retry-Afterfails. revoke_userrounds the cutoff up to the microsecond, so a token issued at the cutoff itself is always revoked.
See CHANGELOG.md for the full list. The rc3 notes follow.
The third 1.0 release candidate: the 1.0.0rc2 code plus a startup check for every configured backend, a clear 503 when the rate limiter's backend is down, and a CORS fix so the hosted playground can reach a local 10xgraph dev server.
pip install -U --pre "10xgraph-api==1.0.0rc3"This pulls 10xgraph 1.0.0rc3. 10xgraph-client stays at 1.0.0-rc.2 (no client changes).
- The server refuses to start while a configured backend is unreachable, in development and production. It checks the checkpointer's Postgres (query and schema version) and Redis, the memory store (
aping(), now required in10xgraph1.0.0rc3), the API's Redis and the rate limiter's Redis. It retries forSTARTUP_CHECK_TIMEOUT_SECONDS(default 30), then exits naming each failed backend and itshost:port(no credentials). SetSTARTUP_CHECKS=falseto skip it. - A rate-limiter backend outage answers 503, not 429. With
fail_open: falseand Redis unreachable, requests get503 RATE_LIMIT_UNAVAILABLEwithRetry-After: 5. A real limit hit is still429 RATE_LIMIT_EXCEEDED. Retry on 503 as well as 429. - Running
10xgraph devon your machine against Docker backends: publish Postgres, Redis and Qdrant on127.0.0.1and uselocalhostin.env. The compose service names only resolve inside the Docker network, and the startup check now reports this at boot.
- The hosted playground (
https://play.10xgraph.com) can reach a local10xgraph devserver: CORS answers the browser's private-network preflight for origins inORIGINS(before, it answered 400 and the browser reported a CORS error). Chrome also asks the user to allow access to the local machine; the playground now shows the steps. - A custom rate-limit backend can signal an outage with
RateLimitDecision.backend_unavailable(). - The Redis rate-limit backend logs one traceback per outage instead of one per request.
See CHANGELOG.md for the full list. The rc2 notes follow.
The second 1.0 release candidate: it is the 1.0.0rc1 code plus graceful shutdown, stricter request validation, 409 answers for writes during a run, idempotency keys, JWT key rotation and a redesigned eval and config experience.
This is a release candidate. Pip skips it unless you pass --pre. It requires 10xgraph 1.0.0rc2, and its version is aligned with core 10xgraph and 10xgraph-client (1.0.0-rc.2). This release has breaking changes. Read "Before you upgrade" first.
pip install -U --pre "10xgraph-api==1.0.0rc2"This pulls 10xgraph 1.0.0rc2.
If you are on 1.0.0rc1, this section is all that is new. If you are coming from 0.9.x, read it and then "Upgrading from 0.9.x" below.
- AG-UI and
/v1/graph/livenow followruns.thread_idslike REST. Under the default"server", a thread id the server never issued gets 403 onPOST /v1/ag-ui, and an error event (not_authorized, close code 1008) on live. A blank or"new"id, or none, makes the server create the thread: AG-UI returns it inRUN_STARTED, and live sends{"type": "session_started", "thread_id": "<id>"}first. Frontends that make their own ids (CopilotKit does by default) must set"runs": {"thread_ids": "client"}in10xgraph.json. - Ids must match
^[A-Za-z0-9._:@-]{1,128}$(thread_id,message_id, memory ids, WebSocket and AG-UI thread ids). Others get 422VALIDATION_ERROR. Snowflake integers and UUIDs fit. - Nested
initial_state,configandresumedeeper than 64 levels get 422, and client dicts and lists are size-capped (1000 keys per object, 10000 items per list, 200000 values in all). - Resume errors answer 409 or 422. A wrong interrupt id, a resume sent to a thread that is not paused, or a plain message sent to a thread paused at
interrupt()returns 409 (before the stream starts on/v1/graph/stream). A resume value that fails the interrupt'sresponse_schemareturns 422. - Writes during a run return 409.
PUT/DELETEthread state,POSTmessages,DELETEof a message or thread, andPOST /v1/graph/fixare refused with 409 while a run holds the thread. Retry after the run ends or stop it first. - Thread delete can return
success=falsewithdata.cleanup_failed(files,traces,cache) when a cleanup step failed. The thread rows are deleted; treat it as "retry the cleanup". JWT_ALGORITHMmust be on an allowlist (HS/RS/ES/PS 256/384/512 andEdDSA), PyJWT must be installed when"auth": "jwt", andauthmust be exactly"jwt".- Other error and format changes: the
graph:setupscope is gone; the duplicateAGENTFLOW_VALIDATION_ERRORcode is no longer sent (useVALIDATION_ERROR); a recursion limit answers 422GRAPH_RECURSION_LIMIT, not 500;POST /v1/graph/streamis labelledapplication/x-ndjsonand sends a blank keep-alive line every 15 s (update proxy rules that matchedtext/event-stream). - Uploads: images over 50,000,000 pixels and PDFs over 10 MB or 500 pages get 413.
10xgraph buildrefuses--docker-composetogether with--k8s; run it once per target.IS_DEBUGis removed. Debug followsMODE. The/v1/evals/runsendpoints are removed, and10xgraph evalwrites the JSON report (and now an HTML report, see below) with no--openflag.- Postgres:
10xgraph1.0 runs schema migration v4 on first use. Stop all old pods, run the migration (one new pod), then start the rest. A rollback needs a database restore./health/readyfails while the schema is older than the code expects. - Dependency minimums:
python-multipart>=0.0.31,starlette>=1.3.1,PyJWT>=2.15.0(jwtextra),opentelemetry-apiandopentelemetry-sdk>=1.45.1(otelextra). The core floor is10xgraph>=1.0.0rc2,<2.0. - Gunicorn and shutdown: the generated command uses the worker class
tenxgraph_api.src.app.uvicorn_worker.TenxGraphUvicornWorker. Regenerate Docker and Kubernetes files with10xgraph build, or point your own command at it. Keep Gunicorn--graceful-timeoutabove the sum of the threeSHUTDOWN_*_SECONDSsettings (50 s by default) and the pod grace period above that. - Alerting: handled client errors log one WARNING line, not an ERROR with a traceback. Alerts that matched ERROR for 4xx must match WARNING.
Shutdown and reliability
- Graceful shutdown:
SHUTDOWN_GRACE_SECONDS(10),SHUTDOWN_STOP_SECONDS(10) andSHUTDOWN_TIMEOUT_SECONDS(30). Cut runs are saved INTERRUPTED and the thread resumes on the next request. While closing, runs answer 503SERVICE_UNAVAILABLEwithRetry-After,/health/readyreportsdraining, and an open HTTP stream ends with a chunk that hasdata.interrupted: true. - HTTP run streams end with
{"event": "updates", "data": {"status": "done"}}, so a client can tell a finished stream from a cut one. Retry-Afteron 429 and 409, typed error envelopes for unhandled errors, 503 for database or Redis connection errors, and 429PROVIDER_RATE_LIMITEDfor model provider rate limits.- Redis timeouts and pool settings (
REDIS_SOCKET_TIMEOUTand others) andREDIS_KEY_PREFIXfor deployments that share a Redis. Leases and revocation use the Redis clock. - A stored thread at a missing node answers 409 (
GRAPH_NODE_MISSING_001); state from a newer10xgraphanswers 500 (STATE_FORMAT_001). - Fixes: stop and shutdown now reach a REST stream after its first chunk, a disconnected stream frees its thread only after the run is saved, each realtime connection gets its own
LiveAgent, and resuming a thread paused atinterrupt()works on Postgres.
New and opt-in
- Idempotency keys (
Idempotency-Keyheader, oridempotency_keyin the WebSocket run frame) so a gateway retry does not run tools twice. - JWT key rotation (
JWT_SECRET_KEYS) and a maximum token age (JWT_MAX_TOKEN_AGE_SECONDS). 10xgraph revokerevokes one token or all of a user's tokens without a restart.- Media limits (per-user files and storage, image pixels, PDF size and pages, inline media size),
GET /v1/files, paging fields on lists, a run trace span tree, andrealtime_optionsin graph details. 10xgraph evalwrites an HTML scorecard next to the JSON report and opens it (use--no-opento skip), prints live progress lines, and--thresholdnow exits 1 only when the pass rate is below it.10xgraph init --template productionscaffolds JWT, ownership authorization, Redis rate limiting and aPgCheckpointer;10xgraph build,10xgraph configand10xgraph auditwarn about missing auth, authorization or rate limiting. The10xgraph configeditor is redesigned.
Security
- Ownership checks on
file_idmedia blocks for message and state writes, percent-encoded Redis keys, secrets redacted from logs and validation errors, and uploads authenticated before the body is parsed.
Everything below was the 1.0.0rc1 guidance and still applies when you jump from 0.9.x to rc2. Where rc2 changes an rc1 item, the rc2 section above wins.
runs.thread_idsdefaults to"server"(was"client").- REST and WebSocket clients that invent a thread id for a new thread now get 403 on
/v1/graph/invoke,/v1/graph/stream,/v1/graph/wsand the checkpointer write routes. - Omit the id on the first request and use the
meta.thread_idthe server returns from then on ("new"over/v1/graph/ws), or set"runs": {"thread_ids": "client"}in10xgraph.json. - AG-UI and
/v1/graph/liveneed no change in rc1, but rc2 enforces this setting there too. Only the built-in ownership and rbac backends refuse.
- REST and WebSocket clients that invent a thread id for a new thread now get 403 on
- Production deployments with the default or wildcard
ALLOWED_HOSTorORIGINSrefuse to start.- Set explicit values, or
STRICT_PRODUCTION_CHECKS=false. - A public, token-less API that runs in production with
ORIGINS=*andCORS_ALLOW_CREDENTIALS=falsealso needs the opt-out.
- Set explicit values, or
- Production runs longer than 300 s are cancelled (invoke 504, stream and AG-UI error). Raise
runs.timeout, or set it tonull. - An unknown
MODEnow refuses to start. Use one ofproduction,prod,development,dev,local(case-insensitive). Values such asstagingortestfail at startup, and in10xgraph dev. - Requests over the size limits get 422: 500 messages, 100 blocks per message, 1,000,000 characters of text. On AG-UI the limits apply to the trailing turn, with a 10,000-message cap on history.
POST /v1/threads/{thread_id}/messagesis now admin-only.- It needs the
checkpointer:importscope held explicitly: an identity with no scopes gets 403, and a"*"role has it. - Send normal messages through invoke or stream, and use the new
PUT .../messages/{message_id}for feedback.
- It needs the
- Assistant and system messages, and
context_summary/execution_meta, are rejected with 422 on the client routes. - Custom authorization backends are isolated by default. A backend that implements only
authorize()now gets per-owner storage isolation. If you relied on sharing, adddef isolation_scope(self) -> str: return "none". - JWTs with an invalid
user_idor scope claim are refused with 401.user_idmust be a non-empty string or an integer. Null, empty, boolean, float, list and dict are rejected.- A
scope/scopesclaim that is not a string or a list of strings is rejected. Fix the issuer. - A string
scopesis now split on spaces instead of matched by substring.
observability.user_id_modein10xgraph.json("raw"default,"hash","mask") controls how the user id appears in traces.on_busyrequest option ("wait"default, or"cancel") on invoke, stream andWS /v1/graph/ws. With"cancel", a run that finds its thread busy asks the running run to stop, then runs.- Uploaded files live and die with the messages that use them. Deleting the last message or the thread that uses a file deletes the file.
media.delete_with_thread(defaulttrue) turns the automatic delete off. DELETE /v1/files/{file_id}deletes a file you own, behind the newfiles:deletescope.PUT /v1/threads/{thread_id}/messages/{message_id}sets feedback on a message.interrupt_idon invoke, stream and WebSocket input, so a resume can name the interrupt it answers. WebSocket input also acceptsresumeagain.RequirePermission(..., explicit_scope=True)and thecheckpointer:importscope.
- Handled client errors log one WARNING line instead of an ERROR with a traceback, and Sentry no longer sends request bodies. Alerts that matched ERROR for 4xx must match WARNING instead.
- AG-UI context, tools and forwarded props are server config, and
ag_uiis a reserved key that other routes drop. - Built-in state fields (
context_summary,execution_meta) are never client-writable (422; AG-UI drops them), client messages are user messages only, forged tool results are refused, and the server generates message ids on invoke, stream, WebSocket andPUT state(AG-UI keeps client ids). - Thread ids are server-made by default (
runs.thread_ids: "server"; operators can set"client"), so nobody can squat an id or probe whether one exists. AG-UI and/v1/graph/livestill accept frontend-made ids. - A client can no longer pick the vector store collection on a graph run:
collectionis a server-owned config key and is ignored if sent. - Clients can no longer set
file_tool_root,workspace_root,tool_timeoutornode_timeoutin requestconfig; set them in your graph instead. /v1/graph/livere-checks the token during the session, and HTTP routes take the bearer token only from theAuthorizationheader.- One run at a time per thread, and external media URLs, upload types and document text extraction are checked or bounded.
- Production hides raw error text on state, stream, WebSocket and AG-UI, and OTEL no longer prints spans to stdout.
- Generated deployment files are locked down by default, and
10xgraph initwrites a.gitignorethat keeps.envand credentials out of git.
- Client mistakes on thread routes are a 404 or 422, not a 500, and state and message responses no longer carry provider or framework internals.
- Development error responses for node and graph errors are no longer a 422
VALIDATION_ERROR. POST /v1/graph/fixno longer deletes healthy tool-call messages.
See CHANGELOG.md for the full list.