Changelog¶
All notable changes to Strata will be documented in this file.
Entries focus on user-visible changes and release framing rather than exhaustive commit history.
The authoritative copy of this file lives at CHANGELOG.md in the repo root; this docs page mirrors it. Maintainers: keep the two in sync when editing.
0.10.0 - 2026-10-13¶
A hardening release. The server stops doing store and blob work on its event loop, so one slow database call or object-store read no longer stalls every other request. Uploads have a size limit, cells stop when the server that started them dies and can be cancelled from the CLI, REST and MCP, and several races between concurrent writers are closed in the database itself. The server image is now published per release.
One slow call no longer stalls the server. Route handlers, the embedded build runner, API-key checks, health probes and notebook cell runs now make their metadata-store and blob calls on worker threads. Those threads share one limit of 40, and the Postgres pool is sized to match, so a request waits for a thread, never 30 seconds for a connection.
Uploads are bounded. Every artifact upload route counts the body as it
arrives against STRATA_MAX_UPLOAD_BYTES (10 GiB by default) and streams it to
disk instead of holding it in memory. Metadata parts and JSON bodies have a
16 MiB limit of their own.
Cells stop with the server, and on request. A cell subprocess, an R cell, a
warm-pool worker and an ssh tunnel now die when the server process that started
them dies, instead of holding CPU, GPU and memory with nobody to read the
result. A running cell can be cancelled through
POST /v1/notebooks/{id}/cells/{cell_id}/cancel, strata cell cancel and the
MCP cancel_cell tool, not only from the WebSocket.
Concurrent writers agree. A named upload becomes ready in the same commit as its name, an alias approval and a rejection of one change cannot both succeed, a version has at most one live publication, and one tenant holds an artifact id, checked inside the write. These hold across processes sharing one store, such as several servers on one Postgres.
Easier to deploy. The server image, with the mcp extra, is pushed to
ghcr.io/bearing-research/strata:<version> with each release. A personal
server's worker registry can now be edited through the admin routes, and the
strata-pool RunPod backend moves to RunPod's REST v2 ahead of v1's retirement.
Upgrading from 0.9.0:
- Back up first. The artifact store's metadata migrates on first start (schema
version 10), one way, and 0.9.0 refuses to open a store this release has
migrated. A notebook's own store migrates the first time a command opens it.
The migration keeps the oldest active publication of any version that had
several and revokes the rest, recording each as a
withdrawevent, so a duplicate link handed out before the upgrade answers 410. (#1117) - Every cached notebook cell runs once more after upgrading, then caches as
before: a cell's environment hash now always uses the lock's runtime
packages and their sources, and now includes the lock's
requires-python. (#1097, #1144) - A SQL cell's result now always reaches downstream cells as a
pyarrow.Table. Before, it arrived as a pandas DataFrame whenever pandas was installed. Code that used it as a DataFrame needs.to_pandas(). (#1098) - On Postgres, each server process can open up to 41 connections (was 16).
Size
max_connectionsfor that times the number of nodes. (#1119) - New upload limit,
STRATA_MAX_UPLOAD_BYTES(10 GiB by default), onPUT /v1/artifacts,PUT /v1/artifacts/by-provenance,POST /v1/artifacts/import,PUT /v1/artifacts/import/blobs/{content_sha256}andPOST /v1/artifacts/upload/{artifact_id}/v/{version}. A larger body, including a chunked one with noContent-Length, gets a 413. (#1089) - A form's metadata part, a staged import's JSON body, and the whole JSON body
of
PUT /v1/artifactsare held to 16 MiB (orSTRATA_MAX_UPLOAD_BYTES, if lower), with a 413 that names the metadata limit. Send large data toPUT /v1/artifactsas multipart Arrow, which streams to disk. (#1095, #1145) PUT /v1/artifacts/by-provenancerefuses anartifact_idthat names a path (one containing/or\, or a..segment) with a 400, and the local blob store refuses any key whose file would leave its directory. (#1142)- A named
PUT /v1/artifactswhose name cannot be written now fails with a 500 and leaves the version building. Before, it returned success with no name. (#1115) - The Docker docs, the
Dockerfileexample and both compose files publish the port on127.0.0.1only, since the image runs personal mode, which runs cells for anyone who reaches it. To reach your own container from another machine, publish it on every interface yourself (-p 8765:8765). (#1142) - The server image is published to
ghcr.io/bearing-research/strata:<version>(linux/amd64) with each release, andlatestfollows stable releases. It now includes themcpextra, soSTRATA_MCP_ENABLED=truemounts/mcpin a container. (#1113) - A personal server's worker registry now changes through
/v1/admin/notebook-workers, which no longer answers 409 outside service mode.notebook_workers.jsonis still read only on first start. (#1112) - strata-pool's RunPod backend calls RunPod's REST v2 at
https://api.runpod.io/v2(REST v1 retires on 2026-11-15). A machine type with no GPU now gets thecpu3gCPU flavor, with its CPU count rounded up to a power of two from 2 to 32. Agpuorcpublock inprovider_optionsis merged into the one Strata builds instead of replacing it, and anenvinprovider_optionsmust be a map. RunPod's v2 refuses fields it does not know with a 422, so rename v1 fields inprovider_options(cloudTypeis nowcloud,imageNameisimage,containerDiskInGbisdisk, andgpuTypeIdsplusgpuCountbecomegpu: {id, count}). (#1137) - A cell on a RunPod worker must finish within 100 seconds: RunPod's HTTP proxy
ends the request after that with a 524, whatever
job_timeout_secondssays. Use the Docker or Fly backend for longer cells. (#1141) - Service mode: a guarded
@fetchnow goes throughHTTPS_PROXY,HTTP_PROXYorALL_PROXYwhen one covers the URL andNO_PROXYdoes not exempt the host. The address check still runs first. A proxy value with no scheme is treated as an http proxy. (#1138, #1142) - The web app's pages now send a script policy
(
script-src 'self'; object-src 'none'; base-uri 'self'). A proxy or extension that injects inline or third-party script into the UI is blocked. (#1086) - Under principal auth, the open
/metrics/prometheusscrape leaves out thestrata_tenant_*andstrata_build_tenant_*series and the tenant label onstrata_ai_*, which become per-model totals. Per-tenant numbers are onGET /v1/admin/tenants(admin:tenants). Dashboards built on those series go empty. (#1087) - WebSocket clients: a socket whose API key is revoked or expires is closed
with code 1008 even if it only listens.
cascade_prompt.cells_to_runlists only the cells that will run, andcascade_progress.completedcounts only finished cells that run, ending attotalwhen the cascade succeeds. (#1087, #1098) - A REST or MCP cell run that is cancelled returns a failed result with
error_code"cancelled", instead of a 500 (REST) or a "could not be executed" error (MCP). (#1101) - The TUI's panel resize keys are now
[and](cell list width),-and=(top detail height) and\(reset), in place of ctrl+arrows and ctrl+x, which macOS never passes to the terminal. (#1136) - New minimum: anyio 4.1, now a declared dependency. (#1119)
Added¶
- Cancel a cell's run from the CLI, REST and MCP.
POST /v1/notebooks/{id}/cells/{cell_id}/cancel(needsnotebook:execute) returns{cell_id, cancelled, status}.strata cell cancel --server URL --session ID <cell_id>exits 0 when a run was cancelled and 1 when the cell was not running. The MCPcancel_celltool does the same. (#1101) - The server image is published per release to
ghcr.io/bearing-research/strata, with themcpextra installed. (#1113) - A personal server's worker registry can be edited through the
/v1/admin/notebook-workersroutes, so a machine type enabled later reaches a running personal server. (#1112) - A queued remote cell's badge says where it stands. A worker or
dispatcher that answers
{"state": "queued"}can addpositionanddetail. They reach thecell_statusframe asremote_queue_positionandremote_queue_detail, and the badge shows the detail, else "queued #N -> worker". (#1140) - A dispatch says whether a person is waiting. Both executor transports
send
metadata.origin:"notebook"for a cell run from an open notebook,"run"forstrata run. It does not change a cell's provenance. (#1139) - A remote worker can start a machine before a cell needs it. The notebook
sends
POST {worker url}/v1/hintswith{worker, runtime_id, notebook_id, reason}when a cell bound to that worker is focused or selected, and before a cascade or Run All, at most once a minute per session and worker. A worker that does not know the route answers 404, which is ignored. The strata-pool dispatcher answers a hint for a machine type by keeping one machine of it warm until cool-down reaps it. (#1147, #1148)
Changed¶
- Store and blob calls run off the event loop. Artifact, build, publication, name, alias, registry, materialize, stream, metadata, admin worker and health routes, API-key checks over HTTP and WebSocket, the embedded build runner, staleness checks and notebook cell runs on signed-transport workers no longer make store or blob calls on the event loop, so a SQLite lock wait, a Postgres round trip or an S3 read no longer stalls other requests. A table input's plan is awaited without holding a server thread. (#1089, #1094, #1109, #1119, #1120, #1121, #1122, #1123, #1124, #1125, #1128, #1145, #1146)
- One thread limit for the server. Store and blob work shares anyio's thread limiter, set to 40, and the Postgres pool holds up to 41 connections per process (was 16). (#1119)
- Cells die with the server. A cell subprocess (cold, Run All batch, warm
pool, or started by
strata-worker), an R cell, an R warm-pool worker and an ssh tunnel are killed when the process that spawned them dies. Windows keeps the old behavior. (#1088, #1091) - Cascade frames count only the cells that run.
cascade_progressno longer reports counts such as 5/4, and a successful cascade ends with a frame wherecompletedequalstotal.cascade_prompt.cells_to_runleaves out ready upstreams that are skipped. (#1098) /health/readybounds its metadata-store check by the same 5 second limit as the artifact-store check, and reports not ready when it runs past it. (#1124)POST /v1/cache/warmplans tables in the planning pool underplan_timeout_seconds. A plan past it is reported inerrors("planning timed out after Ns"). (#1109, #1129)- A remote cell run reads the worker registry once, so a registry change applies from the next run. (#1125)
- strata-pool's RunPod backend speaks REST v2. A 422 from RunPod now names the field it refused, and the region is the pod's data center. (#1137)
Security¶
- Artifact ids cannot name a path. On the local blob store,
PUT /v1/artifacts/by-provenanceused a caller's id as a blob key, so../blobs/<id>reached another tenant's bytes and../../xwrote outside the store. Such an id is now a 400, and the local blob store refuses any key that leaves its directory. (#1142) - One tenant holds an artifact id.
PUT /v1/artifacts/by-provenanceandPOST /v1/artifacts/importrefuse an id another tenant holds at any version, checked inside the store's write transaction, so two tenants racing on one id, or an import of a new version under a held id, can no longer add a version to the other tenant's id. (#1095, #1118) - Upload bodies are capped. See
STRATA_MAX_UPLOAD_BYTESand the 16 MiB metadata limit above. Before, the artifact write routes had no limit and several read the whole body into memory. A build log chunk is trimmed to its last 256 KiB as it arrives instead of after the whole body is read. (#1089, #1092, #1095, #1145) - A revoked API key closes a listening WebSocket. The key was rechecked only on frames that edit or run something, so a socket that never wrote kept receiving cell outputs and console after revocation. (#1087)
- The open Prometheus scrape names no tenant under principal auth. (#1087)
- The web app runs only its own scripts. Pages served by the UI send a
script-src 'self'policy, so script injected through markdown cells or Markdown outputs does not run. (#1086) - A guarded
@fetchrespects the egress proxy. In service mode a fetch connected straight to the checked address and ignoredHTTPS_PROXY, so on a server whose egress is kept by a proxy it reached any host. (#1138) - Proxy credentials stay out of fetch errors. The proxy's user and
password are masked in any fetch error, and a socks proxy without httpx's
socks extra is a fetch error rather than an
ImportError. (#1142) - The RunPod worker credential stays out of the machine catalogue. strata-pool wrote it into the shared machine type, which the admin machine-types view lists. (#1142)
Fixed¶
- Warm pools start. A notebook opened through the server never started its Python or R warm pool, so every cell ran cold. An R pool now also starts when an R cell is added, and a missing pool starts after a dependency change, over REST and MCP alike. (#1096, #1100, #1131)
- The first dev dependency no longer invalidates every cached cell
(for example the pytest that the first
strata cell testadds), and re-pinning a git dependency to another commit still does. (#1097) - Moving a notebook to another Python version re-runs its cells. Results computed under 3.12 were served as cache hits under 3.13 when the packages' hashes did not change. (#1144)
- A loop cell keeps its final iteration's display after reopening, in
cell showand instrata export. (#1102) - A loop cell's console is its iterations' output as written, and
strata cell runon a loop cell now records it forcell showandstrata export. (#1098) strata run --format jsongives a failed cell'straceback. (#1098)- Clearer errors: a cell whose
workernames an undefined worker fails as unknown, not "not implemented yet", and a timeout error names the setting the limit came from (annotation,notebook.toml, or the flag and default). (#1098) - A notebook's uv commands ignore the server's
UV_PYTHON, which made a notebook pinned to another Python fail to run any cell. (#1105) - A cancel no longer leaves a cell stuck running. A cancel just as a run started left the cell running, and every environment change was refused until it ran again. Closing a session also stops a warm worker still booting, and a session closed before its environment job finished starts no warm pools. (#1143)
- MCP dependency edits take the environment reservation, as the REST routes do, so they cannot interleave with a sync or another edit. (#1134)
- A named upload or cache hit names the right version. A named
PUT /v1/artifactsbecomes ready in the same commit as its name, and a named PUT or materialize hit racing a refresh names the refreshed version instead of returning no name or a 500. (#1115, #1121, #1123) - An alias approval and a rejection of one change cannot both succeed, and an approval can no longer apply a stale request while discarding a newer one. The loser answers 404. (#1116)
- Two overlapping moves of one alias no longer leave the table's Iceberg tag on the version the alias left. (#1122)
- Two publishes of one version can no longer mint two live links. (#1117)
- An import that loses a race answers 409 instead of a 500, and can no longer overwrite the winner's bytes. (#1093)
- A stale pull-model executor cannot fail a newer claim's build. Its finalize answers 409 and leaves the build to the newer holder, and a manifest re-fetch after the runner reclaimed the lease answers 409. (#1123)
- No more
PoolTimeoutunder load on Postgres, where the pool was smaller than the threads using it. (#1119) /mcpanswers to the hosts the server answers to. A client addressing the server by name or IP got 421 even whenSTRATA_ALLOWED_HOSTSnamed the host. (#1114)- A table id with an empty namespace or table name is a 400 on materialize and both cache-warm routes, not a 500 or 404. (#1085)
- Opening an artifact store another process is using no longer fails with
FileNotFoundError. (#1108) - Staged remote-worker inputs are named per tenant, so two tenants running notebooks with the same notebook id no longer stage an input under one id. (#1127)
- A notebook deleted and put back at the same path opens again. Re-cloning or re-importing a notebook under an open session made every later open of that path fail with a 500 until a restart; the stale session is now closed and a fresh one opened. (#1150)
- A fan-out cell reports a cache hit when every variant instance was one, instead of always reporting a run. (#1150)
0.9.0 - 2026-10-07¶
Strata becomes something a platform can host for many people. Notebook routes know their tenant, the server runs under a path behind a proxy, workers reach it from behind NAT, and lake reads cover more catalogs and more of what other engines write. The artifact store looks after its own size, and service mode closes several ways a notebook could reach past its own data.
A platform can host it. In service mode every notebook route, the WebSocket
and MCP treat another tenant's session as missing, and with
multi_tenant_enabled each tenant keeps its notebooks in its own folder.
STRATA_PUBLIC_BASE_PATH serves the UI, API and publication pages under a path,
the editor can be framed without its own chrome, idle sessions close, and a
result can be promoted to a team store without a name. There are two deployment
modes and nothing between them: a personal server has one user, and a team runs
one per member or a service-mode server.
Workers go where the hardware is. strata-worker --connect dials out to a
relay, so a machine behind NAT serves cells with no open port. Workers restore a
notebook's renv.lock as they do its uv.lock, fetch prebuilt environments
keyed by what the lock installs, and a remote cell's console streams live,
across nodes and under authentication.
More of the lake reads correctly. Scans apply merge-on-read deletes
(positional deletes, deletion vectors and equality deletes) and follow schema
changes by field id, so tables Spark, Flink and DuckDB write are read as Iceberg
defines them. DuckDB cells attach AWS Glue catalogs and read gs:// and az://
mounts, and a # @dataset read carries the dataset's lineage, so the registry
shows which cells read each name.
The store stays bounded. A personal server now collects results nobody has used for a while, least recently used first, and never anything named, pinned, published or still needed by something kept. A notebook keeps each cell's current value and its last few versions.
Service mode is tighter, and personal servers are harder to reach from a web page. SQL cells are confined to their own database, key files and lake, package installs are wheels only, outbound requests connect only to the address the guard checked, R restores run as the harness user, and the log buffer and a worker's token are out of other tenants' and cells' reach. In either mode the server's Infisical credentials and AI key go only to the operator's hosts, never to one a notebook names. Nothing the server stores is unpickled or served as a page in its process or on its origin. A personal server refuses a hostname it does not know, which closes DNS rebinding, and no route serves files from outside the frontend.
Upgrading from 0.8.0:
- Back up first: the artifact store's metadata migrates on first start, one
way, and a backup is the only way back. With the default SQLite store, stop
the server and copy its artifact directory; on Postgres,
pg_dumpthe metadata database and copy the blob bucket. Keep the notebook storage directory (without.venv) and the artifact directory'snotebook_workers.jsontoo. Operations has the steps. Thestrata artifactcommands that only read (list,show,lineage,verify,audit,pending,archive) never migrate a store: on one an older release wrote they refuse and say to start this release's server on it, so checking a backup or the live store before upgrading leaves it unchanged. - Stop every node before the first start of this release, and do not run 0.8.0
and this release against one store, as a rolling upgrade over a shared
Postgres would. 0.8.0 runs on a migrated store without a warning, and there
every
/p/link handed out before the upgrade answers 404, each publication answers at its id (the SHA-256 the store now keeps, whichGET /v1/publicationslists), so the id works as a public link, links 0.8.0 mints answer 404 once this release serves the store again, and the latest version of each result it writes is never collected. To go back, restore the backup into an empty database or directory. From this release on, an older Strata refuses to open a store a newer one migrated, naming both versions, and thestrata artifactcommands say so instead of printing a traceback. - A configured SQL catalog whose warehouse is in object storage now needs a
uri(STRATA_CATALOG_URI, or the entry's own): the server refuses to start rather than keep the catalog in SQLite on its own disk. - Service mode refuses to start without
STRATA_AUTH_MODE: settrusted_proxy(withSTRATA_PROXY_TOKEN) orapi_key. Before, such a server started with every route open, cell execution and the admin worker registry included. - Behind a trusted proxy, check that the proxy sets
X-Strata-Principal, the tenant header,X-Strata-ScopesandX-Strata-Proxy-Tokenon every request, with an empty value when the user has none: a header the proxy leaves alone reaches Strata from the client. Service Mode has an nginx block that does. - Personal mode refuses a request whose
Hostis not loopback, an IP address, the bind host or a name in the newSTRATA_ALLOWED_HOSTS: a server reached by a LAN hostname or a custom domain needs that name listed (the 400 names the setting). A browser WebSocket from another origin needsSTRATA_CORS_ALLOW_ORIGINS. STRATA_PERSONAL_MODE_USER_HEADERis gone and ignored if set: a personal server has one user, and every caller reaches every notebook. Before upgrading a server that used it, give each member a personal server or switch to service mode. A notebook'sownerkey is ignored and dropped on the next structural edit. The TUI's--user-header/--userflags are gone.- Removed settings are ignored without a warning, so look for them before the
first start:
STRATA_PERSONAL_MODE_USER_HEADER(personal_mode_user_headerin[tool.strata]),STRATA_ARTIFACT_GC_MAX_AGE_DAYS(artifact_gc_max_age_days), and the TUI'sSTRATA_TUI_USER_HEADER_NAMEandSTRATA_TUI_USER. A server that relied on the user header to keep members apart opens every notebook to every caller, with nothing in the log. - The first start narrows the server's state to its own account: the artifact directory, its database and blobs, and the cache directory become 0700 or
- A notebook's
.strata/becomes 0711 (its server-only parts 0700 and 0600) when the notebook is next opened, and a tenant's notebook folder 0711 when the tenant next calls. Nothing is logged, and every start narrows them again. A sidecar, backup agent or group that reads them as another account loses access: run it as the server's account. - New minimums: pyiceberg 0.12, DuckDB 1.5, sqlglot 30.13, filelock 3.24;
mcp 2.2 with
[mcp]; textual-image 0.14 and Pillow 12.1 with[tui]; pydantic 2.13.5 withstrata-pool[server]. - Scan caches are rebuilt: the first scan of each table reads from storage,
and a transform that reads a table directly recomputes once, since a table
input's version now carries the schema id as well as the snapshot. Until it
does, a name over such a result reports stale (
is_stale, the name status route). A notebook's own store migrates the first time a command opens it. Results stored by 0.8.0 count as used at the upgrade, so none is collected for idleness in the first 30 days; the size cap still applies. - Unnamed
materializeresults can now be collected in personal mode; name or pin a result to keep it.STRATA_ARTIFACT_GC_MAX_AGE_DAYSis removed and a leftover value is ignored (useSTRATA_ARTIFACT_GC_MAX_IDLE_DAYS);garbage_collect(max_age_days=)is nowmax_idle_days, and a barePOST /v1/artifacts/gcapplies the configured retention. - Workers upgrade with the server: run
strata-workerfrom this release on every worker. The executor protocol'senvironment.keynow covers what the lock installs rather than the rawuv.lock, so an older worker fails every cell of a locked notebook, and an environment registry addressed by key needs the new keys. An SSH worker that does not report thelaunch_idStrata now checks is refused (let Strata install the new one, or upgrade it on the box). Console chunks carry a requiredseq, a signed manifest'soutputcarriesmethod(Azure uploads are a PUT), and R cells carry anenvironmentblock: an R notebook with anrenv.lockis refused on a worker that does not advertiselocked_r_environments. Workers fetch prebuilt environments fromSTRATA_WORKER_ENV_REGISTRY_URL/<key>/<interpreter>/<platform>and rebuild their local environments once; a registry on the old<url>/<key>path must move (until then every dispatch builds locally). - A
strata-workerwithoutSTRATA_WORKER_TOKENlistens on 127.0.0.1 only: set the token (the default bind is then0.0.0.0) or pass--host. - The server-managed worker registry moves from
notebook_workers.jsoninto the artifact metadata store. The first start imports the file from the artifact directory and renames itnotebook_workers.json.migrated(or.migrated.1and so on, never over an earlier copy), without importing it when the store already holds a registry (another node imported first). A worker the file names twice is imported once, from its last entry. A fleet manager that wrote the file under a running server must use the/v1/admin/notebook-workersroutes: the file is no longer read. - Shared environments are keyed by what the lock installs, not the raw
uv.lock(which carried the project's name): existing shared environments are rebuilt once, and an old one stays on disk until it has been unused forSTRATA_NOTEBOOK_SHARED_ENV_TTL_DAYS(7 days;strata env gc --ttl-days 0removes it sooner). PUT /v1/notebooks/{id}/workersandstrata worker addrefuse an unknown transport; a worker saved astransport = "http"should be"direct".duckdb_sql@v1, in either mode, reads only its inputs: a query that reads a file or URL (read_parquet,read_csv), attaches a database or loads an extension now fails. Pass the data as an input, or run the query on an HTTP executor.- Service mode: dependencies without a wheel no longer install; guarded
outbound connections ignore
HTTPS_PROXY; R packages are not added from the notebook where cells are isolated;/v1/logsneedsadmin:*. - Service mode: a BigQuery connection needs a
credentials_pathkey file in the notebook's directory. One elsewhere, or none at all (which used the server's own Google credentials), is refused; move the key into the notebook. - Service mode: a DuckDB or SQLite connection's database file must be in the
notebook's directory, or outside the server's home and state directories; a
SQLite
uriis refused; a DuckDB cell reads only its own database, the mounts it declares and its catalog tables' locations, and a SQLite write cell cannotATTACH,DETACHorVACUUM. Move such files into the notebook or mount their directory. - Service mode:
name=on materialize needsartifacts:write; under api_key the tenant comes from the key, notX-Tenant-ID; the cache histogram, metadata cleanup,/metrics/tables*and/v1/debug/*needadmin:cacheoradmin:*. - Service mode: MCP tools reach only sessions of the caller's tenant; the Prometheus scrape leaves out per-table series under principal auth.
- Service mode: a notebook session belongs to the tenant that opened, created
or imported it; with
multi_tenant_enabled, each tenant's notebooks live under<notebook_storage_dir>/<tenant>/, so notebooks at the top of the storage root are no longer listed or openable by non-admin callers until they are moved into their tenant's folder. Opening one answers 400, "must be inside your tenant's own folder in the notebook storage". - Service mode: a table URI whose warehouse is in object storage needs a
catalog
uri(STRATA_CATALOG_URI); without one, scans, cache warming and export to a table answer 400 instead of using a SQLite catalog on the server's disk. - With
STRATA_NOTEBOOK_HARNESS_ENV_ALLOWLISTset, a cell that needs a credential-named variable underUV_,PYTHONorR_, a uv index URL, or anything underCONDA_, must name it in the allowlist. s3://andgs://notebook mounts need thes3orgcsextra where Strata is installed (for exampleuv tool install "strata-notebook[s3]"), which carry s3fs and gcsfs.- Adding a dependency from the Environment panel, REST or the WebSocket takes
exactly one requirement: a value that is several packages or starts with
-is refused, and a bare URL or path needs a package name in front, in PEP 508's name-at-URL form. - A notebook's
[ai] base_urlthat is not the server'sSTRATA_AI_BASE_URLor a provider's default no longer receives the server'sSTRATA_AI_API_KEY, in either mode. Put a key in the notebook (any value works for Ollama), or setSTRATA_AI_BASE_URLon the server. - A notebook's
[secret_manager] base_urlmust matchINFISICAL_HOST, in personal mode too; setINFISICAL_HOSTfor a self-hosted Infisical. - The address guards on
@fetch, a notebook's AIbase_urland signed-worker manifest URLs now also refuse100.64.0.0/10, the range Tailscale and carrier-grade NAT use. Asignedworker that reaches its server over a tailnet needs the server named inSTRATA_WORKER_ALLOWED_HOSTS; in service mode an@fetchof a tailnet host needs it inSTRATA_NOTEBOOK_FETCH_ALLOWED_HOSTS. Personal-mode@fetchalready allows private addresses. - In most notebooks every cell runs once more after upgrading, then caches as
before: provenance no longer includes the notebook's own project name, which
changes the cache key of every cell in a notebook whose
uv.lockhas no dev group, and every notebook 0.8.0'sstrata newor/createmade has none. In other notebooks these cells run once more: every SQL cell (its cache key is now its own query text); a cell that reads a name more than one upstream cell defines, which then reads it from the cell the DAG wires; every loop and# @per_variantcell, since their cache keys now cover the@loopand@loop_untilparameters and the sweep group, and the cells that read them; prompt cells that read an upstream scalar; and cells whose[env]held a blanked secret, since that empty value no longer reaches them. - Leaf cells, ones nothing else reads, are now cache hits on an unchanged run,
in Run All too. A leaf kept only for its side effect (writing a file, sending
a message) runs once until its source or inputs change; mark it
# @nocacheto run it every time. - Cell test results recorded by 0.8.0 show stale once: the stale check now covers the cell's mounts, fetches, datasets, tables and environment.
- Notebook sessions with no edits, runs or focus for 4 hours are closed even
with a tab open (
STRATA_NOTEBOOK_SESSION_TTL_SECONDS); the tab offers to reopen. - Secret env values are no longer sent to clients: REST and WebSocket
responses carry
"__strata_masked__"in their place, and sending the marker back keeps the value. - A prompt cell that reads a pickled Python object sees a placeholder, and a SQL bind of a pickled value is refused: the server no longer unpickles cell data.
.mcp.jsonandCLAUDE.mdare ignored in new notebooks and left out of exports; an existing notebook's.gitignoreis not changed.- Nanosecond timestamps in v1 and v2 tables come back as microseconds.
- WebSocket clients:
cell_errorframes carry the failed run'sstdoutandstderrand replace the console ascell_outputdoes, and while a cell runs,notebook_stateholds only what this run has printed (see the Console section of the WebSocket reference). A run refused because another run holds the notebook is anerrorframe withcode: "notebook_busy"and the refusedcell_id. After a source edit, the session's other connections get the edited cell's newsourceindag_update; every other entry carriessource: null. A graceful server stop sendssession_closedwithreason: "shutdown"before the socket closes. - strata-client:
Artifact.lineage()no longer takesdirection(lineage is always upstream), andArtifact.dependents()takeslimit(default 100) in place ofmax_depth. - Publications serve only
image/png, Arrow, JSON and Markdown inline; any other stored type downloads asapplication/octet-stream, andPATCH /v1/publications/{token}refusesartifact_idandversionwith a 400. - A publication's token and link are returned only by the publish that mints
them: republishing returns
tokenandurlas null,GET /v1/publicationslists publications byid, andPATCH/DELETE /v1/publications/{token}andstrata artifact unpublishtake the token or the id. Publish, credit and withdraw events carry the id invalue. Links already handed out keep working. - Notebook stores written before this release recorded an unknown output row
count as 0, and so did results a notebook published to a team or service
store, so
strata artifact verifyflags those Arrow outputs. Running a cell again does not clear it: the old version is kept as one of the cell's three superseded versions, so the flag goes once three newer runs have replaced it and the server has pruned it on opening the notebook. A team store's flagged results stay flagged. POST /v1/artifacts/importrefuses record fields of the wrong type or shape with a 400: counts must be non-negative integers, andtransform_specandinput_versionsJSON strings.GET /v1/registry/audittakeslimitfrom 1 to 1000 and answers 422 outside that range.- Naming, aliasing or tagging an artifact that does not exist answers 404 instead of 400, the same as reading it.
/health/readyno longer has astuck_scansfield.- strata-pool: reading a job needs the
X-Strata-Tenantheader that submitted it. - strata-pool:
PUT /v1/machine-typesneeds the newadmin_token;/v1/usageand/v1/workersreturn only the caller's tenant.GET /v1/machine-typesshowsenvandprovider_optionsvalues as<redacted>without it. - strata-pool: the jobs table's state CHECK gains
cancelled. A pool database created before this rejects a cancel until that constraint is updated or the database recreated. - strata-pool: the stored catalogue gains a
versioncolumn; an existing pool store needsALTER TABLE catalogue ADD COLUMN version INTEGER NOT NULL DEFAULT 0or a fresh store. - The root
fly.tomlis nowfly.example.toml; copy it tofly.toml(gitignored) and fill in the placeholders to deploy on Fly.
Added¶
- The notebook editor can be framed. Under
?framed=1(#/notebook/<id>?framed=1) the editor leaves out what a platform framing it already has: Strata's brand and the notebook's name, the links to the App, Artifacts and Logs pages (which would navigate the frame away from the notebook), the deployment-mode badge, and Delete Notebook, which is the platform's to offer so its own records see it. Running, editing, export and the rest are unchanged. - The artifact store stays bounded on its own. A personal server sweeps its
store every hour, collecting results unused for 30 days and, once the store
is over 20 GiB, the least recently used down to 80% of that. Use is a cache
hit, a read, or a request that names a result as an input. Nothing named,
aliased, pinned, published, awaiting alias approval or still building is
collected, nor anything those were built from, nor anything used in the last
hour.
strata artifact gcruns or previews a sweep without a server, andPOST /v1/artifacts/gcand the client'sgarbage_collecttakedry_run,max_bytes,max_idle_days,min_idle_secondsandcollect_latest. Each limit is a setting (STRATA_ARTIFACT_GC_INTERVAL_SECONDS,_MAX_BYTES,_MAX_IDLE_DAYS,_MIN_IDLE_SECONDS); service mode sweeps only when an operator sets the interval, and a sweep covers every tenant against one cap. A notebook's store keeps each cell output's current value and its three most recent earlier ones (STRATA_NOTEBOOK_KEEP_SUPERSEDED_VERSIONS), pruned when the server opens the notebook. - A result can be promoted without a name.
POST /v1/notebooks/{id}/artifacts/{aid}/v/{n}/promotewithoutnamecopies the result and its chain into the team store and names nothing, so a platform can move a chain there to publish it without adding every published result to the team's registry. An unnamed copy stamps no promotion tag, and an alias without a name is refused with a 400. The CLI and the MCP tool still take a name. - Scans read Iceberg tables with merge-on-read deletes. A table that Spark, Flink or DuckDB deleted from without rewriting its data files used to be refused. The scan now drops the rows its positional delete files (format v2) and deletion vectors (format v3) name, and caches each row group without them under a key that names the snapshot and the schema.
- Scans read Iceberg tables with equality deletes, which Flink's upsert
sink and CDC pipelines write, and which pyiceberg itself cannot yet plan.
Every older row whose key a delete names is dropped, null matching null, in
the delete's partition or across the table for an unpartitioned one. A
delete file whose key range cannot meet a row group is skipped for it.
Applying equality deletes holds their keys in memory, so a scan in which a
row group would need more than
STRATA_MAX_EQUALITY_DELETE_ROWS(10 million by default) is refused while planning, with a message pointing at compaction, never read partially. A delete file in ORC or Avro, or keyed on a struct column, is refused the same way. Keys compare the values the scan returns: on a v1 or v2 table a key a file holds in nanoseconds is truncated to microseconds, so a delete a JVM writer (Flink, Spark) wrote with the microsecond value matches, and a key a file omits because it is the file's identity partition matches on the partition value. - Scans read Iceberg tables whose schema changed. Columns are matched by
field id, so a table with an added column, which used to be refused, reads
its older files with the column null, and a widened type comes back wide. A
scan of the current table reads the current schema; one that names a
snapshot reads that snapshot's. The same goes for the fields inside a
struct, list or map column. A column added with a default (format v3) reads
its default from files that predate it, and an identity-partition column a
file omits (a Hive-layout file registered with
add_files) reads the file's partition value, as pyiceberg reads it. - Notebook sessions have limits. Idle sessions close after
STRATA_NOTEBOOK_SESSION_TTL_SECONDS(4 hours), at mostSTRATA_NOTEBOOK_MAX_SESSIONS(50) stay open, and withSTRATA_NOTEBOOK_SESSION_MIN_AVAILABLE_MBset the least recently used idle session closes when available memory drops below it. A sweep applies them every minute. Edits, runs, cell focus and MCP calls count as activity; a session running a cell or an environment job is never closed.STRATA_NOTEBOOK_WARM_POOL_SIZEsets the warm pool (0 turns it off). A closed session tells its clients with asession_closedframe, and the browser offers to reopen; the terminal viewer stops, shows why, andrreopens the notebook by path. A graceful server stop (Ctrl-C, SIGTERM,docker stop) sends it with reasonshutdown, and the terminal viewer also stops this way when a reconnect is refused.POST /v1/notebooks/{id}/closecloses a session and keeps the notebook. - A worker can answer a direct cell with 202.
POST /v1/executemay return ajob_url; the server polls it, a machine's boot no longer counts against the cell's timeout, and the finished job's URL returns the bundle. - strata-pool jobs can be cancelled.
POST /v1/jobs/{id}/cancelstops a queued job before any machine starts, and a running one on its machine, which returns to warm. The job endscancelled. - Results record who computed them. On a server that authenticates its callers, every artifact a notebook run stores records the member who started the run, so promoted and published results name who computed each step.
strata cell pin-fetchwritessha256=pins for a cell's fetches from the local fetch cache. A pin mismatch carrieserror_code: fetch_pin_mismatch, and publication pages and RO-Crates show when fetched bytes were retrieved.- Named catalogs take a credential. A
STRATA_CATALOGSentry acceptscredential = "<name>", resolved fromSTRATA_NOTEBOOK_CREDENTIALS. - Every cell has a digest.
strata runstores leaf cells' variables too, so--format jsonreports a digest for the final result, and the ZIP export'sprovenance.jsonlists each cell's output digests. A leaf's console record is not an output there, nor in the MCP "no stored output" error. - Archives verify either file. The RO-Crate lists
artifact.parquetwhen the archive has one, andGET /p/{token}/verify?sha256=names the bundle file a digest matches. Each credit edit is acreditevent on/v1/events. - Snapshots carry fetched bytes. An
include=allsnapshot carries the notebook's fetched files and index, so a pinned orrefetch=neverfetch runs from an imported copy without its URL. Import checks every fetched file against its digest and the fetch cache's path rules. - Presigned URLs for GCS, Azure and S3 roles.
STRATA_ARTIFACT_PRESIGNED_URLSnow presigns for GCS (V4 URLs and a size-limited POST policy, from a service-account key or IAM signBlob), Azure (blob SAS from the account key or a user delegation key) and S3 stores whose credentials come from an instance role. New extrass3andgcs. A configuredSTRATA_AZURE_SAS_TOKENis never handed to workers. - Environment registry by build. A worker fetches a prebuilt locked
environment from
<registry>/<key>/<interpreter>/<platform>, so one registry serves every Python build and platform, and builds locally on a 404. - R environments on workers. A
strata-workerwhose R has renv restores the notebook'srenv.lockonce per lock and R build and runs R cells against that library, advertisinglocked_r_environments; the registry serves R libraries at<registry>/r/<key>/<R version>/<platform>. The R worker image installs renv. - Live remote console across nodes. Worker console chunks carry a
seqand are shown in order and once each; opening a notebook mid-run shows the last 64 KiB a remote cell printed; withSTRATA_NODE_ADVERTISED_URLset, chunks that land on another node reach the session through the shared build store.cell_consoleframes carrychunk_seq; chunk 0 starts a stream's console for the run. - Dataset lineage. A
# @datasetread copies the dataset version with its inputs and the steps behind it (up to 10 deep), so lineage walks past the dataset, and the Registry tab shows which notebook cells read each name ("Read by";readerson/v1/registry/summaryand the notebook artifacts route). In the all-tenant summary each row carries itstenantand lists only that tenant's readers.GET /v1/artifacts/{id}/v/{n}returnsinput_versions. - DuckDB cells reach more lakes. Connections attach AWS Glue catalogs, read
gs://andaz://mounts through the mount's own filesystem, and on a personal server use catalogs defined in notebook.toml ([catalogs.<name>]).# @cache snapshotworks on DuckDB catalog cells, pinning the Iceberg snapshots the first run read. - The publication page shows the snapshot horizon for a Snowflake or BigQuery
# @cache snapshotstep: the warehouse state's moment and how long it stays queryable. - strata-pool: a catalogue set on one pool process reaches every process on
the same store;
pool.queueandpool.bootspans join the submitter's trace. - The generic reference executor reports its hardware on
GET /health. benchmarks/capacity_sweep.py --no-servertakes--table-uri.- Tenant-aware notebook routes. In service mode every notebook REST route,
the WebSocket and MCP treat another tenant's session as missing (a 404, a
refused upgrade, or a session MCP neither lists nor reaches);
admin:*reaches every session. A member the proxy sends without a tenant header is the default tenant there too. Withmulti_tenant_enabledeach tenant gets its own storage folder. This is a convenience for small trusted tenants, not an isolation boundary: cells still share the host. - Serving under a path.
STRATA_PUBLIC_BASE_PATHserves Strata behind a reverse proxy at a non-root path, whether or not the proxy strips it; the UI, its WebSocket, the API docs, signed build URLs and publication pages carry the path. The frontend bundle uses relative asset URLs. - A worker behind NAT.
strata-worker --connect wss://...binds no port: it dials out to a relay and serves cells over that WebSocket, reconnecting with backoff. The relay protocol is documented in the Worker Relay Protocol reference; Strata does not ship a relay. GET /v1/streams/{id}carriesX-Strata-Artifact-Uri, the canonical artifact of the rows it serves.- Superseded versions are counted.
GET /v1/artifacts?state=supersededlists them,GET /v1/artifacts/statsand/usagereportsuperseded_versionsbeside the ready, building and failed counts, and the web UI's Artifacts page filters by that state and shows the count. Theirtotal_bytescounts every version that still holds bytes, superseded ones included: the figure the size cap uses. - A cell's test result carries a
staleflag, so tests run against older source, tests or inputs show stale after a reopen. gs://andabfs://warehouse catalogs read with the server'sSTRATA_GCS_*andSTRATA_AZURE_*settings, including an Azurite or private blob endpoint (Strata derives the connection string adlfs needs).strata cell add --language widgetadds a widget cell.
Changed¶
- An unnamed result is a cache entry. In personal mode a
materializeresult nobody has used for 30 days, or the least recently used ones once the store is over 20 GiB, is collected, and the same request computes it again. Name it or pin it to keep it.STRATA_ARTIFACT_GC_MAX_AGE_DAYSis gone:STRATA_ARTIFACT_GC_MAX_IDLE_DAYScounts from a version's last use, andgarbage_collect(client and route) takesmax_idle_daysin place ofmax_age_days; a leftoverSTRATA_ARTIFACT_GC_MAX_AGE_DAYSis ignored, and a barePOST /v1/artifacts/gcapplies the configured retention. - Nanosecond timestamps in v1 and v2 tables come back as microseconds.
Those formats define timestamps in microseconds, but a file can hold
nanoseconds (or INT96), which 0.8.0 returned as
timestamp[ns]. They are now read at the table's microsecond unit, truncating, nested ones included. - Nothing 0.8.0 cached for a scan is reused. The row-group cache moves to
a new version directory, the metadata store is discarded and rebuilt, and a
scan's provenance now includes the schema, so scan artifacts are built
afresh. 0.8.0 could have cached rows a merge-on-read delete removed, or a
column's values under another column's name. The first scan of each table
after upgrading reads from storage. The old
v3directory under the cache directory is deleted at startup when it holds nothing but cache entries; anything else is left in place with a warning. - pyiceberg 0.12 or newer is required. Before 0.12, pyiceberg misread a manifest whose entries leave their snapshot id to be inherited, which is how DuckDB writes them, and dropped the delete files those manifests listed.
- The
[mcp]extra needs mcp 2.2 or newer. mcp 2 renamedFastMCPtoMCPServer, and the notebook's/mcpendpoint now uses it. With mcp 1 still installed,/mcpis off after upgrading: the server logs that the installed mcp is too old and keeps running without it. - sqlglot 30.13 or newer is required, the first release that writes DuckDB's snapshot clause after a table's alias.
- DuckDB 1.5 or newer is required (
strata-notebook, itssql-duckdbextra, andstrata-client'sduckdbandallextras). DuckDB 1.5 deprecatesfetch_arrow_table()forto_arrow_table(), which a connection only has from 1.5, so Strata now calls the new name. - The
[tui]extra needs textual-image 0.14 and Pillow 12.1 or newer. The older floors let the TUI install versions that fail at import or when drawing an image. - The uv-only guard says what to run. Started from a conda environment or
a plain
pip install,strataandstrata-notebookused to point at apyvenv.cfgmarker those environments do not have. The message now says which case it is and names the two supported ways in:uv tool install strata-notebookfor a release, oruv sync && uv run strata-notebookfrom a checkout.strata --helpprints from any Python; every command is still refused outside a uv-managed environment before it does anything. - Published bytes are served as data. Only Strata's own types are shown
inline (
image/png, the Arrow stream,application/json,text/markdown), now under their real media types; anything else downloads asapplication/octet-stream. Every such response carriesX-Content-Type-Options: nosniffand asandboxpolicy.archive.zipis built once per publication and cached on disk (strata artifact unpublishdrops it), and a table over 128 MiB is archived without its Parquet copy. - Publications follow the whole chain their page shows. The archive walks
the chain as deep as the page (25 steps), and so does
strata artifact archive --token. A core scan or transform result publishes and archives as Arrow (artifact.arrow). - Leaf cells are cached like any other. A cell nothing downstream reads,
printing or silent, keeps its console under the cell's provenance, so it
reads
readyafter a reopen (in the web UI,strata statusandcell list) and an unchanged run is a cache hit with its output replayed. This holds in Run All, for loop cells, and after a run with the rerun button, Rerun All orstrata run --force, whose console the next plain run or reopen shows. Reverting an edit is a cache hit for a leaf too, whether or not that run made a display. Before, a silent leaf always ran again, every leaf re-ran on every Run All, and a printing leaf read idle after a reopen. strata run,strata cell runandstrata cell add --runkeep notebook INFO logs off stderr;STRATA_LOG_LEVELbrings them back.- Notebooks share an environment whatever their names. In shared mode, notebooks with the same resolved dependencies link one environment, and the notebook's own project is never installed into it.
- Exports carry the committed set. Snapshot and ZIP exports include
everything under
cells/, subdirectories too, and the notebook's.gitignore, and import restores it. publish --tostreams and remaps. Uploads go through the staged import route instead of memory, and a chain another tenant already holds lands as remapped copies instead of a 409.promote --toalso streams.- Service mode reuses only your own session. Opening a path reuses a live session only for the member who opened it.
PATCH /v1/publications/{token}refusesartifact_idandversionwith a 400 instead of ignoring them.- Identical cells share results across notebooks. The provenance env hash leaves out the notebook's own project name, so two notebooks with the same resolved dependencies get the same provenance and share team-cache hits.
- MCP errors reach the agent. A tool call that fails for a reason the agent can act on returns that reason instead of a bare "Error executing tool".
/metrics/prometheusleaves outstrata_table_*series under principal auth.- In service mode a table URI with an object-store warehouse and no catalog
uriis refused with a 400 namingSTRATA_CATALOG_URI, on scans, cache warming, export and transform inputs (which used to start a build that could only fail). - Under principal auth the AI usage series on
/metrics/prometheuscarry noprincipallabel: they are per-tenant, per-model totals. - A named catalog's
credentialresolves against the notebook's environment wherever a notebook reads the catalog, not only for the DuckDB attach. - The
azureextra installsadlfs, which PyIceberg needs to read Azure lake tables. - The materialize and stream handlers moved from
server.pyinto routers; their routes now carry the "materialize" and "streams" OpenAPI tags. - Two modes. The personal-mode multi-user header is retired: personal
mode has exactly one user, and a team runs a personal server per member
sharing results through a service-mode store, or one service-mode server.
discoverand the notebook state no longer carryowner, andmcp_enabledis no longer refused in personal mode on its account. - A transform's table input plans in the planning pool under the plan timeout, and answers 504 past it.
- A table an export creates is written under the warehouse the request names, and a table URI that names a warehouse is a 400 when the configured catalog is REST or Hive.
- Signed-worker URLs last at least the cell's timeout plus the provisioning
wait plus five minutes;
STRATA_SIGNED_URL_EXPIRY_SECONDSis a floor for notebook cells. - strata-pool submit routes take
timeout_secondsandwait_secondsonly as positive finite numbers andpriorityas a 64-bit integer (422 otherwise); a job's timeout can shorten its machine type's but not extend it;PUT /v1/machine-typesrefuses wrongly typed or out-of-range fields (amax_workersbelow 1, a timeout of zero or less) with a 400 naming the field. - The server-managed worker registry lives in the artifact metadata
store (Postgres or the SQLite file) instead of
notebook_workers.json, so every node sharing the store offers the same workers and a change made on one applies on the others at their next request.POST /v1/admin/notebook-workers/reloadonly refreshes worker health. A staleness pass reads the registry once. When the store cannot be read, a service-mode server fails worker resolution andPUT /v1/notebooks/{id}/workersrather than fall back to the notebook's own[[workers]]. - A service-mode server whose transforms block is enabled but lists no
transforms warns at startup; the docs show the
duckdb_sql@v1entry (executor_url = "embedded://local") the built-in SQL transform needs there. - Notebook outputs record an unknown row count as unknown, shown as
-instrata artifact list, instead of 0. strata artifact verifyprints the metadata store and blob backend it checks; its JSON output addsmetadataandblobs.- The REST reference says
POST /v1/notebooks/createand/importreturn before the new environment is synced, and how to wait before running cells. python -m strataandstrata-notebookprintStrata cannot start:and the reason, then exit 1, on a refused configuration, instead of a traceback.
Security¶
- Service-mode SQL cells are confined to their own database and lake. SQL
cells run inside the server process, and a DuckDB cell could read any file
the server can (on Linux,
/proc/self/environholds the server's secrets) orCOPY ... TOandATTACHany path. In service mode a DuckDB connection is now locked once its own setup is done: it reaches its database, the roots of the mounts it reads and the locations of the catalog tables it reads, nothing else, and the cell cannot turn that back on. A catalog table whose data files live outside its own location is refused under confinement, naming the file. A SQLite write cell is refusedATTACH,DETACHandVACUUM. A SQLite or DuckDB database file must be in the notebook's directory, or outside the server's state and passing the mount-root rule; a SQLiteuriis refused, and the file is opened by its resolved path, checked again just before opening. A BigQuery connection must name acredentials_pathkey file in the notebook's directory (thewrite_credentials_pathone too, when set): without one it would run as the server's own Google credentials, and reopening a notebook no longer reads a key file the connection may not use.${VAR}in a connection'sauthreads the notebook's env, never the server's environment. The cell, the connection's schema listing and saving the connection all refuse what breaks these rules. Personal mode is unchanged. - Service-mode notebooks install wheels only. Building a package from a
source distribution runs its build backend as the server's user with the
server's environment, which is what the harness user keeps cell code away
from. Every uv command a notebook runs in service mode (
sync,add,lock, theuv runthat starts a cell) now refuses to build, and a dependency with no wheel fails to resolve with uv's message saying so. Personal mode still builds, so opening or importing someone else's notebook there runs its package build code; the environment docs now say so. - Outbound fetches connect only to an address the guard checked. The URL
guard on
@fetchand worker manifest URLs resolved a host and checked every address, but the request resolved the name again when it connected, so a name could pass with a public address and then connect to127.0.0.1or the cloud metadata address (DNS rebinding). Each connection now resolves the host once, checks every answer and connects only to one of them; TLS still verifies the certificate against the hostname. Guarded connections go direct and ignoreHTTPS_PROXY, since through a proxy the proxy would resolve the name; personal-mode fetches andSTRATA_WORKER_ALLOW_LOCAL_HOSTSkeep the environment's proxy settings. - A notebook cannot choose where the server's Infisical credentials go.
The secret manager logged in with the server's
INFISICAL_CLIENT_ID/INFISICAL_CLIENT_SECRET(orINFISICAL_TOKEN) at whatever host the notebook's[secret_manager] base_urlnamed. The host is now the operator'sINFISICAL_HOST(or the public default), in personal mode too, and any otherbase_urlis refused before any login, so opening a cloned notebook no longer sends your Infisical credentials elsewhere.project_id,environmentandpathstill come from the notebook, so scope the machine identity to what every notebook author may read. - R package restores in service mode run as the harness user. Restoring a
notebook's
renv.lockbuilds packages from source, which runs their configure scripts, and it ran as the server with the server's environment. It now runs asSTRATA_NOTEBOOK_HARNESS_USER, and so does the R package listing; with no harness user nothing is restored, and the log says why. What the restore writes (the notebook'srenv/, its library in the shared store, the package cache) is handed to that user, and a library link that points anywhere but the shared store stops the restore rather than giving the harness user that directory. Where cells are isolated, R packages are no longer added from the notebook: add them to the committedrenv.lock. - A notebook's
[ai] base_urlis checked like an@fetchURL in service mode. A prompt cell posts to it from the server and shows the answer, so it could read the cloud metadata address or an internal service. Private, loopback and link-local hosts are refused unless listed inSTRATA_NOTEBOOK_FETCH_ALLOWED_HOSTS. The operator'sSTRATA_AI_BASE_URLand the providers' own URLs are not checked. - The server's AI key goes only to the server's AI hosts. In either mode
STRATA_AI_API_KEYis sent only toSTRATA_AI_BASE_URLor a provider's default URL, never to a[ai] base_urla notebook names. - A confined SQL cell refuses a local mount root that exposes the server.
Confinement admits everything under a mount's root, so a
file:///mount let a DuckDB cell read the whole disk. In service mode a local root is refused if it is/or has fewer than two path components, if it holds or sits inside the server's artifact, cache, metadata or notebook directories or its home (the notebook's own directory excepted), or if it is inside/proc,/sysor/dev. - Guarded URLs refuse every address that is not globally routable.
@fetch, a notebook's AIbase_urland worker manifest URLs checked for private, loopback and link-local addresses; they now refuse any address that is not global,100.64.0.0/10included. - Opening a notebook no longer downloads its
@fetchURLs; only running the cell does. - The in-process
duckdb_sql@v1transform reads only its inputs: no server files,COPY TO,ATTACHor extension installs. - A cell's file must stay inside
cells/. A notebook whosenotebook.tomlnames one outside (an absolute path or..) is refused with an error naming the cell, so a snapshot import can no longer read, overwrite or delete files outside the notebook. - Worker secrets stay with the worker.
GET /v1/notebooks/{id}/workersno longer returns a managed worker's literal token, and a cell on asignedworker no longer stores its env, secrets included, in the server's build and artifact rows: only the names and a digest are kept, and the values travel only to the worker. Adding an SSH worker connects only when the worker answering on its port reports the one-offlaunch_idthat launch gave it, instead of sending the token to whatever answered on that port. - A withdrawn publication keeps nothing public but its tombstone.
GET /v1/publications/{token}for a withdrawn publication carries only the token, title and the publish and withdraw times, no lineage, sources, environments or principals. @datasetresolves names in the session's tenant, and another tenant's version is never handed over.- A denied table input is denied before it is planned. A principal denied
a table used as a transform input got a 422 naming the table and its
delete-file paths, or a 400 that let materialize build without checking the
ACL. It now gets a 404 (a 403 with
STRATA_HIDE_FORBIDDEN_AS_NOT_FOUND=false), before anything is planned. - A cell cannot read the worker's token from its parent process. The
worker took
STRATA_WORKER_TOKENand the credential variables out ofos.environ, but/proc/<pid>/environstill showed the environment it started with, and on the worker image cells run as the worker's user. The values are now zeroed there too, also in an app built withcreate_notebook_executor_app()(Modal, any ASGI host). - The server's log buffer needs
admin:*.GET /v1/logsand/v1/logs/streamhold every tenant's records and were open to any authenticated principal. Under principal auth they now needadmin:*; personal mode is unchanged. - Secrets fetched from Infisical stay out of
notebook.toml. Saving the Runtime panel sent every row back, and values whose names do not look secret (DATABASE_URL) were written to the committed file and stopped following rotations. An unchanged fetched value is no longer written and keeps its source; an edited one is a manual override as before. - strata-pool job reads are scoped to their tenant.
GET /v1/jobs/{id}and its/resulttook no tenant, so any token holder could read another tenant's job by id. They now need the submitting tenant'sX-Strata-Tenantand answer 404 for anyone else. - The web UI's fallback route no longer serves files outside the
frontend.
GET //etc/passwdor/%2fetc%2fpasswdreturned the file, in both modes, to any caller the server admitted. - A web page can no longer drive a personal server by DNS rebinding. The
origin guard trusted the request's own
Host, and the notebook WebSocket checked no Origin, so a page whose name was rebound to 127.0.0.1 could create and run cells. UnexpectedHostheaders are refused (over HTTP and WebSocket), and the WebSocket checks Origin. - The server never unpickles cell data. Prompt cells, SQL binds and cell tests loaded pickled values inside the server process, so a cell running as the harness user, or an artifact carried in by a snapshot import, could run code as the server. Cell-test inputs are now loaded in the test process.
- A publication cannot serve a page on the server's origin.
/p/{token}/dataused the content type the uploader declared, sotext/htmlor an SVG ran script there. - Exports follow the table ACL and stay in their tenant. Exporting an artifact read from a denied table copied it somewhere readable, and an export could overwrite another tenant's table. The ACL now also covers results derived from a denied table (an artifact or name input with a dot in it is not mistaken for a table there).
- Secret env values stay on the server, even for a
notebook:readprincipal in another tenant. - Service-mode authorization gaps closed: registry names set through materialize, the tenant context under api_key, stream ownership across tenants, cache warming past a deny rule, warm jobs and operator diagnostics visible to every tenant, a write error naming another tenant, and a WebSocket that kept working after its API key was revoked.
- strata-pool's catalogue is the operator's: any token holder could
rewrite a machine type's image and read its
env, and read every tenant's usage and workers. Tokens are compared in constant time. - Uploads are bounded before they are read: an
.ipynbimport reads at most its cap, and a snapshot whose members would expand past 16 GiB, or holding a file the import reads whole over 16 MiB, is refused, as is an artifact record of the wrong shape. - Markdown export escapes what it did not write. Raw HTML in a cell is
shown as text, as the notebook shows it;
javascript:and similar link targets become#; cell and notebook names, notes, table text and image attributes are escaped; and an image is written only for an inlinedata:image/source. - Only the publication embed card may be framed by any site; a lookalike path no longer frames the editor.
- A worker started through
uv runwarns that its parent still holds the token, and the docs startstrata-workerdirectly. - Service mode needs an auth mode. A service-mode server without
STRATA_AUTH_MODEstarted with every route open, cell execution and the admin worker registry included. It now refuses to start. - The proxy sets every identity header. The trusted-proxy contract says
the proxy sets
X-Strata-Principal, the tenant header,X-Strata-ScopesandX-Strata-Proxy-Tokenon every request, empty when the user has none; a proxy that leftX-Strata-Scopesalone let a client claimadmin:*. - The server's state is owner-only. The artifact directory, its database
and blobs, the cache directory and a notebook's server-only runtime state
are created 0700 or 0600, and existing ones are narrowed; a notebook's
.strata/and each tenant's storage directory are 0711. One harness user serves every tenant and is not a tenant isolation boundary: the service-mode docs say what separate tenants need. - Publication tokens are stored as their SHA-256, so a copy of the database holds no link this release serves. Existing tokens are hashed on upgrade and links already handed out keep working. 0.8.0 does not hash the token it is given, so run on a migrated store it serves each publication at the stored hash (see Upgrading).
- An env allowlist keeps credentials out of cells. With
STRATA_NOTEBOOK_HARNESS_ENV_ALLOWLISTset, credential-looking names underUV_*,PYTHON*andR_*,UV_PUBLISH_*andUV_INDEX_*_USERNAME, and the uv index URLs that can embed a login (UV_INDEX,UV_INDEX_URL,UV_EXTRA_INDEX_URL,UV_DEFAULT_INDEX,UV_INDEX_<name>_URL) no longer reach cells, andCONDA_*is no longer passed; a name listed exactly still passes. strata-workerlistens on loopback without a token. It bound0.0.0.0, so a worker started withoutSTRATA_WORKER_TOKENran whatever anyone who reached the port sent. With no token the default is now 127.0.0.1.- A cell cannot reach the server's files through its run directory. Run as the harness user, a cell owns the directory it writes its outputs to, and a process it leaves running keeps that directory after the cell ends. The server read a cell's outputs, displays, harness result and test results there by path, so a symlink or hard link made it store a file only the server may read, and it wrote a module export, an embedded worker's bundle and a cell-test run's files there by path, through any link planted first. It now reads and writes there without following links, refuses a file name that leaves the directory, and stages a cell-test run before handing its directory over.
- Startup refusals no longer print the configuration. A refused configuration's message ended with the settings it was given, part of the S3 secret key included, in the journal or container log. Notebook connection and worker validation errors no longer echo their input either.
Fixed¶
- An import no longer hangs a new shared server. On a service-mode
server with the shared environment backend, a notebook import followed by
an open, before the notebook's environment was built, stopped the whole
server,
/healthincluded, for up to an hour: the import's environment job held the environment's lock whileuvinstalled, and the open waited for that lock on the event loop, which the job needed to finish. The open now waits through the environment job, and a blocking environment sync refuses to run on the event loop instead of hanging it. - A refused run says why in the notebook. A frame the server refused for
want of permission (an
errorframe with coderead_onlyorinsufficient_scope: a viewer's Run All, or a cell run behind a gateway that admits reads only) was dropped by the notebook, so the click did nothing visible. The notebook now shows the server's sentence as an error notice, once while it stands. - A table Strata refuses to read says why.
POST /v1/materializereturned a bare 500 for a scan of a table it cannot read (an unreadable delete file, now too many pending equality deletes), and accepted the same table as the input of another transform only to fail later in the build; both now return 422 with the reason. - A row group left with no rows is read as empty. Building its empty
batch raised a
KeyError; equality deletes can now remove every row of one. - A SQL cell reading a catalog table under an alias runs again. Pinning
lake.taxi.trips tto its snapshot wroteAT (VERSION => n) AS t, which DuckDB rejects as a syntax error; the pin now comes after the alias. Any read of an aliased catalog table failed. - A SQL cell over a table named only at run time no longer serves a stale
result, and neither do the cells below it. Snowflake's
IDENTIFIER($var),TABLE($var)and a bare$var, a table function such as DuckDB'sread_parquet(...)orquery_table(...)or a Postgres set-returning function, DuckDB'sFROM 'file.parquet'(or"file.parquet", orfile.parquetunquoted), and BigQuery's wildcard tables andINFORMATION_SCHEMAviews name their table only when the query runs, so the default cache could not see it change and kept returning the first result. Such a cell now runs its query every time, its header says why, and its result's content is folded into its provenance, so a downstream cell re-runs exactly when the rows changed instead of serving a result computed from the old ones.# @cache sessionorttlopts back into reuse.IDENTIFIER('db.schema.table')andTABLE('t')with a literal are tracked like any table. The diagnostic for SQL that does not parse now says the cell will not run until it does, which is what happens. - Snowflake freshness probes find unquoted tables. The freshness, schema
and retention probes asked for a table's name as typed, and Snowflake stores
an unquoted
eventsasEVENTS, so a change to it was never detected. Unquoted identifiers are now looked up uppercased; quoted ones as written. - Cell code runs only in the notebook's own environment.
strata run --no-syncand thecellcommands'--no-syncran cells with whateverpythonwas on PATH rather than the notebook's.venv, and failed outright where no barepythonexists. They now use.venv/bin/python, and refuse with exit 2 when it is missing or points nowhere. Wherever the environment is not ready, cells, cell tests (the WebSocket path too) and the inspect REPL refuse and say so instead of running with PATHpythonand storing results the notebook's environment never produced, and reopening a notebook whose sync failed syncs again rather than making its cells runnable. - Postgres cells notice changes to tables named in mixed case without
quotes. Postgres stores
FROM Eventsasevents, but the freshness probe asked for"Events", found nothing, and served the cached result however the table changed. - A required column made optional no longer fails a scan. Files written before the change carried the column as not null and files after it as nullable, and the scan refused to stream the two as one schema. Every column, and every field inside a struct, list or map, now reads at the snapshot's nullability.
- Notebook uv commands act on the notebook's environment. A server
started with
UV_PROJECT_ENVIRONMENTset passed it to everyuv sync,add,lockand theuv runthat starts a cell, which then acted on the server's environment instead of the notebook's.venv; a server started underuv runpassed itsVIRTUAL_ENV, which put a uv warning in every operation log. Both are dropped now. - A transform over a table input rebuilds after a schema change. A rename
or a dropped column makes no snapshot, and a table input was versioned by
its snapshot alone, so
duckdb_sql@v1over a renamed table returned the artifact built before the rename and its name reported itself fresh. Table inputs are now versioned by snapshot and schema. - A GCS mount on its own endpoint no longer stalls for two minutes. Since 2026.6, gcsfs asks Google's Storage Control API over gRPC what kind of bucket it is, and an endpoint that is not Google's (the fake-gcs-server emulator, a GCS-compatible service) cannot answer, so gcsfs retried for about two minutes per bucket in every process that opened the mount. Such mounts now use gcsfs's standard filesystem; Google's endpoints keep its default.
- A cell's timeout in Run All counts only its own run. While Strata looked up a cell's cache entry the harness waited, and that time counted toward the cell's per-cell timeout, so a slow store (or a busy machine) could time out a cell whose code had not started. The parent's work is now off the cell's clock.
- A scan no longer returns rows DuckDB deleted. 0.8.0 refused a merge-on-read table only when pyiceberg reported delete files, and pyiceberg before 0.12 lost the ones DuckDB writes, so the scan served the deleted rows. Nothing 0.8.0 cached for a scan is reused (see Changed), so those rows are not served again.
- A scan no longer returns a dropped column's values under a new column's name. Strata read columns by name, so dropping a column and adding one of the same name, or renaming a column and reusing its old name, returned the old column's values where Iceberg has nulls. A renamed column came back under its old name, and a projection naming the new one failed partway through the stream. A schema change makes no snapshot, so the row-group cache and a scan's provenance now name the schema as well as the snapshot.
- Tag lookups on a server pointed at a team store answer from that store.
GET /v1/registry/artifactsread the local store; every other registry route already forwarded. - The metadata store closes its SQLite connections. Reads left them open until garbage collection.
/health/readyno longer reportsstuck_scans. Nothing recorded scan progress, so the check could never fail readiness.AsyncStrataClient.materializehonorspoll_interval. It polled a build every 0.5 s whatever was passed.- A blanked secret no longer hides the server's value from a cell.
notebook.tomlkeeps a secret-looking[env]name with an empty value, and a cell saw that empty string in place of a value of the same name in the server's environment until someone filled it in (and a${VAR}in a named credential resolved to it too). An empty secret-looking value is no longer passed on. # @nocacheapplies to SQL and prompt cells. Only Python and R cells honoured it, so a SQL cell with# @cache foreverkept serving its stored result.@tableno longer warns on the forms it accepts. The diagnostic asked for<warehouse>#<namespace>.<table>even for<catalog>:<namespace>.<table>and<namespace>.<table>, which a cell reads fine.strata runruns widget cells. It reported them as skipped with "unsupported language".environment.yamlimport keeps packages whose names start withpython.python-dateutiland the like were dropped as if they were the interpreter pin.- A tab closed in the last minute no longer cancels a later run. The grace teardown cancelled whatever was running when it expired, including a CLI, agent or REST run started after the tab closed.
- A full disk no longer empties a cell file. Cell, test,
pyproject.tomland console writes go through a temp file, asnotebook.tomldoes. - One slow dependency no longer stalls the server.
uv syncand the renv restore on open, the secret-manager fetch (now capped at 20 s and no longer repeated on every structural edit),POST /v1/artifacts/gcand buildingarchive.ziprun off the event loop. - A crash between storing two outputs of one run no longer mixes runs: a run's outputs become current together.
- A
mode="stream"miss nobody fetches is marked failed instead of stayingbuilding, and abandoned writes (oldbuildingrows in a notebook store, stale blob.tmpfiles) are reclaimed. - Postgres stores no longer log a pool warning on every read.
- The PyPI page shows its images and links,
index.htmlis served withCache-Control: no-cache, every response carriesX-Content-Type-Options: nosniff, and strata-client and strata-pool ship their LICENSE. - Opening a notebook while its imported environment was still building in shared mode could hang the whole server; a blocking shared-environment sync now refuses to run on the event loop.
- A personal server read a
gs://orabfs://warehouse with no catalogurithrough a SQLite file at a malformed local path and failed with a 500; it now uses theSTRATA_METADATA_DBcatalog, ass3://does. - A remote cell's console streamed to nobody: chunks were addressed to the notebook id instead of the session. It now shows as it prints.
- A SQL cell binds datetime, date, time, Decimal, UUID and bytes values (and numpy scalars) from an upstream Python cell.
- A secret typed this session survives a session reload (changing the timeout, workers, mounts, adding or renaming a cell, reopening), and saving the Runtime panel's env keeps the cells that read it ready.
strata worker add --transport, MCPadd_workerand the REST worker PUT refuse an unknown transport; the worker editor keepsmanifest/buildworkers signed instead of saving them asdirect.- A malformed table URI is a 400 and a table missing from its catalog a 404, instead of 500s; cache warm refuses a malformed URI up front.
- A local warehouse directory that does not exist and an unknown snapshot id are 404s, and a projected column the table lacks is a 400 naming it, instead of 500s.
- Promote, publish and snapshot import rewrite the version on a
strata://name/input when its artifact lands under another id. - The hardware probe answers on Windows.
- The embed snippet's resize listener accepts messages from the Strata origin only, and several snippets on one page each size their own iframe; re-copy a snippet pasted before this.
POST /v1/notebooks/recents/validateanswers only for paths under the caller's notebook root, so it no longer reveals which other directories on the server hold a notebook, and one malformed saved path no longer fails the whole check.- Service mode: a worker's live console chunks are accepted with only the signed log URL, so the console streams under trusted-proxy and API-key auth instead of arriving only when the cell ends.
- A character split across two reads on a remote worker no longer shows as two replacement characters in the live console, and the closing console no longer starts a character late after a dropped chunk.
- The web UI and the terminal viewer show only the current run's console, in the order it arrived, for local and remote cells alike: a failing cell no longer keeps its earlier runs' output, and a cache hit that replays no console shows the cell's last console live, as after a reload. The terminal viewer shows a run's variables as soon as it finishes.
- A transform over a
gs://,abfs://, schemeless/wh#ns.t, named-catalog or bare table URI, every formscan@v1reads, runs instead of answering pending and failing in the runner; an input whose plan fails answers the planner's 4xx instead of building a snapshot-less result. - Two in-flight misses for one scan both serve, and the second artifact reads
supersededinstead offailed;/v1/artifacts/usagecounts no failure for it, and strata-client returns the canonical artifact, soset_nameon it works. A version a duplicate build overtakes drops its own blob as soon as it finalizes and serves the canonical artifact's bytes, so the store holds them once instead of until the retention sweep. Notebook readers, retention, delete and promotion follow it to those bytes (a duplicated prompt cell reads its result on every run), and deleting the canonical first gives a published or pinned duplicate its own copy. - A worker whose
uv syncorrenv::restoretimes out, or whose environment registry serves a broken archive, reports alocked environment:error naming the step instead of a generic executor failure. STRATA_NOTEBOOK_PYTHON_VERSIONSaccepts a comma-separated list, as the reference documents.- Importing a Conda
environment.yamlworks on a base install; PyYAML is a core dependency. STRATA_LOG_FORMAT=textrenders metrics events as text instead of JSON lines.- Offline CLI commands that write the notebook log each annotation diagnostic once instead of twice.
- The prompt-cell "LLM not configured" error names
[env]innotebook.tomland the server'sSTRATA_AI_API_KEY, not only the Runtime panel. python -m strata --helpprintsusage: python -m strata;strata cell --helplistsoutput.- A failed persist of planning metadata to SQLite is logged at debug instead of silently dropped.
- The Azure artifact blob store reads
STRATA_AZURE_ENDPOINT_URLas the blob host with the account as the first path segment, the way the lake path and the docs do; the documented Azurite form with an account name and key now works for artifacts and presigned URLs. A URL that already ends in the account name is used as given, by the blob store and the lake catalog alike. - A SQL cell's cache key is its own query text, so edits that change the
result (for example
REALtoNUMERIC) run again; a# @cache snapshotSQL cell runs its own query; a DuckDB:namebind in the select list parses. - A
:nameinside a string whose quote is escaped with a backslash is no longer read as a bind: PostgresE'it\'s :z', and ordinary'it\'s :z'on Snowflake, BigQuery and MySQL. - A cell reading a name that more than one upstream cell defines gets it from the cell the DAG wires, in single runs, Run All, warm workers, loop seeds, SQL binds and prompt templates; before, it could silently read the shadowed value. Lineage lists only the cell the name was read from.
- A cell reading a loop cell's output runs again when the loop's result
changes, and a
start_from=<cell>@iter=kloop depends on the cell it starts from, and fails naming it when that cell's latest run stopped before iteration k (failing at the first included), instead of seeding from a step an older run left behind. - Loop cells: editing
max_iter,carry,start_fromor@loop_untilruns the loop again, where it was a cache hit on the old result; a@loop_untilthat raises fails the cell; names the predicate reads from upstream are inputs; a carry updated in place works; an extra output first read after the loop's last run is stored; a body failure with no message names its exception. - Switching a
# @per_variantcell to another sweep group runs it again, where a group whose variants share names with the old one returned the old group's results. - Prompt cells render an upstream datetime, Decimal or numpy scalar as the value, and a provider timeout names the error type.
- The dependency graph handles
import os.path, a name read before the same cell assigns it, PEP 695 generics andtypealiases, andmatchcaptures; on Python 3.14 a cell with a module-level annotated assignment can share its functions and classes. - An R cell that reads
Sys.getenv("KEY")goes stale when that variable changes, anddf$col <- valuelinks the cell to the cell that madedf. @fetchworks when the notebook's path has a space or a non-ASCII character, and a mistyped@fetchURL or a damaged.strata/fetch/index.jsonno longer stops the notebook from opening.@datasetreads the registry's version even when this clone has its own output with the same id.- A NaN or infinite widget value is refused, and a notebook that already saved
one opens again; a
Noneinside a displayed value is no longer exported as an empty string. - Cell tests get the cell's
@fetchfiles,@mountpaths, env,@datasetvalues and@tablenames and snapshot ids (one that cannot be resolved is a single errored test naming it), show stale when anything the cell reads changes, after reopening too, and are deleted with their cell. - A leaf cell reads stale, not idle, after a cell it reads is edited and re-run
or a
# @nocachecell it reads runs again. strata watchandstrata-notebook-tuiwithout the[tui]extra print one line naming the extra instead of a traceback.- Opening a notebook outside the server no longer runs
uv python liston every config load. - Importing a requirements.txt or environment.yaml no longer removes
pyarrow,orjsonandcloudpickle. - Jupyter import writes only installable dependencies, leaves
%and!inside strings and brackets alone, keeps indented blocks valid, and mapsgoogle.cloud.<x>imports togoogle-cloud-<x>, or to the package that provides one named differently (google.cloud.sql.connectortocloud-sql-python-connector). Only a;ending the last statement hides a cell's value, as in IPython, not a comment line ending in one, and an R-kernel.ipynbimports as R cells. - MCP edits reload the session once.
- The notebook page no longer goes blank when the browser blocks site data, an embedded app view included.
- After a dropped connection the notebook keeps retrying until it reconnects, and the header shows "Not connected" while it is down instead of "Live".
- Running a cell while another runs no longer leaves it spinning: the cell returns to its previous status and the notebook says another cell is running.
- Edits are no longer lost when typed while the cell runs, while the connection is down, or while an agent adds or removes a cell, and two tabs on one notebook see each other's edits instead of silently overwriting them.
- Snapshot round trips carry SQL identity and loop iterations, accept file names with spaces or non-ASCII characters, refuse malformed bundles with a 400, and export artifacts over 2 GiB.
- Downloading a notebook whose name has non-Latin-1 characters no longer fails with a 500; the file keeps its name.
- The RO-Crate description has no dangling or duplicate ids, and the publication page links an ORCID given as a URL without doubling it.
strata artifact publishand the MCP publish tool publish into the store the server actually serves (Postgres metadata, object-store blobs), and publish's withdraw hint names the right store.copy_chaincopies every ancestor before its descendants, so a published chain no longer shows phantom steps.- Registry summary and by-tag reads scope tenants correctly: a tenant sees its
own notebook stamps, and an
admin:*summary lists every tenant. - A transform build reads its inputs at the versions admission recorded, even
after an append or a name move, and a table empty at admission is read
empty, with the schema it had then; a pull-model manifest does the same for
a
strata://name/input. - A refused, cancelled or failed stream fetch frees its slots and keeps the stream expiring, so overload no longer leaks streams or leaves every later fetch answering 429. That covers a store that cannot be opened, a build that cannot start, and, with no artifact store, a client gone before the first chunk.
- A Parquet footer whose statistics cannot be converted is served from memory instead of failing the load.
name_prefixand dependents lookups match underscores literally.- On Postgres: a
GET /v1/eventsfollower no longer misses an event that commits late; deleting an artifact while one of its names is deleted no longer deadlocks; and one skipped row no longer rolls back a whole retention sweep whose blobs were already deleted. - Cleaning up failed artifacts no longer fails on a failed build's record.
- An SSH worker starts on a box whose login shell is bash, dash or sh, and removing an SSH worker in service mode answers 403 instead of 500.
- Cancelling a remote cell while its worker is fetching inputs or building the locked environment stops it before the cell runs; waiting out another cell's environment build is a "locked environment" error, not a 500.
- The Docker pool backend no longer leaves a container running when it cannot read its port.
- A generic
STRATA_AI_API_KEYin a notebook's environment overrides the server's key, as documented. - A failed transform build records the executor's
error_message. - A table with no snapshots yet reads as zero rows with its schema (scans,
transform inputs, cache warm,
@tableinputs) instead of a 500, and nothing read from it is reused. - A personal server always offers the built-in
duckdb_sql@v1: a[tool.strata.transforms]block withoutenabled = true(one holding onlynotebook_workers, say) turned it off, so it answered 400. An enabled block adds its entries to it. strata apikeywrites keys to the store the server is configured with (STRATA_ARTIFACT_DIR,[tool.strata], the metadata DSN) instead of~/.strata/artifacts, so a key minted for a service-mode SQLite store no longer gets a 401.STRATA_TRANSFORMS_ENABLEDaccepts any boolean (1,yes,false) like every other setting.- A catalog
uriwritten aspostgresql://(the form the startup message suggested) made every scan a 500 because it needs psycopg2; it now uses psycopg 3 from thepostgresextra when psycopg2 is not installed, and the docs showpostgresql+psycopg://. /health/readyanswers 503 when the artifact store's database does not answer within 5 seconds.strata_scans_total,strata_rows_returned_total,strata_row_groups_pruned_total, thestrata_table_*series,/metrics/tablesandstrata_tenant_scans_totalmove again: every completedscan@v1is counted, cache hits included.- The documented MCP URL
http://localhost:8765/mcpworks; before, only/mcp/reached the MCP endpoint. - Adding a package with a version range, extras or a marker (
pandas>=2.0,pandas<3) works from the Environment panel, REST and the WebSocket as it does from the CLI. strata artifact verifyno longer reports a notebook's JSON, image or pickled outputs, or its DataFrames, as corrupt; Arrow blobs are still parsed and row-counted, and every blob is still checked against its digest.- A server without the
sqlextra answers 400 namingstrata-notebook[sql]when a notebook with a SQL cell is opened, instead of a bare 500, and refuses a new SQL cell (REST,strata cell add, MCP) or connection before writing it, where the written cell made every later open fail. - The 403 from a personal-only artifact route and the 409 from the admin notebook-worker routes in personal mode name the setting that applies.
- A remote cell on a worker that queues its jobs shows its
startingbadge while the job is provisioned, and the badge updates no longer clear the console it has streamed. strata cell add --runreports the cell it ran asready, matchingstrata status.strata worker add, MCPadd_workerandPUT /v1/notebooks/{id}/workersrefuse a worker namedlocal: the built-in worker resolves first, so the entry never ran and could not be removed.strata agentwrites the server it starts to.strata/server.loginstead of the terminal; with--no-tuiit prints the session id straight away when its output goes to a file or pipe, andSIGTERMstops the server it started.- The starter cell of a new notebook gets an 8-character id like every other cell.
- A Codespace starts the server again:
.devcontainer/start.shwas not executable. It installs thesql,sql-duckdbandsql-sqliteextras, so SQL cells work there. - The service-mode demo's
gpu-http-signedworker runs cells: its executor now names thestratahost inSTRATA_WORKER_ALLOWED_HOSTS. - The Docker image's and the compose files' healthchecks follow
STRATA_PORT. - The arXiv example's
df-clusterworker image builds again, runs as a non-root user and refuses to start withoutSTRATA_WORKER_TOKEN. - The SDK example scripts run against the table
examples/setup_demo.pybuilds, instead of placeholder URIs. strata migratecopies publications, pins and the notebook worker registry to Postgres. Before, every/p/link answered 404 after the move, pins were lost and the workers registered through the admin routes disappeared. Runningstrata migrate --allow-nonempty-targetagain from the SQLite store, if it is still there, copies what an earlier move left behind and skips the rows the target already has.- A result published to a team store records an unknown row count as unknown
rather than 0, so
strata artifact verifyno longer flags it. A row count that is not a non-negative integer is refused with a 400. - A worker registry that names a worker twice (a 0.8.0
notebook_workers.jsonor a configured table) no longer stops the server starting, and the first admin change to such a configured table no longer answers 500. - The admin worker registry refuses a worker named
local, as the notebook's own list does: the built-in worker resolves first, so the entry never ran. strata artifact archive --tokentakes the publication's id as well as its token, asunpublishdoes.- Upgrading a store with many publications no longer scans the registry audit once per publication.
- The readiness probe's log line names the reason when the artifact store check times out.
strata apikeycreates a new store owner-only, as the server does.strata env gcremoves a collected environment's lock file.- A tenant opening a notebook outside its folder is told the notebook must be in its own folder, not "inside configured notebook storage".
- After a server restart, open notebook tabs no longer retry forever showing "Not connected": the page sees the session is gone, reopens the notebook as a new session and sends the typing the old session never received. Closing a tab with unsent typing asks first.
- The Registry tab's audit list updates after a put, promote, tag or approval, and the Profiling panel after each run.
- The Connections panel offers DuckDB, no longer labels every connection "Preserved from notebook.toml: options, credential", and shows no literal backticks.
- The browser no longer logs a warning for each
cascade_progressframe, and the lineage modal closes on Escape. - In service mode the Artifacts page shows the team's stats without an empty table under a "disabled" error.
- A cell that has never run no longer offers "Why stale?".
- Adding, removing or importing packages from the Environment panel records the environment as installed, as REST, the CLI and MCP do, so team-store publishing is no longer skipped until a reopen, and "Last sync" updates.
- In service mode a
# @workernaming a worker from the server's catalog no longer warns that it is not declared and execution will fail; one naming a worker the notebook declares, which service mode refuses, is flagged. - Jupyter import no longer drops code from a
;-suppressed cell containing U+2028, a form feed or a bare carriage return. - Loading the configuration (importing
strata.server, CLI commands, notebook runs) no longer creates theartifacts,cacheandnotebooksdirectories under~/.strata; the server creates them when it starts. - Cells that do not use matplotlib no longer import
matplotlib.pyplot, so its font-cache notice no longer shows up as another cell's stderr. - A snapshot import's
namenames the notebook, not only its directory. strata artifact publishprints the link withSTRATA_PUBLIC_BASE_URLandSTRATA_PUBLIC_BASE_PATH.- The TUI data viewer's key hint,
[n]ext [p]rev [s]ort col [e]xport csv, shows its brackets.
0.8.0 - 2026-09-27¶
A notebook stops being one person's machine. This release is about the three things that were in the way: the work runs somewhere else, it reads the organization's data, and several people share the server it runs on.
Work runs somewhere else. The worker pool that arrived in 0.7.0 stops being a single process to lose: several pool processes share one fleet through a store protocol, a Postgres store and per-worker leases. It boots Fly Machines on the private network alongside the existing Docker and RunPod backends, hands out GPUs per cell under a concurrency cap, and a cancelled cell now stops the machine it was running on. And a remote cell runs in the notebook's locked environment rather than whatever the worker image happens to hold, which is what makes a result computed there the same result.
Work reads the organization's data. A catalog gets a name and credentials
are vended rather than copied, on S3, GCS and Azure alike. A DuckDB cell reads
that catalog and the notebook's mounts together, pinned to the snapshot its
provenance names. Snowflake and BigQuery cells can pin a durable snapshot
through time travel. What a cell reads from outside a store is recorded as an
input too: bytes from a URL with @fetch, a registry name with @dataset.
Several people share the server. Cells run as a separate OS user, so a cell cannot read the server's secrets out of its own parent process. Presence, cell focus and soft locks make two people in one notebook visible to each other, and every cell records who wrote it. Notebook scopes are checked on the REST routes from the same table the WebSocket uses, and an MCP tool call runs as its caller within those scopes.
Around the edges: a result travels to the team by promotion rather than by copying a directory, a notebook's whole state exports as one bundle and imports back, and an artifact store can be swept, pinned, and reported on per tenant. A publication carries its authors and a DOI, every version carries a content digest, and a chain can be copied into a central store over HTTP and archived from it as a zip that is the same bytes on every fetch.
Added¶
- An import can upload its bytes first.
PUT /v1/artifacts/import/blobs/{content_sha256}streams a version's bytes to the store and checks them against the digest, andPOST /v1/artifacts/importwith a JSON record imports the version whosecontent_sha256names them, so a large artifact is never held in memory. Uploads are per tenant, and one nothing imports is dropped a day later. The multipart form the CLI sends still works. - An agent can look at the plot it made. A plotting cell's output used to
reach an agent as
image/pngwith no preview, which says something was drawn and nothing about what.strata cell output <nb> <cell> --out plot.pngwrites a display output to a file, and the MCP toolsave_cell_outputwrites it into the notebook's.strata/outputs/and returns the path. The cell view now also reports each output's size and artifact URI. - An agent can drive a variant group and a widget, and read what they hold.
The tab strip could switch a variant group and move a slider; an agent
holding the same session over MCP could do neither, and after running a sweep
had no way to find out what it had produced.
set_variantpicks a variant or sets the group toswitch/sweep,set_widget_valuesets a control and re-materializes the cell, and both refuse a name the notebook does not declare rather than reporting success for a change that did not happen.get_variablenames each instance of a swept variable and the spellinglineagetakes for it;get_cellreports a widget's controls with their defaults and current values;add_cellacceptswidget, which the HTTP route already did. Lineage also records the fan-out instances a consumer reads: every instance for a collapse consumer, and for a chained# @per_variantcell the one it zipped to. - The server image can use a Postgres artifact store. Setting
STRATA_ARTIFACT_METADATA_DSNon the published image previously refused to start, because thepostgresextra was not in it and installing one means building your own image. Several servers over one artifact store is the deployment the image is for. - The worker pool runs as more than one process. A store protocol, a Postgres store and per-worker leases let several pool processes share one fleet, so the pool is no longer a single point of loss. Fly Machines join the Docker and RunPod backends that shipped in 0.7.0, booting on the private network. Concurrent executions are capped and GPUs handed out per cell; the machine-type catalogue can be replaced without a restart; a dead warm machine is found before a job is sent to it; a cancelled cell stops the machine it was running on instead of leaving it billing; and a personal server offers the machine types it manages, with the server-managed registry surviving a restart.
- Remote cells run in the notebook's locked environment. A worker that
advertises it installs the notebook's
uv.lockand runs the cell there, so the result matches what the same cell computes locally. R cells run on remote workers too, against the worker'srenvlibrary. - A remote cell's console arrives while it runs. Output is streamed from the
worker as it is produced rather than at the end, so a long cell is no longer
silent for its whole life. The worker also streams manifest inputs to disk as
they download, reports the machine's hardware in
/healthand in each job's bundle, names the hosts a manifest may point at, and accepts an asynchronous execute with a provisioning deadline kept apart from the cell's own timeout. - Named catalogs, vended credentials, and GCS and Azure on the scan path. A catalog is configured once and referred to by name; a query gets credentials vended for the table it reads rather than the warehouse holding them.
- DuckDB cells over the organization's catalog and mounts. A SQL cell reads catalog tables and the notebook's mounts in one query, with catalog tables pinned to the snapshot the cell's provenance records.
- Durable snapshots for Snowflake and BigQuery.
# @cache snapshotpins a query to a point in time through the warehouse's own time travel, so a cell that reruns reads what it read before. - Named credentials for mounts and connections. A credential is named once and referred to by name; the name is part of what a cell's provenance folds, the value never is, so rotating a secret does not invalidate a cache.
@fetch: bytes from a URL are a recorded input. A cell declares a URL, the bytes are content-addressed and checked according to its policy, and the digest is part of the cell's provenance. Fetched URLs appear in lineage and are listed where the result is read; the bytes are delivered to remote workers as inputs like any other.@dataset: a registry name is a recorded input. A cell names a dataset in the registry and gets the version it resolved to, recorded as an input.- Promote a result to the team, with the work behind it.
strata artifact promotecopies an artifact and the chain that produced it into the team's store, from the notebook, from the CLI, or from inside a cell. Any cell output can be promoted, and a team cache hit says which promotion it came from. - A notebook's state exports as one bundle, and imports back. Cells, config, runtime state and the artifacts behind them travel as a single file.
- Publications record who wrote them and what cites them, land in the audit trail, and are served as an events feed. An archive bundle is served over HTTP, and a tabular bundle carries Parquet beside the archived bytes.
- Tabular artifacts can be written into Iceberg tables, with aliases kept as tags on the snapshot that version wrote.
- Cells run as a separate OS user. The harness runs as its own user and the run directory is handed to it, so a cell cannot read the server's environment through its parent process. A service host without such a user refuses to run cells rather than running them as the server.
- Presence, cell focus and soft locks. Two people in one notebook can see where each other is working, and every cell records who wrote it, in the notebook and on every surface that writes one.
- Notebook scopes on the REST routes, read from the same table the WebSocket uses, and MCP tool calls that run as their caller within those scopes. An agent can read lineage, promote and publish through MCP.
- Artifact pins, service-mode GC for admins, and a scheduled sweep, plus a tenant's usage reported in service mode and model tokens counted by tenant, principal and model.
- Quiesce a notebook or project so it can be copied consistently while the server is running.
- A shared environment backend, one environment per lockfile (and one
renvlibrary perrenv.lock), so notebooks that declare the same dependencies stop each building their own. - Trace context is carried from the server through the pool to the worker, and a signed build manifest says who and what it is for. Build manifests can carry presigned object-store URLs.
- A new notebook directory is ready for git, and a store's schema can be evolved after it already holds data.
Changed¶
Upgrading changes some behaviour that was previously silent. Each of these was a case of the server reporting more confidence than it had:
- Service mode refuses an artifact store without
STRATA_ARTIFACT_DIR. The store is created only when that directory is set, even whenSTRATA_ARTIFACT_METADATA_DSNand an object-store blob backend hold everything. A deployment with a DSN, a non-local blob backend or service writes and no directory used to boot and then fail every artifact route with a 404 or a 500; it now stops at startup and says so. - A read SQL cell may only read.
SET,PRAGMA,ATTACH,CALL,COPY … TOandEXPLAIN ANALYZEare refused, naming the statement, because a read cell's connection is a transaction its body could end. Use# @sql connection=<name> write=truefor a cell that changes a database. - A SQL cell on the default
fingerprintpolicy opens idle, not ready. That policy is a promise to ask the source before trusting the cache, and opening a notebook does not ask.# @cache forever,sessionandttlsay what they depend on and are restored when it still holds. - A cell no longer receives the team store's credentials. Its ambient client
points at its own server, and so does the same cell inside Run All, which
used to reach the team store instead with no credential for it.
strata.put(name=...)inside a cell lands in the notebook's own store and reaches the team through promotion, which is the path that can copy a chain. strata artifact export --tablerefuses a table Strata did not write, rather than replacing its contents. Export to a table of its own, or append outside Strata.- Importing a different computation under an id another one holds is
refused with a 409 instead of silently keeping the row already there. Retry
with
remap=trueto land it under a fresh id. - A worker that cannot be asked whether it runs locked environments is refused for a notebook that has a lock, instead of running the cell in the worker's own environment under provenance that claims the lock. A worker that answers and does not have the feature is unaffected.
- Deny rules match named catalogs and GCS and Azure warehouses now that a
table is authorized the same way wherever it lives. Check any deny rules you
wrote against
file:URIs. - A service host runs no cells of its own until you name a user for them.
Set
STRATA_NOTEBOOK_HARNESS_USERto a second OS account (the server must be able to become it) or send the work to remote workers. A service deployment upgrading from 0.7.0 that ran cells on the server host runs none until one of those is true. The alternative was continuing to run notebook code as the server user, which is what the rest of this release is about not doing. - Every notebook REST route is scope gated under principal auth. A
principal that holds only
notebook:readcan no longer reach the routes that write or execute, the same way it already could not over the WebSocket. Issuenotebook:writeandnotebook:executewhere a client needs them; a route nobody has classified requiresnotebook:execute. - An ACL rule refuses a key it does not recognise. A rule dropped anything
misspelled, so
tenants = "acme"(the plural) lefttenantunset and the rule applied to every tenant while still reading correctly in the file. A deployment whose ACL carries a key Strata never understood will now fail to start, naming it, rather than enforcing something narrower than it looks. - A mount's fingerprint covers its uri, so every
@mountcell is stale once after upgrade and re-runs. Nothing cached is wrong: the key changed, not the data. One consequence to know: a mounted cell's provenance now carries an absolute path, so the same notebook on two machines no longer shares those cache entries.
Dependencies now require higher minimum versions: websockets>=17.1 and
pydantic-settings>=2.15.0 for every install, and azure-identity>=1.25.3 for
the [azure] extra. These match the versions the test suite runs against; an
environment pinned below them has to move before it can install 0.8.0.
Removed¶
- The built-in AI assistant. The notebook's chat and agent panel, the
/v1/notebooks/{id}/ai/*routes, the assistant's WebSocket frames (agent_progress,agent_text_delta,agent_confirm_request,agent_done,agent_cancelandagent_confirm_response) and[ai] approval_toolsare gone, along with the settings only the assistant read:STRATA_AI_APPROVAL_TOOLS,STRATA_AI_GATES_LOCKED,STRATA_AI_APPROVAL_TIMEOUT_SECONDS,STRATA_AI_MAX_CONTEXT_TOKENS,[ai] approval_timeout_secondsand[ai] max_context_tokens. Prompt cells run as before, but the panel was also the only UI for their default model: set it with[ai] modelinnotebook.toml, or per cell with# @model. The panel's conversation history,.strata/agent_history.json, is no longer read; delete it, since it holds whatever was typed into the chat. Drive a notebook with an external agent through MCP or the CLI instead.
Security¶
- A cell cannot read the server's secrets. The worker takes its token and
credential map out of the process environment at startup and keeps them in
memory, so a cell cannot recover them from
/proc/<ppid>/environ; the harness runs as its own OS user; and the cell's manifest no longer carries the team store's proxy token, which would have let a cell assert any principal. - A read SQL cell cannot write files.
COPY … TOinside a read cell is refused before anything reaches the driver, and so are theEXPLAIN ANALYZEandEXPLAIN /*comment*/ ANALYZEforms that run the statement they describe. - A bundle writes only inside the notebook it imports. Member names, cell ids and artifact ids from an uploaded snapshot are all checked before anything is written; an artifact id becomes a blob key, so one naming a path wrote outside the notebook as the server's user.
- A citation is linked only if it is a link. An identifier recorded as a
urlis rendered as text unless it is reallyhttp(s), sojavascript:cannot become a clickable link on a publication page served to anyone holding the token. - A table export is authorized and a publication is tenant-scoped. The export route gates on the table ACL wherever the table lives, a published artifact cannot be deleted out from under its page, and the chain behind a publication is kept out of the garbage collector's sweep.
- A manifest may only point at named hosts, and a worker's job URL is checked against the manifest's own origin.
- A deny rule covers every address of a table. A table pattern names the
address a table was requested under. With a SQL catalog configured, one S3
table is
s3:under its S3 URI andfile:as a bare name or behind any other path, so a deny ons3:finance.*left the other two readable underdefault = "allow". With a SQL catalog, a deny is now checked against every name the table answers to. Allow rules still match only the name requested. The configuration reference says what each prefix names and still recommends*:finance.*deny patterns, which also cover a named catalog holding the same data.
Fixed¶
- The service-mode demo stack starts again.
docker-compose.service.ymlexited at boot: the image defaultsSTRATA_DEPLOYMENT_MODEto personal, and the environment beat the mounted[tool.strata], so personal mode refused its trusted-proxy settings. Its smoke script also hung on a new notebook's environment sync. Both are fixed and the smoke passes end to end. - Concurrent scans read each row group from storage once. Scans that missed the cache on the same row group at the same time each read it from storage; 16 cold scans of a 37-row-group table read 127 row groups. Misses on one row group now share a single read, and the rest are served from it.
- Parallel reads of one Parquet file no longer share a file handle. The
fetch pool read row groups of one file from a single cached
ParquetFilein several threads at once, which pyarrow does not support; pyarrow 25 segfaults the server on it. The fetcher now caches each file's parsed footer instead, and every read opens its own handle with it and closes it after. strata artifactopens the store the server serves. Every artifact command opened a SQLite file in the artifact directory and ignored the server's settings, so against a service store on Postgres and S3 it saw an empty store:strata artifact archive --tokenanswered "No such publication". Without--artifact-dirit now opens the configured store, from[tool.strata]andSTRATA_*like the server, metadata DSN and blob backend included; with it, exactly that local store and nothing else. A store that does not exist is reported, not created.- An import without
created_atis a 400. It reached the database's NOT NULL constraint and came back as a 500. strata newon an existing notebook leaves it as it is. It kept the notebook's id and cells but rewrotenotebook.tomlwithout its env, workers, mounts, connections, AI settings or variant groups, andpyproject.tomlwithout its dependencies. Anotebook.tomlit could not parse was replaced outright, under a new id. It now adds only missing scaffolding.- An agent's edit honours the soft lock. Edits through MCP skipped the check REST and the WebSocket make, so an agent could overwrite a cell someone changed moments ago, and its own change held nothing against them. It now waits out someone else's recent change and holds the cell in turn.
- A cell on a remote worker shows every display it made. Only the last
one travelled back, because it is also the variable
_, so a remote cell that drew three figures showed one. - Shift+Enter runs the cell. In the cell editor it inserted a newline: CodeMirror's default keymap binds Enter with a Shift variant, and it was listed ahead of the run binding at the same precedence, so it always won. Cmd/Ctrl+Shift+Enter (rerun) was unaffected. The button's tooltip and the docs said Shift+Enter all along.
- A refreshed secret replaces the one it rotated. The first fetch from a secret manager left each value in the notebook's environment, and a refresh treated any value already there as one set by hand, so a rotated secret kept its old value until the notebook was reopened. A value you set yourself in the Runtime panel still wins over the provider.
- Traces and the OpenAPI document report the installed version. Both still
said
0.2.0, as/healthdid until it was fixed. - A chain is walked past a step that was rerun. Rerunning a cell supersedes its earlier version, which a result computed from it still names and still reads. The lineage walk treated a superseded step as unknown and stopped there, so a publication's page showed the chain ending at it, and promoting or publishing to another store left everything upstream of it behind.
- A publication's archive is the same zip every time. Each member of
/p/{token}/archive.zipcarried the modification time of a file written moments before, so two fetches of one publication differed in bytes and inContent-Digestthough no file inside did.strata artifact archive --tokenbuilds that zip from the store, with the publication's authors and identifiers, and a--toending in.zipwrites it byte for byte. - A displayed value is a cached result like any other. A leaf cell's
display output was resolved before the cache decision and returned
regardless of it, so every way of asking for a fresh run came back with the
first run's value:
# @nocache, a read-write mount, "run this only", and rerun all. For a cell whose point is what it displays, a clock read, a counter, a file it just wrote, the effect those callers exist to re-trigger never happened. - A cache hit hands back the value its key identifies. A consumer of a
# @per_variantfan-out had nothing from the fan-out in its cache key, so a change to any variant came back as the consumer's old dict. Reverting a cell to an earlier value served that value's bytes under the later value's preview, and recovering from a failure through a cache hit lost the cell's display. Each stored display now records its own description, and a fan-out consumer is keyed on every variant it reads. - A fresh value reaches the cells that read it. A cell reading a
# @nocachecell's value kept serving what it had computed from an earlier one: its cache key followed the producer's provenance, which is the same on every run while the value is not. The key now follows the stored bytes for producers like that, narrowed to the variables the consumer actually reads, so a steady value beside a changing one still hits. And one run executes each cell once, whether it is a whole notebook (headlessstrata run, a cascade, Run All) or a single requested cell: a@nocacheproducer read through two branches used to execute once per branch and hand them different values. Cells whose upstream moved readstale · upstream changedrather thanidle, including after a run that failed. - A failed cell keeps what it failed with. The traceback and the prints
that preceded it went only to whoever started the run. Asking about the cell
afterwards returned an empty console, no error, and a status of
idlethat reads as "never run", so the only way to see what happened was to run the failure again. A failure is now recorded against the source that produced it, survives a reopen and a snapshot round trip, appears in Markdown and HTML exports, and stops being erased when an unrelated edit elsewhere recomputes staleness. The curated cell view also carries thedefinesandreferencesthe MCP tool description has always promised. - An agent moving a slider gets what a person gets.
set_widget_valueran the widget and stopped, so a# @livenotebook that auto-computes for a person left an agent looking at stale downstream cells and the previous answer. Both callers now run one path. A control nobody has moved also reports the declared default as its value rather thannull: that default is what the cell runs at, so reporting nothing described the storage instead of the notebook. - A widget's selection is state the notebook keeps. A
widget_updaterefused because the notebook was busy had already written the new values to disk: the reply named the run that owned the notebook, nothing re-materialized, and the next materialization quietly computed at the value the server had refused. The write now happens under the execution reservation. The values also never reached the live session, so every payload reported an empty control map and a reconnecting client could not tell a selected 0.7 from a declared default of 0.5. And a snapshot carried every cell's provenance and outputs but not the selection behind them, so an imported copy fell back to each control's default and recomputed a different scenario than the bundle was taken from. - A failed Run All publishes nothing built on the failure. Continuing past a failed cell ran every cell after it without refreshing what it read, so a cell downstream of the failure used the results from before it and reported a fresh success; a cell with side effects would have run them. Cells the failure reached are now skipped and say they did not run because something they read from failed, while cells it did not reach run as normal. A cell that reads two broken cells now reports both in one run, rather than naming one and leaving the other for the next attempt to find.
- A cell that recovers stops showing its old error. Fixing a broken cell and running something downstream of it told a client only that the cell was ready, and a client kept showing the error it had been sent over a result that was no longer wrong. The cell now sends the result that replaces it.
- Every cell a broken chain passed through says what happened. A chain fails more than once: the cell that breaks, and each consumer that could not run without it. Only the last one seen was reported, so a client running the end of a chain heard about its middle and never about its start, and the cell at fault went on showing the result it produced before the break. The kind of cell no longer decides this either: SQL, prompt and loop cells used to let the failure escape by a route that reported nothing, so one of them in a chain was passed over and one at the end took the whole run with it.
- Every message the server sends carries its own number. The protocol reference asks clients to deduplicate on it, and several batches shared one: an execution's console and its result, every cell a Run All started, every cell a failure made stale. A client following that advice dropped the rest of each batch, including the message saying a cell had finished.
- A write cell ending in a comment runs. Splitting a body into statements
kept the text after the last semicolon whatever it held, so a script closing
with
-- donehanded the database a bare comment and was told it had failed, after doing its work. A comment is not a statement now, wherever it sits, and a statement after one keeps the row count it reported. - A failed dependency says what went wrong, not only that something did. A cell that failed while being materialized for another one turned its error colour on over the result it produced before the failure, with nothing to read. It now sends what that run returned, which is also what carries the offer to install a missing package. Reconnecting to a session that is still open no longer restarts the message numbering either, which a client following the protocol reference reads as messages it has already seen.
- A watching client is told what every other reader is told. A SQL cell
that failed while being materialized for a consumer recorded its error, and
the MCP view, a sync and an export all reported it; only the live stream said
idle, because the override that makes a standing failure win was applied to the cell and not to the state the WebSocket broadcasts from. A viewer applying updates kept the table from before the failure until it resynced. Presence frames also reused the sequence of whatever was sent before them, which a client deduplicating on that number, as the reference tells it to, reads as nothing having happened. - A write cell runs the statement it declares. Each statement was
regenerated from its parse tree before being executed, and that is not always
the statement the cell contains: a named recursive CTE lost its column list,
so
counter(n)reached the database ascounterand was refused. Statements are now taken from the cell's own text. - A query holds the database, not the server. A SQL cell ran its driver
work on the event loop, so for as long as a query lasted nothing else on the
server was served and a cancel could not be delivered until the query it
meant to stop had finished on its own, at which point the run published its
result anyway. The query, the freshness probes and the write path now run off
the loop: a cancel lands at once, the run is abandoned without publishing,
and the next cell can start. The query itself keeps running in the database,
because ADBC exposes cancellation per driver and several answer
NOT_IMPLEMENTED; the protocol reference now says what a cancel does and does not stop. A SQL cell that failed while being materialized for a consumer also says so now, rather than stayingidlewith its last successful table showing, and a repaired query no longer comes back green still carrying the error before it. - Every frame the server sends carries its own sequence number. The protocol reference promises one counter that increments on every outbound message and tells clients to deduplicate on it, but a batch of staleness frames shared one number and both a sync reply and an agent note were fixed at 0. A client following that advice dropped legitimate frames, including the status saying a cell had finished.
- A SQL cell's result is a display like any other. The table was the one
display in the notebook backed by no artifact, and everything built on that
record went wrong with it: a SQL cell reached as an upstream sat at
idleshowing nothing while its value was current and in use downstream, one whose consumer had recomputed at a new parameter went on showing the table from before the change,save_cell_outputrefused it, and an export rendered the query and dropped its result. The table is now stored and recorded like any other display, without loosening the check that keeps a reopened SQL cell honest about the connection it read. A query also records the notebook variables it bound, so the chain behind a result names the value it was run at rather than stopping at the query. - A worker that answers has answered. A worker predating the health
document replies 404, which says it is older than every feature it would
list, so the cell runs in the worker's own environment, as documented,
instead of being refused. Only a transport failure or a 5xx leaves the
question open. The probe also waits longer than the
/healthit asks: reporting a machine's hardware shells out tonvidia-smi, so the first cell on a freshly started GPU worker used to time out and be refused. - Run All takes part in the team cache. It neither offered what it computed nor looked before computing, so a team running notebooks the ordinary way, top to bottom, shared nothing and reused nothing, while the same notebook run cell by cell did both.
strata status,strata cell lsandstrata cell showreport what they found. Offline they answeredidle, with no staleness reasons and no outputs, for every cell of every notebook: the state a session starts in, not one about the notebook. This is the surface agents read.@fetchmay reach your own machine.http://localhost:8000/data.csvwas refused in personal mode by a guard meant for a shared server. Service mode is unchanged.- A
@fetchor@datasetthat cannot be read says so. Both were dropped silently when malformed, so a typo became aNameErrorabout a variable nothing explained the absence of. - A cell's console is copied for a reader, or not at all. Every locally run cell's output was also accumulating in the server process for the length of the run, for a reader that discards it.
- Reconstructing a 64-bit jax array says that it changed the process.
Enabling
jax_enable_x64is process-wide and jax has no per-array form, so later cells in a reused worker get 64-bit defaults with nothing in their provenance recording it. SetJAX_ENABLE_X64in the notebook's env to make the choice explicit and hashed. - Run All sends a cell to the same place a single run does, and gives it
somewhere to promote to and the inputs to name. With a team store configured,
identical cell source used to reach a different store depending on how it was
run, with no credential for the place it was sent;
strata.promote(...)in a batch reported there was no team store;# @nocachewas ignored, so a cell marked as having an effect the artifact does not capture was served from cache; and a mount naming a credential failed to resolve. - Deciding whether a cell is stale cannot stall the server. The check reads
an
@fetchURL, an@tablecatalog and a@datasetregistry, and it now does that before taking the lock that serializes it. Held across those, one slow host blocked every socket in the process. - A cell's in-place mutations and its
@tableinputs reach a worker. Both were dropped on the way to an HTTP worker while the provenance hash claimed otherwise, so a cell that changed a value without rebinding it stored nothing and downstream cells read the value from before, from cache, indefinitely. - The reference worker image can do what it says.
/healthanswered that it builds locked environments whether or notuvwas installed, and the image shipped without it, so every Python cell failed. It probes now, and the image shipsuv. - An edit made while Run All is working is not mistaken for what ran. The batch executes the source it was given; persisting it read whatever the cell held by then, so editing a cell whose turn had not come filed its output under a hash of source that never ran.
- A loop cell checks the cache. Every other cell kind checked it before running; the loop dispatch happened before that check, so running any cell downstream of a loop re-ran the whole loop.
- A reopened cell says ready only about what it checked. A SQL, prompt or widget cell was restored on a hash that never covered its connection, its cache policy or its model, so a notebook repointed at another database reopened green, showing the first database's rows.
- A cell's console leaves the process while the cell runs. The harness captured the cell's output for the result manifest and nothing wrote it through, so the pipe a worker streams from stayed empty for the cell's whole life.
- A mount's fingerprint covers where it points, not only what is in it: two empty directories, or two paths that do not exist yet, hashed alike.
- A SQLite cell sees writes made outside it, through the file's size, mtime,
header change counter and
-walsidecar. - A catalog table is pinned however it is written: qualified, unqualified, quoted or in a different case.
- Two pool processes over one store are two processes, each with its own instance id, so a restart waits out the old leases rather than adopting them. A failed stop keeps the row, and a machine is not reaped while it is running a cell.
- Shared environments are not changed, or swept, under a running notebook,
and a failed
renv::restore()no longer destroys the library it was replacing. - A cell's dtype survives the cell boundary, and a fetch annotation is honoured on R cells and refused on loop cells rather than ignored.
- Only the runner that holds a build can fail it. A runner whose lease had moved on to another could, when its own attempt then errored, fail the build the other runner was about to finish, so a build that would have succeeded was reported failed with the stale attempt's error. Shutdown did the same to builds it no longer held.
- A request that gives up its turn in the queue passes it on. On Python 3.12, a queued scan cancelled just as a slot was handed to it left that slot idle, and the next request in line waited out its whole queue deadline, and could then be refused with a 429. Python 3.13 and later were not affected.
!=keeps rows whose value is NaN. Parquet leaves NaN out of a float column's minimum and maximum, so a row group holding only5.0andNaNwas pruned forvalue != 5.0and its NaN rows were missing from the result.- Garbage collection keeps a value while it is being rebuilt. Rerunning a cell whose last result was older than the collection cutoff let a sweep during the run delete that result, and if the run then failed, the cells reading it had nothing to read.
- Two notebooks with identical cells each keep their own values. A duplicated notebook's cells produce the same provenance under different ids, and running one took the other's current value away from the cells reading it; running the other took it back.
- A busy tenant keeps its limiter. With more than 1,000 tenants active, the registry could evict one whose scans were still running. Its next request got fresh slots, so it could run over its quota, and graceful shutdown no longer counted the scans still streaming.
- A cell that is running says so when its upstream is edited. Editing a cell's upstream while it ran reported it as stopped, and it then finished as ready although it had read the old value, so running a cell downstream offered no cascade. It now stays running and finishes stale.
- A column named
a,bhas its own cache entries. The projection fingerprint joined column names with commas, so projecting that one column and projectingaandbshared cached row groups. - A build's output is published once, by the attempt that holds it. A runner whose lease had been taken over still wrote its result over the version's bytes and marked it ready, and the runner that took over then replaced those bytes under readers, so a ready artifact's content changed and no longer matched its digest. Likewise, a pull executor still holding an earlier manifest could upload into the slot the current holder then finalized. Each attempt now writes under its own key, and publishing the artifact and completing the build commit together or not at all. The build runner removes what an attempt wrote once its build is over and nothing can still write to it, unless that attempt was the one published.
0.7.0 - 2026-09-06¶
A result can now leave the notebook without leaving its history behind. A figure gets a URL: open it and you see the plot, the code that produced it, and the code and environment of every step behind it, with no account and no install. Or a self-contained bundle to deposit with Zenodo or OSF, because a link printed in a paper outlives most servers.
What that page claims is deliberately narrow, and it says so in words. It shows what was recorded when the bytes were produced, and that they have not changed since. It does not claim the result was reproduced. That needs a re-run, and random seeds, thread counts, floating-point order and unavailable input data each break it. A green check would be read as "someone reproduced this", and a badge that can be wrong is worse than no badge. There is a badge for a README, and it reports the size of the recorded chain rather than a verdict, and is deliberately not green, because green is the badge convention for "passing" and a record is not a pass.
The result travels, too: paste the link somewhere that speaks oEmbed and it unfurls into a card, or let a repository read the chain as RO-Crate JSON-LD instead of only displaying it.
Three more things arrived alongside it, each about work you no longer have to do twice. Service mode runs on Postgres, so the artifact store is no longer one node's SQLite file. A worker pool starts machines on demand, holds them for a tenant, and stops paying when the work finishes. And the team cache lets a cell be served by a colleague's earlier run. Not a named artifact somebody chose to publish, but the expensive middle of a pipeline that nobody names.
Added¶
- Publishing names where it writes.
--artifact-dirsays where to read from, as in every other subcommand;--intosays where to publish, and defaults to the store your server serves. The destination is printed on every publish rather than only when something is copied. Previously it was resolved from configuration and never mentioned, sopublish --artifact-dir Xwrote somewhere the command line did not name. - A provenance badge for a README.
/p/{token}/badge.svgreports the size of the recorded chain and links to the page. It claims nothing about the result and is deliberately not green, since green is the badge convention for "passing". A withdrawn publication still renders, readingwithdrawn, rather than leaving a broken image. The page hands over ready-made snippets for the badge, the iframe and the RO-Crate URL. - The chain is published as RO-Crate JSON-LD. Inline on the page, at
/p/{token}/ro-crate, and asro-crate-metadata.jsonin an archived bundle, so a repository can index the provenance instead of only displaying it. Each step is aCreateActionwhose instrument is the cell source; upstream steps are described but never declared as files the crate does not contain. - A published artifact embeds elsewhere. Paste its link into a wiki or CMS
that speaks oEmbed and it unfurls into a card (the figure, a one-line
summary of the chain, and a link through to the full provenance), or take the
/p/{token}/embediframe directly. The card is framable from any origin because that is the point of it; the full page keeps its restrictive default. - Publish an artifact so anyone with the link can check it.
strata artifact publishmints a URL that resolves to a page showing the result, the code that produced it, and the code and environment of every step behind it. That is what a figure in a paper needs to be checkable by a reader with no account and no install. The page states what it checked in words rather than showing a badge: it can prove the bytes have not changed since publication (a digest is recorded then), and it says plainly that it does not claim the result was reproduced. Publishing is explicit, per artifact version, and lists the whole chain it will expose before you send the link;strata artifact unpublishwithdraws it, and the token is never reissued for other content. See Publishing an artifact. - Publishing copies the artifact into the store that serves it. A notebook
writes to its own
.strata/artifactswhile the server serves~/.strata/artifacts, so a published link could 404 unless the two happened to be the same directory. Publishing now moves the artifact and its whole chain across, keeping their ids, versions, authors and timestamps, and says how many it copied. - Archive an artifact to a bundle that needs no server.
strata artifact archivewrites a directory (the page, the bytes, a machine-readable manifest, and a README) that opens in a browser with no server and no external requests, for depositing with Zenodo or OSF. A hosted link resolves for as long as your server does; a URL printed in a paper outlives most servers. The bundle records the digest a reader gets fromsha256sum, so the one check it offers is one they can actually run. Archiving grants nobody access to a running server and mints no link. - The artifact store runs on Postgres.
STRATA_ARTIFACT_METADATA_DSNpoints the store at a real database instead of a node-local SQLite file, which is what lets more than one server share it (needs thepostgresextra). Connections are pooled re-entrantly, the build store sits on the same dialect seam, and API keys authenticate callers that do not arrive through a trusted proxy. A stream request that lands on the wrong node is redirected to the one holding it. See Service mode. - Migrate an existing store onto Postgres.
strata migratecopies an established SQLite store across, per-row and idempotently, so an interrupted run can simply be run again. It reports the rows Postgres refuses (a store that has been collecting artifacts for months has dangling references SQLite never enforced) rather than failing the whole move. Metadata only: blobs are configured separately and are not copied. - A worker pool, as its own package.
pip install strata-poolmanages machines you own: it starts one when no warm worker of that type is free, forwards the job, meters the execution, and stops paying for machines that finished. A machine belongs to one tenant for its life and is destroyed rather than handed on. Docker and RunPod backends ship; the fleet has a global cap, not just a per-tenant one. Optional HTTP service viapip install "strata-pool[server]". See Distributed workers. strata-workeris now a worker-pool worker. The pool dispatches toPOST /execute, which the worker did not serve, so a pool had no image to drive, and the one the docs named did not exist. The worker now answers that path (delegating to the same handler as/v1/execute-manifest, so the two cannot validate differently), andworker.Dockerfilebuilds the image the pool's examples reference. The payload is a build manifest: the worker fetches its own inputs and uploads its own result, so job bytes never flow through the pool. See Worker pool.- The team cache: a cell served by a colleague's run. With
STRATA_NOTEBOOK_TEAM_CACHE_ENABLED=true, a local cache miss asks the shared store whether anyone has already run this exact computation, and a cell that does run offers its outputs back. It works because a cell's provenance key carries no notebook id and no cell id. The notebook shows what the cache actually saved, and lineage now says who computed each step, on which machine, and in which environment: the questions you ask first when a result came from somewhere else. See Service mode → the team cache. - Artifacts are addressable by provenance hash, which is what makes the lookup above a single request rather than a scan.
- Unknown libraries are stored as Arrow, not pickle. A value from a library
Strata has never heard of (a DuckDB relation, a cuDF frame) is stored as
Arrow if it exports
__arrow_c_stream__or__dlpack__, so the artifact stays readable by other tools and the notebook can show its schema and row count. A DuckDB relation could not be stored at all before: it is not picklable. - Typed WebSocket payloads, with TypeScript generated from them. Fifteen
frames now have a payload model, and
frontend/src/types/ws-payloads.generated.tsis generated from those models, so a second client narrowspayloadinstead of casting. See Notebook protocol. - An empty notebook says what to do next instead of presenting a blank cell.
Security¶
- The notebook WebSocket is authenticated and enforces notebook scopes; a connection could previously drive a notebook without one.
- A web page can no longer drive the loopback notebook API. Any origin could reach a personal-mode server on localhost.
- Build manifests require trusted-proxy auth in service mode, and a build is claimed when its manifest is issued; two executors could otherwise be handed capabilities for the same build.
- Only the lease holder publishes a build's result, and ownership is checked before the artifact is committed rather than after. A fence that runs after publication keeps the build row consistent; it cannot un-publish bytes.
- ACL rules that can never match now fail closed, and
STRATA_ACL_CONFIGno longer silently discards every rule it was given. An API-key caller is authorized, not merely authenticated. - Transform inputs are tenant-gated, the table ACL applies to the cache-warm endpoints, and lineage reads are gated. Three cache-plane information leaks are closed.
- The environment attestation was forgeable and could be revoked; a notebook no longer publishes from an environment that is not its lockfile.
- The SSH worker token stays out of
argvand error messages.
Fixed¶
- A notebook artifact's lineage now walks past its first hop. Cell outputs
recorded their inputs as bare provenance digests, which neither the lineage
API nor
strata artifact lineagecan resolve, so every upstream showed up as an unidentifiable leaf and the walk stopped there. A chain three cells deep reported one node. Loop cells were worse: they recorded no inputs at all, leaving an empty graph for exactly the artifact a training run produces. Cached results are unaffected: what is hashed did not change, only what is recorded. - Cell outputs carry the source that produced them. An artifact recorded
only
source_hash, which explains nothing to a reader who does not already have the notebook. The source is now stored with the artifact, captured at execution, so editing a cell afterwards cannot pair a cached result with code that did not produce it. - Reverting a cell edit is a cache hit again. Running a cell, editing it, and reverting recomputed a result the store already held, immutable and valid. That is the one loop where content addressing should never let you wait.
- Iceberg merge-on-read tables are refused rather than silently returning deleted rows. Applying delete files is tracked separately; returning wrong rows is not a thing to leave running while it is designed.
- Foreign keys are enforced. SQLite ignores
FOREIGN KEYunless a per-connection pragma is set, and nothing set it, so a name or a build row could outlive the version it pointed at. Garbage collection now clears the rows that reference a version before removing it. - Garbage collection stopped deleting current values. An unnamed artifact's latest version is how the store resolves "the current value", and notebook cell outputs are never named, so a routine sweep deleted the live state of any notebook older than a week.
- Several ways a machine could keep billing unwatched: a pool that leaked machines it could no longer vouch for, a claimed machine nobody was watching, five paths where the RunPod backend could strand a billed pod, and freed fleet capacity with no route back to the work waiting on it.
- The migration no longer reports copying rows it dropped. Without a per-row savepoint, one rejected row rolled back every uncommitted insert in its batch, measured at 99 of 100 lost, reported as copied.
- Resource leaks under load: a QoS slot the admission actually holds is now released, the fetcher's file-handle cache is synchronized and stops closing live handles, per-client rate-limit tracking is bounded, the Prometheus scrape no longer blocks the event loop, and the v1 push executor protocol is bounded in memory.
- Data-plane correctness: Iceberg schema evolution is detected while planning rather than mid-stream, persisted Parquet stats are keyed by column path rather than leaf name, a scan's projection is validated and reflected in the plan schema, the pre-flight size estimate uses the projected columns, and a damaged cache entry no longer poisons its key forever.
- Notebook execution: a loop cell's non-carry consumed variables are stored,
fan-out cells record their base provenance so they survive a staleness
recompute, prompt-cell inputs and
max_tokensare in the cache key, DAG edges are wired for variables that shadow builtins,notebook.tomlis written atomically, REST- and MCP-driven runs take the execution reservation, and reordering cells no longer deletes the ones it never saw. - Each cell value is serialized once, not once per artifact. A cell ending in a bare variable handed the same object to two serialization loops, so a lazy query handle ran its query twice and a one-shot stream was already drained by the second write.
- Eviction pressure is measured over the time actually observed, not a constant hour, so a server that has been up for ninety seconds no longer reports a fraction of the traffic it is seeing. A burst raises the band instead of averaging away.
STRATA_GCS_CREDENTIALS_JSONaccepts inline JSON, which is what its name invites and what a container deployment usually has.STRATA_GCS_PROJECT_IDis renamed toSTRATA_GCS_DEFAULT_BUCKET_LOCATION(it never set a project), with the old name still accepted.- Also: an agent's edits reach an open browser tab, the deployment-mode badge stops mislabelling a personal server, profiling stats survive a restart, deleting a sweep variant's displayed cell no longer hides the whole group, and the TUI stops reading cell output as Rich markup.
0.6.0 - 2026-08-27¶
0.6.0 makes the notebook a coding agent's cached scratchpad and lets it reach
remote compute. The headline is running cells on a machine you reach over
SSH: hand a coding agent an SSH target and it provisions a strata-worker on
the box, opens a secure tunnel, and routes heavy cells there, cached by
provenance like everything else. Around it the agent story matures: a shipped
scratchpad skill + Claude Code plugin, the one-command strata agent
on-ramp, one-call add-and-run with leaf-cell console caching, and
runtime-hydrated module export for sharing defs that close over runtime
values. No breaking changes.
Added¶
- Run notebook cells on a remote machine over SSH. Hand a coding agent an SSH
target (a GPU box, a bigger machine, one closer to the data) and it provisions a
strata-workerthere, opens a secure tunnel, and registers it, so heavy cells run on that box, cached by provenance, while light ones stay local (# @worker local). The agent reaches for it through theconnect_ssh_workerMCP tool (it's in the managed working agreement now);strata agent --worker-ssh user@hostwires it up front. Key-based SSH only; the worker binds remote-localhost and is reachable only through the tunnel, and its generated token never touchesnotebook.toml. See Driving a notebook with a coding agent. - Register a worker without hand-editing
notebook.toml. Newstrata worker add|ls|rm|defaultCLI writes notebook-scoped[[workers]]definitions (e.g. a remote executor's/v1/executeendpoint), and matchingadd_worker/set_default_worker/list_workers/remove_workerMCP tools let a coding agent register a worker and route cells to it in a live session, and the session reloads and the change appears in any attached viewer. Previously the only way to point a cell at a worker was to edit the TOML by hand; the# @worker <name>annotation could only reference a pre-declared one. Groundwork for the SSH remote-worker path. - Add-and-run returns the cell's rendered outputs.
strata cell add … --runand therun_snippetMCP tool now return the post-run cell view, so a cell ending in a bare expression (df.describe(),total) hands its value back inoutputs. An agent gets the result without a separatecell showor aprint(). - Variable-addressed lookup: "do I already have this?" for a scratchpad.
strata cell show <dir> --var NAME(and theget_variableMCP tool) return the cell that defines a variable (its source, status, and outputs), or, if it's not defined, the list of variables that are. Lets an agent reference a value it already computed instead of recomputing it. See Notebook CLI. strata new --project-mount: read project files from a scratchpad without absolute paths. Adds a pinned, read-only notebook-level mount of the project directory as aprojectPath variable, so a scratchpad living in a subdirectory reads project files asopen(project / "file")even though a cell runs in the notebook dir. Pinned → no directory hashing and no staleness churn. Thestrata-scratchpadskill now uses it, removing the absolute-path papercut the un-primed measurement surfaced. See Notebook CLI.strata-scratchpadClaude Code plugin. The repo now ships a plugin (marketplacestrata, pluginplugins/strata-scratchpad/) bundling the scratchpad skill plus a/strata-scratchpad:scratchcommand. Install with/plugin marketplace add bearing-research/stratathen/plugin install strata-scratchpad@strata, the most reliable way to give any Claude Code session the scratchpad behavior. See Driving a notebook with a coding agent.strata-scratchpadagent skill + sharpened working agreement. Thestrata-notebookpackage now ships astrata-scratchpadskill (under<site-packages>/strata/.agents/skills/) that a coding agent discovers in any project, so it reaches for a cached notebook cell for throwaway Python instead ofpython -c/ temp scripts. Thestrata agentCLAUDE.md working agreement is rewritten to lead with a decision rule, the one-callrun_snippet, and the consumed-output caching + edit-and-rerun patterns (including a note that a cell runs in the notebook dir, so project files need absolute paths). See Driving a notebook with a coding agent.- One-call add-and-run +
strata watch- lower-friction agent scratchpad.strata cell addgains-c 'src'(inline source) and--run(execute right after adding, folding the run outcome into the JSON), and the notebook MCP server gains arun_snippettool that adds and runs a cell in a single call - so an exploratory computation costs one call, the same aspython -c. Newstrata watch [dir|--session]attaches the read-only TUI to a live session without writing any config (unlikestrata agent), so a human can watch an agent's scratchpad at any moment. See Notebook CLI and Terminal Viewer. - Runtime-hydrated module export - share a cell's
def/classeven when it closes over a runtime value. Previously a cell that defined a reusable function referencing a runtime global (an upstream artifact, or a value computed in its own cell - a loaded model, acwd-derived path) failed to share it downstream ("cannot be shared across cells yet"). Now those values are stored and hydrated into the synthetic module at load time, so the pattern works - locally, in run-all batches, on warm pools, and on remote HTTP workers (both the direct and signed/manifest transports). Only genuinely unresolvable names (bound nowhere in the notebook), top-level lambdas, and star imports still block. -
Leaf-cell console caching +
# @nocache- the notebook as an agent scratchpad. A leaf cell (no downstream consumer) that onlyprints now caches its stdout/stderr by provenance and replays it on an unchanged re-run, instead of re-executing cold every time. This makes the notebook a viable cached scratchpad for a coding agent, replacing throwaway/tmpscripts. Cells that must always run - a side effect, a live API call, a fresh random draw - opt out with# @nocache(# @nocache offre-enables); the gate applies to every language. See Cell Annotations and Driving a notebook with a coding agent. -
strata agent <notebook-dir>- one command to drive a notebook with a coding agent. The MCP server, the CLI ops, and the terminal viewer already existed, but wiring them together by hand was a fiddly, ordered dance nobody did - so a coding agent never reached for the notebook. This command collapses it: it creates-or-opens the notebook, starts (or reuses) a server with the MCP endpoint enabled, opens a session (the one step an agent can't do itself), drops a.mcp.json+ a managedCLAUDE.mdworking agreement into the notebook directory, and attaches the read-only TUI. Then you runclaudein that directory in another pane: it auto-connects, reads the agreement, and drives cells that light up live in the TUI. Needs the[mcp]and[tui]extras. See Driving a notebook with a coding agent. agent_demoexample - a coding agent's notebook, and the recipe to record it. A small, runnable notebook (examples/agent_demo/) that trains a model once and reuses it from cache when only the evaluation changes - the content-addressed payoff.examples/agent_demo/RECORDING.mddocuments the exactstrata agentflow to capture it as a demo.
Performance¶
- Cache reads skip the Rust mmap path below 4 MB.
read_file_mmaprouted every cache read through the Rust mmap path, but mmap loses toPath.read_bytes()below ~4-6 MB (syscall + FFI + copy overhead), and a single row-group cache entry is often that small. Reads underSTRATA_MMAP_MIN_BYTES(default 4 MB) and any mmap error now fall back to the plain read; larger reads still take the faster mmap path.
Fixed¶
- Case-differing variables no longer share one artifact on a case-insensitive
filesystem. On macOS/APFS (and Windows), two variables whose names differ
only in case - the idiomatic
from pkg import Tikhonovclass +tikhonov = Tikhonov(...)instance, orWidget/widget- mapped to the same blob filename, so the store wrote one over the other and every downstream read of either name got the last-written value (a confusing__call__() got an unexpected keyword argumentfar from any visible shadowing). Per-variable blob filenames (the serializer's write, the executor's input staging and output reads, the artifact store's blob key, and the direct-HTTP executor transport) now append a short hash of the exact-case name when it contains uppercase, so distinct-case names stay on distinct files, and the executor matches output files case-sensitively. All-lowercase names are unchanged, so existing caches survive. strata agentnow detects a missing MCP endpoint instead of silently proceeding. The mount check probed/mcp(no trailing slash), which falls through to the SPA and always returns200- so a server without the[mcp]extra looked "mounted", the agent got no notebook tools, and it fell back to driving the REST API by hand. The check now probes/mcp/and distinguishes the JSON MCP endpoint from the HTML SPA, failing fast with a clear hint.
0.5.0 - 2026-07-15¶
0.5.0 turns the notebook into an interactive, shareable app. The headlines
are interactive widget cells (declarative control panels whose values
downstream cells consume) with a live-updating app view you can embed in
another site as an <iframe> or export as a frozen, self-contained snapshot;
and an MCP server that lets a coding agent drive a live notebook session.
Alongside them: an interactive DataFrame viewer (page / sort / filter /
search + export), per-variant fan-out (# @per_variant), and Logs /
Artifacts operator pages. No breaking changes.
Added¶
- App view - open a notebook as a read-only interactive app. Click App
in the notebook header (or visit
/app/<sessionId>) to render just the widget control panels + display outputs - no editor, DAG, or toolbars. The connection is read-only (edits and cell runs are rejected server-side); viewers can still drive widgets, so with a widget's ⚡ Live toggle on it's an interactive "tweak a parameter, see the result" dashboard.# @app hidekeeps a cell out of the view. - Embed the app view in another site. The read-only app view can be dropped
into a dashboard/wiki/portal as an
<iframe>- pick Embed from the notebook's Export menu to copy a ready-to-paste snippet.?embed=1strips the standalone chrome and the embedded app posts its content height to the parent (strata:embed:resize) so the host sizes the frame with no inner scrollbar; widgets stay live inside the frame. Cross-origin embedding is opt-in and secure by default - a notebook is framable only from its own origin (Content-Security-Policy: frame-ancestors 'self') until you list host origins inembed_frame_ancestors(envSTRATA_EMBED_FRAME_ANCESTORS), which also closes the prior gap where no framing header was sent at all. See Cell Types → Embedding the app view. - Interactive widget cells. A new
widgetcell kind is a declarative control panel - one control per line (alpha = slider(0, 1, default=0.5), plusnumber/dropdown/checkbox/text). Each control defines a variable downstream cells consume; dragging a control marks those cells stale (run them, or flip the widget's ⚡ Live toggle to auto-run the cheap downstream cells as you drag). Values are content-addressed, so returning a slider to a prior value is a cache hit. Add one from the + menu; see Widget Cells and theexamples/widget_playgroundnotebook. (#420–#423) Omit a slider'sstep=and a readable increment is derived from its range (slider(0, 100)→1,slider(0, 1)→0.01); an explicitstep=always wins. (#431) - An interactive data viewer for DataFrame outputs. Table cell outputs now render in a scrollable grid you can page, sort (click a header), search, and filter per column - backed by the full cached artifact, not a 20-row preview - with CSV / Parquet export. The terminal viewer (TUI) gains the same paging / sort / CSV-export over the grid. (#416, #417)
- An MCP server for driving a live notebook from a coding agent. Enable
mcp_enabled(personal mode only, behind the[mcp]extra) and Strata mounts a Model Context Protocol endpoint at/mcp-claude mcp add --transport http strata http://localhost:8765/mcp. An external agent (Claude Code, any MCP client) gets the same operations thestrataCLI drives - read (list_notebooks/get_notebook/get_cell/dag/status), run (run_cell/run_tests), author (add_cell/edit_cell/remove_cell/move_cell), and dependencies (add_dependency/remove_dependency) - against a warm session, not an offline copy. Because the tools reuse Strata's broadcasting execution paths, the browser UI and the terminal viewer become a live view of the agent at work. (#117) - Per-variant fan-out with
# @per_variant. A sweep group already runs every variant and hands the whole{variant: value}dict to one downstream cell (sweep mode). Annotate a downstream cell# @per_variantand it instead fans out - running once per variant with the upstream value bound to that variant's scalar, so each variant is computed independently: its own artifact identity, its own cache entry, its own worker dispatch (# @per_variant+# @worker gpu→ N independent jobs, and adding a variant only runs the new instance). A plain downstream cell collapses the fan-out back to a{variant: value}dict. Bare# @per_variantinfers the group when the cell reads exactly one sweep group; name it (# @per_variant model) to choose among several. The web UI and terminal viewer show per-variant progress chips as the instances run. See Annotations and theexamples/model_variants_sweepnotebook. (#407, #408, #412, #413, #415) - Logs and Artifacts pages in the web UI. Two operator views: Logs
(
/logs) renders the server-log stream with filters and a live SSE tail (GET /v1/logs,GET /v1/logs/stream); Artifacts (/artifacts) lists stored artifacts - sortable and filterable, with summary stats - overGET /v1/artifacts(newsince/ sort / order filters) andGET /v1/artifacts/stats. (#402–#405) - App-view snapshot export.
strata export --app-view(and App snapshot in the notebook's Export menu) renders a frozen, self-contained picture of the app view - widget panels as their current control values, markdown, and display outputs, with no cell sources - to a single HTML/markdown file with images baked in asdata:URLs. It's the static counterpart to embedding the live app view: portable to anywhere the server can't reach (email a report, archive a run).# @app hideand prompt-response privacy are honored, matching the live app view. See Export.
Fixed¶
- Widget changes now actually re-run downstream cells. A widget cell wasn't
publishing its per-control value artifacts onto the cell, so a downstream cell
that reads a control resolved no upstream input - its provenance never
reflected the control value and it cache-hit the stale output. Dragging a
slider (with ⚡ Live on, or in the app view / an embed) now recomputes the
cells that depend on it, as intended. (widget cells now populate
artifact_urislike a Python cell's multi-output vars.) The live cascade also no longer skips sibling cells: with two outputs driven by the same control (e.g. a table and a plot), both refresh, not just the first - the cascade snapshots its target set up front so running one cell can't demote a not-yet-run sibling to idle and skip it. - Consistent notebook-header buttons. The Add cell and Export menus
referenced CSS variables that don't exist (
--border-color,--bg-secondary), so they fell back to hardcoded light colors - an off-theme white island, especially wrong in dark mode - and the Embed button didn't match its sibling nav links. The header controls now share one theme-aware style in both light and dark. - No more spurious
displaymutation warning. Any cell whose last line was a bare expression (e.g. a trailingdfto show it) emitted a false "'display' was mutated in place" warning - the harness's own injected display helper being flagged. Injected/ambient names are now excluded from mutation detection. (#418) - Downstream cells now read "stale", not "idle", when an upstream changes.
When you edit an upstream cell (or its inputs, mount, or environment change),
a downstream cell that already holds a result is now marked stale with an
"upstream changed" reason, instead of a bare "idle" with no explanation. The
web UI surfaces this as
stale · upstream changedand the terminal viewer shows the stale glyph. A never-run downstream stays idle - there is no cached result to invalidate until its upstream produces inputs. (#361) - Tidier notebook header + navigable operator pages. All the ways to get a notebook out now live in one Export menu - Markdown, HTML, the app snapshot, and the app Embed snippet - instead of a separate header pill, so the header carries fewer, more consistent buttons. The Logs and Artifacts pages gained a ← Back button (they previously only linked to the notebook list, stranding you if you'd opened them from a notebook).
0.4.0 - 2026-07-01¶
0.4.0 is a consolidation and hardening cycle. The headlines are a new read-only terminal viewer for notebooks and a full agent-facing notebook CLI, alongside an internal restructuring of the server, per-cell unit tests, broader value serialization with in-place mutation tracking, concurrency-bug fixes, and CI hardening. No breaking changes.
Added¶
-
A terminal viewer for notebooks (
strata-notebook-tui). A read-only, full-screen spectator that attaches to a running notebook session over the WebSocket protocol and renders it live - cells flip status as they run, with the detail view following the action. The detail pane splits into a code group - Source (syntax-highlighted with the one-dark theme, matching the web UI) and Tests (the cell's test source) - and a runtime group: Output (markdown cells and markdown outputs render as markdown, a DataFrame/table renders as a real table, images render inline via the terminal's graphics protocol - enlarge one full-screen withi), Console, Agent (an AI agent's reasoning streams here as it drives the notebook), and Results (each unit test's outcome + failure diff). Plus a layered ASCII DAG view (d), a per-cell run-time column, cascade / environment-job progress in the header, follow mode, per-cell unit-test result badges, a?keybinding reference, and background auto-resync so the view stays live without manual refresh. Ships behind the[tui]extra (uv tool install "strata-notebook[tui]"); it never edits or runs cells - purely for watching, e.g. an agent build a notebook in one terminal while you watch in another. See Terminal Viewer. -
A full notebook CLI for agents (
strata cell,dag,status,dep). Thestratacommand grew an agent-facing surface over one sharedNotebookOpscore: inspect (cell list/show,dag,status), execute one cell at a time (cell runin normal /--rerun/--forcemode,cell test), and author (cell add/edit/rm/mv,cell annotateto splice# @keyannotations,dep add/rm) - all with--format jsonand a stable exit-code contract (0ok /1operation failure /2invocation error), so an agent can drive a notebook as a first-class tool. Every command runs either offline against a notebook directory or against a live session on a running server via--server/--session(the same session a human watches in the TUI). See Notebook CLI and Authoring Programmatically. -
Per-cell unit tests in the notebook. Every Python code cell gets a Tests panel (the
🧪toggle next to Inspect, which doubles as a health badge:✓ 4/4green, failing red, errored amber,· stalewhen the cell or its tests changed since the last run). Writepytest-style tests against the functions a cell defines - they run as real pytest against a re-executed copy of the cell with its upstream inputs injected, so assertion rewriting, fixtures, parametrize, and marks all work (def test_x(cell): assert cell.featurize(cell.trips)…-cell.Xis any def or input after the cell ran). Test source is a committedcells/{id}.test.py; results persist in.strata/runtime.jsonand rehydrate on reopen. Driveable over WebSocket (cell_run_tests→cell_test_status/cell_test_results). Python cells only;pytestmust be in the notebook's environment (a missing-pytest run surfaces an actionable message). The generated-conftest runner is written to be liftable to a standalone plugin for CI/pre-commit later. See thepandas_basicsexample for a worked set of cell tests. -
Cell-test tooling auto-provisions. Running a cell's tests when
pytestisn't in the notebook environment now installs it on demand (a generic dev-tool provisioning path) and retries, instead of failing; a failing test's captured stdout/stderr is surfaced in the result message. Dev-group dependencies are excluded from the cell-provenance environment hash, so adding a test tool doesn't invalidate cached cell outputs. -
Broader value serialization with in-place mutation tracking. Cell outputs now serialize polars, torch, and jax values through a unified Arrow type registry (alongside the existing pandas / numpy / pyarrow support). In-place mutations -
df.sort_values(inplace=True), or mutating a numpy array / dict / list / set / torch tensor received from upstream - are detected (statically recaptured into the DAG and verified at runtime via a fingerprint registry), so provenance stays correct when a cell mutates a value it didn't define. -
General mutation detection for stateful (ML) workloads. Runtime mutation detection is no longer limited to a hand-written type registry - it falls back to a serializer-based fingerprint, so an in-place mutation of any serializable object (a
torch.nn.Moduletrained viaoptimizer.step(), an sklearn estimator, a custom class) is caught with no per-library rule. A cell that mutates an input in place but doesn't export it now warns (downstream would otherwise read the pre-mutation value), andstrata runsurfaces those warnings in both human and JSON output. Strata also warns when two of a cell's outputs share a mutable object (the optimizer-over-a-model footgun: stored as separate artifacts they decouple downstream). New Stateful objects & value semantics docs cover the one-cell training pattern. -
Variant sweep mode. A variant group can now run in sweep mode (
mode = "sweep"innotebook.toml): instead of only the active variant executing, every variant of the group runs on each execution and the downstream cell receives the group's variable as a{variant_name: value}dict - for comparing alternatives (models, hyperparameters, prompts) side by side in one downstream cell. The default stays switch mode (one active variant, single value). Per-variant input hashes are grouped into the provenance key so caching stays correct across the fan-out. In the UI the group renders as a tab strip with a sweep badge, a run-all button, and a readiness rollup; clicking a tab shows that variant's source while all still run. The CLI and WebSocket protocol expose the mode toggle. See themodel_variants_sweepexample. -
Live-mirror: REST/CLI notebook edits stream to spectators. Cell edits and runs driven over REST - e.g. by the agent CLI - are now broadcast to connected WebSocket clients, so a human watching in the TUI or the web UI sees an agent's changes appear live without a manual refresh.
Changed¶
-
Server internals decomposed (gates-first).
server.pywent from a ~6,950-line module - where all ~76 routes hand-wired their own mode/auth/tenant/QoS gates, the shape behind the service-mode gate bug the 0.3.0 registry review caught - to ~3,210 lines, in three layers: typed dependencies (strata/api/dependencies.py: distinctReadStore/WriteStore/PersonalModeStoretypes + principal/scope/tenant gates, so a route can't wire the wrong gate), services (strata/services/: pure, HTTP-free, unit-testable artifact/registry/build logic), and per-domain routers (strata/api/routers/: cache, debug, registry, metrics/health, admin, artifacts, names, builds, metadata - nine domains). A route-table snapshot test freezes the full HTTP surface (path, methods, gate count) so the move is provably behavior-preserving. No API changes. The materialize + streaming data plane stays inserver.pyfor a later cycle (its QoS/ACL coupling needs the same gate extraction first). -
WebSocket frame payloads are now typed. Notebook WS payloads were inline dicts; the execution/test, cascade, cell-status, environment-job, dag-update, impact-preview and profiling-summary frames (
cell_status,cell_console,cell_output_delta,cell_iteration_progress,cell_test_status,cell_test_results,cascade_prompt,cascade_progress,environment_job_*,dag_update,impact_preview,profiling_summary) are nowpydanticmodels validated at the emit boundary, so the protocol is self-describing for non-Vue clients. A few low-traffic frames remain inline and follow in later cycles. Wire shapes are unchanged (one stale doc field,cascade_prompt.steps→cells_to_run, was corrected to match what's actually sent). -
Runs on CPython 3.14; notebook server uses the sans-I/O WebSocket backend. The server now drives uvicorn with
ws="websockets-sansio"and raises the uvicorn floor to>=0.35.0(the first release withWebSocketsSansIOProtocol). This replaces uvicorn's deprecated legacy-asyncio WebSocket protocol, whose_drain_helpertrips anAssertionErroron CPython 3.14 the first time the server sends a frame - which broke the notebook WebSocket entirely on 3.14. No new dependency (websocketswas already required); 3.14 is now in the tested and classified matrix.
Fixed¶
-
QoS slots no longer leak when a request is cancelled. Two admission paths caught
except Exception, which missesasyncio.CancelledError(aBaseException) - a client disconnect mid-acquire would leak the interactive slot, and enough of them could wedge the limiter. Both now release on cancellation. -
QoS admission limiters reconcile with config + adaptive sizing (#185): the interactive/bulk limiter pools now track the configured and adaptively-tuned slot counts instead of drifting from them.
-
No more error-level log spam for re-importable module inputs. A cell that uses an
import numpy as npfrom an upstream cell would log an error-level "artifact still missing" line per module when that binding wasn't materialised (e.g. a producer running on a remote@workerthat doesn't ship module blobs back). Module bindings are re-importable by name, so the consuming cell just re-imports them - that case now logs at debug; a genuinely missing data artifact still logs at error. -
Actionable error when
uvisn't on PATH. A headlessstrata run(ssh, cron) where uv's install dir (~/.local/bin) isn't on PATH failed every Python cell with a bare[Errno 2] No such file or directory: 'uv'. uv is now resolved viashutil.whichwith a fallback probe of the standard installer dirs (~/.local/bin,~/.cargo/bin), and if it still can't be found, cells fail with an actionable message ("uv not found on PATH. Install uv … or add it to PATH …") - matching the existingRscript not foundguard. -
Terminal viewer no longer storms on image-heavy notebooks. The TUI's WebSocket client used the
websocketsdefault 1 MiB frame cap; anotebook_stateframe carrying base64 plot/image outputs exceeds that, so the client rejected the first frame and wedged in a fast reconnect loop (ConnectionClosedErrorin the status pill). It now connects with no frame-size cap, matching the browser client. The[tui]extra also declaresPillowexplicitly, souv tool install "strata-notebook[tui]"launches instead of failing withModuleNotFoundError: No module named 'PIL'. -
GC tracker no longer self-deadlocks. The GC callback took a non-reentrant lock that a GC pause triggered during the callback could re-enter; it's now non-blocking, removing a rare hang.
-
Filter values serialize correctly through the client integrations (#193): the filter-value serializer is now wired through the duckdb/pandas/polars adapters and the server, so typed filter literals round-trip consistently.
Internal¶
- CI hardening: a per-test
pytest-timeoutplus a job backstop convert multi-hour hangs into named 3-minute failures; process-global server state (including the rate limiter) is reset between tests so the parallel (xdist) unit-test runs are isolated; flaky wall-clock timing assertions were replaced with structural checks;tyis scoped to shipped code and the type-check is clean including warnings; the notebook WebSocket tests no longer driveTestClient's portal (a py3.12/macOS hang).
0.3.0 - 2026-06-17¶
Security¶
-
Table ACL is enforced on transform inputs (service-mode hardening): under trusted-proxy auth,
AclEvaluatorpreviously gated only the directscan@v1path, so a principal denied a table could still read it by passing it as an input to a transform (/v1/artifacts/materialize,explain-materialize, name-status). The scan path and every table-input resolution now share one_authorize_table_accessgate, so a transform input can't bypass the ACL. Personal mode (no auth) is unaffected. -
Registry authorization hardening (pre-release security review): the registry audit read is now tenant-scoped - a principal sees only its own tenant's history (
admin:*sees the whole store), matching every other registry route. Deciding protected-alias changes (approve/reject) requires theadmin:registryscope under trusted-proxy auth, and approval enforces separation of duty (the requester cannot self-approve withoutadmin:*). These close latent cross-tenant-disclosure and self-approval gaps before the registry is exposed in multi-tenant service mode; personal mode (single operator) is unaffected.
Added¶
strata-client: a slim, independent client distribution. Using Strata as a library no longer means installing the whole server. The newstrata-clientpackage depends only on httpx + pyarrow (no pyiceberg / fastapi / duckdb / pydantic, no Rust extension) and ships the full client -materialize/put/fetch/scan, the registry surface (aliases, tags, names, audit), theFilterhelpers, and the duckdb/pandas/polars/datafusion integrations (as extras, e.g.strata-client[duckdb]):pip install strata-client, thenfrom strata_client import StrataClient. It resolves its server URL fromSTRATA_SERVER_URL/STRATA_HOST/STRATA_PORT/pyproject.tomlwith no pydantic. The client and the server (strata-notebook) are independent - they share only the JSON wire protocol, neither depends on the other.
Breaking (import paths): the client moved out of the strata namespace.
from strata.client import StrataClient → from strata_client import
StrataClient; the integrations moved from strata.integration.* /
strata.duckdb_ext / strata.polars_ext to strata_client.integration.*.
The server keeps from strata.types import Filter working (it owns its own
copy of the dependency-free Filter wire types).
-
Registry dashboard in the notebook (#147–#150): the registry is now a first-class UI surface, so promotion and approvals don't have to be code. A cell that publishes with a name (
strata.put(model, name="taxi/tip-model")) is stamped with its cell and shows a promote strip right below it; the bottom drawer gains a Registry tab with the pending-approval queue (Approve / Reject - the human gate, in the UI), a names table (alias chips, latest version, tags, and a[Promote▾]champion/candidate menu), and a collapsible audit timeline; and a lineage view rendersmodel ← features ← scan ← table @ snapshot. Promote toasts the result (✓ appliedor⏳ pendingfor a protected alias). New reads:GET /v1/notebooks/{sid}/artifactsandGET /v1/registry/summary. Personal-mode only - the dashboard hides itself in service mode. -
Ambient
strataclient in notebook cells (#146): every locally-executed Python cell gets a readystrataclient in its namespace - nofrom strata.client import StrataClient/StrataClient(base_url=…)/close(). It covers the common operations (materialize,put,set_alias,set_tag,resolve_alias, …) over a lightweight stdlib client path-loaded into the notebook venv (no new dependency), is created fresh per run and closed automatically, and - like a mount or@tablevariable - is an injected tool, not a cell input, so it never affects provenance. Local execution only; remote-executor cells import a client explicitly. -
Warm Rscript pool (#81): notebooks with R cells pre-spawn R workers that have already paid interpreter startup, renv activation, and
jsonlite/arrowloads - an R cell run skips the ~1–2s cold-start tax and reportsexecution_method: "warm". Single-shot workers preserve per-cell isolation;renv.lockedits drain and respawn the pool; pure-Python notebooks and machines withoutRscriptnever start one. The pool machinery is the existing Python warm pool with a parameterized worker command - the stdin/stdout frame protocol is language-agnostic. -
Live-provider LLM tests (opt-in):
STRATA_TEST_LIVE_LLM=1runs integration tests against the real Anthropic and OpenAI APIs - unary completions, schema enforcement (native tool-use / strictjson_schema), and streaming with usage accounting - catching provider contract drift the mocked tests cannot. Each provider class skips unless its API key is present; models are overridable viaSTRATA_TEST_LIVE_{ANTHROPIC,OPENAI}_MODEL. -
Structured output degrades gracefully on minimal providers: some OpenAI-compatible servers reject
response_formatorstream_optionswith a 400 - schema-constrained prompt cells used to die on the raw provider error. Strata now retries once without the extensions, steering the model with a schema-guidance system turn and marking the result degraded; the client-side validate-and-retry loop carries full enforcement. Validation is also lenient about packaging: JSON wrapped in code fences or prose is extracted before validating instead of burning a retry on the wrapper. -
Notebook cells can target a remote shared store (
notebook_remote_store_url, shared research store): the ambientstrataclient injected into cells can now point at a central deployment instead of the local notebook server, so a team of researchers publishes/consumes datasets against one store.notebook_remote_store_headerscarries the auth the remote store needs (e.g. trusted-proxy identity/token) - set via env so secrets stay out of committed config. Unset → the ambient client targets the local server as before. -
Authenticated write-back in service mode (
service_writes_enabled, shared research store) - preview: an opt-in capability letting authenticated clients publish to a service-mode store -put,set_name,set_alias, tags - so a team can share processed datasets through one central deployment. Each write requires trusted-proxy auth and theartifacts:writescope, lands in the caller's tenant (team = tenant; can't target another team), and is attributed to the publishing principal in the registry audit. Default is off - service mode stays read-only unless you enable it, and it requiresauth_mode='trusted_proxy'(enforced at startup) so every write is attributable. Pairs with the now-resolvable registry names (above) to make a published dataset "always available to the team." -
Configurable pull-model signing secret (
STRATA_TRANSFORM_SIGNING_SECRET): the HMAC secret that signs v2-pull build URLs can now be pinned via config instead of being a random per-process value. Without it, the secret was regenerated on every restart - so in-flight signed download/upload/finalize URLs broke on restart and never matched across replicas. Set a stable value for any multi-replica or restart-surviving deployment; ifpull_model_enabledis on without it, the server logs a warning at startup. -
Approval gates on protected aliases: set
STRATA_REGISTRY_PROTECTED_ALIASES=champion,productionand moves or deletes of those aliases queue for approval (HTTP 202) instead of applying -POST /v1/registry/pending/approveapplies the change with the approver as the audit actor,…/rejectdiscards it, and every step (request, approval/rejection, the applied move) lands in the registry audit. Unprotected aliases are unaffected; the default is no gating. SDK:list_pending_changes/approve_alias_change/reject_alias_change; CLI:strata artifact pending. -
Registry layer: aliases, tags, and an append-only audit log (#129): promotion is no longer a silent pointer swap. A registry name can hold many aliases (
taxi/tip-model @ champion,@ candidate) following the post-stages industry model; artifact versions carry queryable tags (auc=0.91,validated_by=…); and every name/alias/tag mutation lands in an immutable audit written in the same transaction (who, what, from → to, when) - including names set bymaterializeitself. New SDK methods (set_alias,resolve_alias,set_tag,get_registry_audit, …), REST routes under/v1/names/{name}/aliasesand/v1/artifacts/{id}/v/{n}/tags, andstrata artifact audit [name]which renders the history (old-id@v1 -> new-id@v1). Alias refs (name@alias) are accepted anywhere the artifact CLI takes a reference. -
Prompt cells stream live (#111): LLM output renders token-by-token on the cell card as the model generates, instead of appearing all at once on completion. Schema-validation retries surface as a badge on the stream. New
cell_output_deltaWebSocket frame (ephemeral - not persisted or replayed; external WS clients that ignore unknown frame types are unaffected). - Structured streams render as partial JSON (#113): prompt cells
with an
@output_schemashow fields popping in as the model finishes them - a lenient partial-JSON parser pretty-prints the valid prefix, with a character ticker and raw-tail fallback while a field is still in flight. strata validate(#115): static notebook checks without executing anything - TOML parse (with line numbers), DAG cycle detection, and the same per-cell annotation diagnostics the server runs on open.--format jsoncarries per-cell defines/references. Exit codes mirrorstrata run.strata new(#115): scaffold a notebook directory from the CLI without the server. Idempotent on existing notebooks - re-running never orphans artifacts.- Programmatic authoring guide (#115):
docs/notebook/agent-authoring.mdis the contract for scripts and coding agents writing notebooks as plain files - the worked example is pinned by the test suite. - Per-cell
stdout/stderrinstrata run --format json(#116): read computed values back from the run payload (truncated at 10k chars) instead of screen-scraping. - Agent conversation memory survives restarts (#119): per-notebook
agent history now persists to
.strata/agent_history.json(atomic writes, 12-turn window, tool traces never persisted) so a server restart no longer wipes the conversation. Destructive-tool approval prompts also get a configurable timeout (STRATA_AI_APPROVAL_TIMEOUT_SECONDS/[ai] approval_timeout_seconds, default 120s; expiry counts as a decline). - Lake-aware cells:
@tableannotation: declare an Iceberg table input on a cell (# @table trips file:///wh#nyc.trips) and the table's snapshot id joins the cell's provenance - new data landing in the lake makes the cell stale and the normal cascade re-runs it, with<name>(URI) and<name>_snapshotinjected so the cell scans exactly the snapshot its provenance recorded.snapshot=<id>pins a cell to one snapshot forever. - Artifact inspection CLI:
strata artifact list / show / lineage / pullwork directly against a local store, no server needed.lineagerenders the provenance chain down to the lake -model ← features ← scan ← table @ snapshot- answering "which snapshot trained this model?" in one command. References accept a name,id@v=N, or a bare artifact id; name resolution is tenant-agnostic so legacy stores inspect cleanly. - Personal mode executes transforms: the embedded build runner now runs
in personal mode, so
materializewithduckdb_sql@v1executes server-side out of the box - previously the request was accepted and then sat inbuildingforever (no mode could run the full scan → transform → train → put workflow). Unknown transforms fail fast with a 400 listing what's available, and aname=on an async materialize is now set when the build completes. -
Artifact store integrity hardening (#123): artifacts are validated at finalize time (the blob must be exactly one readable Arrow IPC stream matching the recorded row count - a mismatch becomes a
failedartifact, never a serveable one);refresh=Truenow rebuilds the same artifact as a new version and supersedes the old one instead of forking a parallel identity the cache never returns; builds stuck inbuildingare swept tofailedat startup; andstrata artifact verifychecks a whole store's blobs against metadata after the fact. -
strata-notebook --notebook-dir: control where new notebooks are created. They default to~/.strata/notebooks- not the directory you launched from - so pass--notebook-dir .to use the current directory, or any path (equivalently, setSTRATA_NOTEBOOK_STORAGE_DIR). The server now also prints the active notebook location on startup.
Changed¶
-
Registry name resolution works in service mode (shared-store groundwork): resolving a published dataset by name -
GET /v1/names/{name}, alias resolution, name-status, and tag reads - used to 403 in service mode (gated as a write). These are reads, so they're now enabled and tenant-scoped (a team resolves its own namespace; cross-team is not found). Registry writes (set_name/set_alias/tags) and listing all names stay blocked - those are the next step (authenticated write-back). -
Service-mode config coherence is checked at startup (hardening): three misconfigurations now fail fast instead of silently misbehaving at runtime -
multi_tenant_enabledwithauth_mode='none'(the tenant header would be unauthenticated and spoofable, and reads aren't tenant-filtered without auth, so multi-tenancy now requiresauth_mode='trusted_proxy'), ACL rules withauth_mode='none'(ACL is only enforced under trusted-proxy auth, so the rules would never run), and transforms enabled without anartifact_dir(builds persist artifacts and need a store). -
Artifact-mode scan builds are bounded-memory: the background build for
materialize(mode="artifact")now writes each row-group chunk straight to the blob store (write-through) instead of accumulating the whole result in memory before persisting. A multi-GB scan no longer holds the full result resident on the server. (Part of decoupling the scan build from the client - see A client never poisons a scan artifact under Fixed.) -
Default cell timeout raised from 30 s to 300 s: the previous default was an easy footgun for I/O-bound cells (network pulls, slow APIs), which timed out at exactly 30 s unless a
# @timeoutannotation was added. The new default matches the core scan timeout; a genuinely hung cell is still killed at the wall, and per-cell / per-notebook overrides are unchanged.
Fixed¶
-
materialize(mode="artifact")without a store fails fast (service-mode hardening): requesting artifact (persisted) mode in a deployment with noartifact_dirused to return abuild_idthat never resolved - the background build silently no-ops with no store to write to, so the client polled forever. It now returns400up front, pointing atmode="stream"(scan without persistence) or configuringartifact_dir. -
Materialized results are readable in service mode (service-mode hardening):
GET /v1/artifacts/{id}/v/{n}/dataand the artifact metadata GET were gated as writes, so they returned 403 in service mode - an identity-scan cache hit returned a/dataURL the client couldn't fetch, and a build service couldn't serve its results. Reads are now allowed in service mode, gated by tenant (_ensure_artifact_access) and the table ACL of the artifact's inputs (so a principal denied a table can't read it back via a cached scan result - "result retrieval is ACL-gated"). Personal mode is unchanged. -
Ambient cell
strataclient survives large materialize streams (ML dogfood): a cell scanning a big lake table via the injectedstrataclient could fail withIncompleteRead- and leave the artifactfailed- on a fresh multi-row-group scan. The client read the stream in one blockingresp.read(), which let the server's send buffer fill and tripped itsis_disconnected()check, aborting the stream. The client now drains the response in chunks (as httpx does), so large scans complete. Cell execution and warm/cached scans were unaffected. -
A client never poisons a scan artifact (server-side root fix for the above): the
/v1/streamsGET no longer scans-and-persists inside the response generator. The build now runs as a decoupled, bounded-memory background task and finalizes the artifact on its own merits; the GET waits for it and then streams the persisted blob. A slow or dropped reader can no longer abort the build or mark the artifactfailed- a mid-stream disconnect leaves itready. The QoS scan slot is released the moment the build completes. -
Headless
strata runno longer drops console output on cache hits (ML dogfood): a re-run whose cells hit cache carried no fresh stdout, and the empty-console write then unlinked the file the producing run had persisted - so.strata/console/ended up holding only the cell that actually re-executed. Cache hits now leave the persisted console untouched, soprint()output stays recoverable across runs. -
Registry hardening (pre-release review): garbage collection and
delete_artifactnow respect alias pointers - a champion alias pinning an old (even superseded) version protects it from collection, and deleting an artifact cleans its aliases (audited) and tags.approve_alias_changeis fully transactional: the pending-consumption, approval audit, and the alias move itself commit together, and a pending change whose target vanished fails cleanly with the entry intact for an explicit reject. Concurrent refresh rebuilds of one artifact no longer race version allocation. Alias writes targeting the version already pointed at are idempotent no-ops (status: "unchanged") - re-running a promote cell doesn't refile approvals or spam the audit. -
Namespaced artifact names are no longer write-only (friction from the ML dogfood): names containing
/(team/dataset/raw) could be created but every read route 404'd on them. The name routes now use path converters, so slash-namespaced names - the natural registry convention - resolve, report status, and delete normally. - Legacy
_default-tenant artifacts stay nameable: artifacts written by pre-fixPUT /v1/artifactscarry tenant_default; single-tenant name requests (no tenant) may now point names at them instead of being rejected with a tenant mismatch. Real cross-tenant mismatches are still rejected. - Put-created artifacts can be named after the fact:
PUT /v1/artifactsstamped artifacts with tenant_defaultwhile the name routes resolve no-header requests to no tenant - soset_nameon an artifact you had just created was rejected with a tenant mismatch. The put route now resolves the tenant the same way materialize does. - Multi-input transforms bind inputs in caller order: the stored
transform spec sorted its inputs "for deterministic hashing", so the
build runner bound
input0/input1by lexicographic artifact id - joins could silently swap their operands depending on generated UUIDs, andf(a, b)deduplicated againstf(b, a). Input order is part of the computation now (and of provenance). Existing caches of multi-input transforms whose caller order differed from sorted order will rebuild once. - Multi-row-group scans no longer silently truncate (#121): scanning
a table whose plan spans multiple Parquet row groups or files produced
an Arrow IPC body that standard readers stopped reading after the
first row group -
materialize+fetchreturned ~1M rows from a 2.9M-row table with no error. Both the streamed response and the persisted artifact blob are now a single valid IPC stream, with regression tests over a multi-file warehouse. - Cross-process lock around renv mutations (#109): concurrent
strata runinvocations and the server no longer race on the same notebook's R environment - renv init/install/restore now take a file lock on.strata/renv-process.lock, and a held lock surfaces as a structured failure instead of corrupted state.
0.2.0 - 2026-06-03¶
Second release. Headline: R cells alongside Python in the same
notebook with cross-language Arrow handoff - first-class in the UI, with
an R environment panel (one-click renv bootstrap + package install),
automatic renv::restore() on open, and inline plot output (ggplot2 /
base graphics render to PNG). Plus run-all batching that amortises
subprocess cost across consecutive Python cells, a 60-second WS reconnect
grace so a flaky network doesn't kill a running execution, real-emulator
integration tests for the S3 / Azure / GCS mount schemes, and versioned
docs via mike.
Upgrading from 0.1.0 is non-breaking. The artifact cache stays valid -
compute_lockfile_hash was extended to fold renv.lock content but
yields byte-identical output for notebooks without one (every existing
Python-only notebook). The WS protocol gained reconnect + MessageType
frames; external WS clients that ignore unknown frame types are
unaffected. No Python API surface removed.
Added¶
R cells¶
R is a first-class notebook language alongside Python: cells execute
end-to-end, cross-language Arrow exchange works, provenance/caching is
language-agnostic, and the full UX layer ships in this release - R cells
in the Add-cell menu, an R environment panel with one-click renv
bootstrap + package install, and automatic renv::restore() on open.
The example notebook below shows the shape.
LanguageExecutor+LanguageAnalyzerprotocols + registries undersrc/strata/notebook/languages/- generalises the cell-language story beyond Python.- R DAG analyzer (
languages/r/analyze_cell.R) - defines/references via Rscript walking the parsed expression tree; source-hash cache keeps re-analysis cheap. - R harness (
languages/r/harness.R) - manifest-driven cell-execution subprocess. Reads inputs viaarrow::read_ipc_stream, runs the cell body, writes outputs as Arrow IPC (fordata.frame/ tibble), JSON (for atomic scalars / lists), or RDS (everything else, taggedr_only=true). ContentType.RDS_OBJECT = "application/x-r-rds"+ theStrataRArtifactErrorexception - Python cells consuming an R-only RDS artifact fail with a structured "re-export asdata.frame" hint instead of aNameError. Same gating in the batch harness and the warm-pool worker.renv.lockcontent participates in the env hash viacompute_lockfile_hash, so editing the lockfile invalidates R cells' cache the same wayuv.lockinvalidates Python cells'. Backward- compatible: notebooks withoutrenv.locksee byte-identical hashes._renv_synchelper +[r]block schema innotebook.toml, wired into session open: opening a notebook with anrenv.lockrestores the project library automatically (theuv syncanalogue for R).- R cells in the Add-cell menu with the correct
.Rfile extension - no more hand-editingnotebook.tomlto add one. - R environment panel at parity with Python: a stacked R card shows the
current renv state (System R vs in-sync vs lockfile-edited), a one-click
Initialize renv bootstrap (install renv → bare project library →
jsonlite+arrow→ snapshot) with live streamed progress, and a per-package Install action driven off the missing-package hint. R environment jobs stream stdout/stderr over the WS and persist a synced R runtime (lock hash + timestamp + R version) on success. - A missing-package error in an R cell surfaces a structured install hint; an erroring R cell now marks its READY downstream cells stale instead of leaving them green.
- Inline plot output for R cells: base graphics and grid-based plots (ggplot2 / lattice) render to PNG and display in the cell like a Python matplotlib figure. A bare trailing plot object auto-renders (REPL-style); multiple plots in one cell produce ordered displays.
# @mount,# @env KEY=VAL, and# @nameannotations work on R cells with no R-specific parser changes - the annotation parser is language-agnostic.- Headless
strata runexecutes R cells (previously skipped as an unsupported language) and restores the notebook'srenv.lockinto a project library first - so R and mixed notebooks run end-to-end from the CLI for CI / scheduled jobs, not only through the server. - New
examples/r_lm_vs_sklearn/notebook - Python cell builds a housing DataFrame, R cell fitslm(price ~ sqft + bedrooms + age + location), Python cell fits the same model with sklearn and prints a side-by-side comparison. - New
examples/r_mtcars_analysis/notebook - a pure-R analysis (every cell R):lm()+aggregate()+ inline ggplot2 and base-graphics plots, showing the R DAG,data.frameArrow handoff, and R-only (RDS) object handoff between R cells. - CI
r-testsjob runs on Ubuntu + macOS viar-lib/actions/setup-rwith thearrow+jsonlitepackages installed from posit/RSPM binaries, against both Rreleaseandoldrel-1. The cross-language suite (tests/notebook/test_r_cells.py) exercises Py→R→Py Arrow round-trip, R-only RDS refusal, mount injection, error shapes, cache hit/miss,renv.lockchange invalidation, inline plot capture, realrenv::restore, and env-annotation injection; a separater-examplesjob runs the R example notebooks end-to-end viastrata run.
Run-all batching¶
Consecutive Python cells share a single harness subprocess on
run all / rerun all, amortising the ~150ms cold-start across the
batch. R cells are still single-cell (Phase 2). Mixed notebooks
partition into per-language runs automatically.
harness.execute_batchlibrary entry point +--batchCLI flag - one subprocess executes a sequence of cells against a shared namespace, communicating cache-check / persist requests with the parent over JSON-line pipes.CellExecutor.execute_batchorchestration with per-cell timeout watchdog inside the batch subprocess (a hung cell can't take down the whole run).is_cell_batchablegate keeps the partitioner conservative - prompts, SQL, R cells, and any cell with# @worker/# @mount rwopt out automatically.
Reconnect resilience¶
- 60-second WS reconnect grace before the server tears down a session's execution state - a Wi-Fi blip mid-cell no longer kills the run.
MessageTypeStrEnum extracted toprotocol.py- single canonical source for every C↔S frame name, removes string-literal drift across the codebase + the docs.- New
docs/reference/notebook-protocol.md- the full client-author reference (bootstrap, auth model, reconnect grace, cold-start payload, every message type) so external clients can target the WS protocol without reading server code. notebook.tomlwrite path preserves TOML datetime values and thearray-of-tablesshape, so a saved-and-reopened notebook produces a byte-identical TOML for unchanged sections.
Mount integration tests¶
- S3 mount tests against MinIO via
testcontainers. - Azure mount tests against the Azurite emulator.
- GCS mount tests against
fake-gcs-server. - The notebook-side mount-credentials hook (
MountResolver) gets exercised against all three, so credential resolution + path normalisation regressions surface in CI rather than at first remote upload.
Rerun cells¶
↻button + Cmd+Shift+Enter rerun a single cell bypassing its cache (and rerunning stale upstreams).notebook_rerun_allWS message + UI "Rerun all" entry - cascade with cache disabled, useful when you've changed something the provenance hash can't see (a non-deterministic data source, an outside-the- notebook file the cell reads, etc.).
Versioned docs¶
- Documentation site is now version-aware via
mike. Visithttps://bearing-research.github.io/strata/- the version dropdown in the header lets readers picklatest(always the current release) or a pinned version (0.2.0,0.1.0, ...). Pre-release preview lives underdev.
Changed¶
- Docs site builds + deploys via
mikeinstead ofmkdocs gh-deploy. PRs that touchdocs/still validate--strictwithout touchinggh-pages; main pushes update thedevalias; release tags pin a versioned snapshot + thelatestalias. create_notebook'spyproject.tomlshape is built from metadata rather than templated as a string - adding a default dep is now a one-line list edit instead of a multi-place template change.MountResolverderives its TOML on-disk shape fromMountSpec.model_dump()rather than a hand-rolled mapping, so schema changes only touch one place.test_routes.py+test_ws.pyboilerplate collapses into shared helpers / fixtures - net subtraction in the test suite, fewer places to drift on protocol changes.- README's Highlights section calls out R cells, DAG view, loop cells, prompt-cell variable resolution, and auto-install hints. The buried feature list under "Quick Start" is gone.
Fixed¶
strata runwithout--no-syncno longer fails at environment sync with "env sync finished without a status snapshot". The headless runner read the session's currently-running-job slot (reset toNoneon completion) instead of the returned job's terminal status, so the default invocation aborted on every notebook before running a cell.notebook.tomlTOML datetime values andarray-of-tablesrows no longer churn on a round-trip save (#45). Pre-fix, saving a notebook with no edits would rewrite datetime fields as strings + collapse array-of-tables into inline tables, polluting git diffs./{session_id}/...routes are now owner-gated in personal mode withSTRATA_PERSONAL_MODE_USER_HEADERset (#41). Pre-fix, a request with the proxy-supplied user header could read another user's session state viaGET /v1/notebooks/{id}/cells.- Three correctness gaps in the run-all dispatcher and three in the batch dispatcher, caught by review (#34, #35, #36).
- The 1-element identifier collapse in the R analyzer's JSON emit
(
auto_unbox = TRUEwas eating single-name vectors). Wrappeddefines/referencesinjsonlite::I(). - The R analyzer walker mis-attributing reads under in-place mutations
(
df <- df[complete.cases(df), ]correctly keepsdfin references). - A Python-only artifact (a
picklevalue or amodule/*content type) consumed by an R cell now fails with a structured error naming the variable and the re-export fix, instead of aborting the R subprocess and surfacing a generic "Rscript exited without producing a result manifest" - symmetric with the existing R-only (RDS) → Python guard (#107). - A Python numpy array / non-tabular scalar read into an R cell now
warns that the value is flattened into a
data.frame(the Arrow shape metadata can't round-trip into R) instead of changing shape silently (#107). - macOS / Linux + Python 3.14 SQLite I/O flake in cache_warm tests gets
one retry (
tests/conftest.py); Icebergtemp_warehousefixture disposes its catalog engine before yield to close the related flake.
Security¶
- Phase A scorecard hygiene -
SECURITY.md, Dependabot config, least-privilege workflow permissions. - Phase C SHA pinning - every GitHub Action across every workflow
pinned to a 40-char commit SHA with the version annotation in a
trailing comment. Docker base images (the shipped image + the
df-cluster example) are pinned by
sha256digest. A Dependabotdockerecosystem keeps both digests and Action SHAs current via weekly group PRs. - Token-permission least privilege -
docs.ymlandrelease.ymldefault to read-only, escalating tocontents: writeonly in the single job that pushes the rendered site (mike → gh-pages) or creates the GitHub Release. - WS upgrade owner-gating closes the cross-session-read path noted above.
Compatibility¶
- Cache: non-breaking for Python-only notebooks.
compute_lockfile_hashwas extended to foldrenv.lock, but the extension is a no-op when the file is absent (every Python-only notebook gets byte-identical hash output). - WS protocol: the new
MessageTypeextraction is purely a refactor - frame strings are unchanged. The new reconnect-grace - per-cell-watchdog frames are additive; clients that ignore unknown frames continue to work.
- REST API: unchanged.
- Wheel ABI: still
abi3-py312(one wheel per platform covers 3.12+). - Python deps: no breaking changes; R support is fully optional (Python-only users don't need R installed).
0.1.0 - 2026-05-20¶
First stable release of Strata Notebook. The package is published on
PyPI as strata-notebook; the Python module is imported as strata.
Wheels ship for Linux (x86_64, aarch64), macOS (x86_64, arm64), and
Windows (x86_64) and are abi3-compatible from Python 3.12 through 3.14.
Strata refuses to start outside a uv-managed Python environment;
uv tool install strata-notebook is the canonical install path,
with uv add strata-notebook for project-style installs. pip
install into a hand-rolled python -m venv is rejected by the
startup guard. The notebook app boots via strata-notebook (or
python -m strata); strata-worker boots a remote worker; and
strata run | export | import covers headless notebook tooling.
Added¶
Notebook UI and lifecycle¶
- notebook home / create / open flows with recent-notebook tracking
- notebook rename, delete, duplicate, and management improvements
- per-notebook Python environments (managed by
uv) with status, sync, import / export, and async environment jobs - Python-version selection in the new-notebook flow
- inline cell display outputs: PNG images, markdown,
display(...)side effects,plt.show()/Figure.show(), ordered multiple visible outputs per cell - markdown cells for prose / documentation
- timing instrumentation and a browser benchmark for create / open flows
SQL cells¶
- SQL cell language with
# @sql connection=<name>annotation, named-bind parameters resolved from upstream cells, and an Arrow-IPC artifact produced per query - per-driver
DriverAdapterProtocol with capability flags (per-table freshness, snapshot support, separate probe connection requirement) - five built-in driver adapters:
- PostgreSQL via ADBC, freshness via
pg_stat_user_tables - SQLite via ADBC, freshness via
PRAGMA data_version/schema_version, read-only via URImode=roplusPRAGMA query_only - Snowflake via ADBC, URI-as-identity, runtime schema resolution,
write_rolefor read / write principal split - BigQuery via ADBC, credentials principal in identity, ambient-ADC
sentinel, notebook-relative credential paths,
write_credentials_pathfor read / write principal split - DuckDB (embedded) via the native DuckDB DBAPI, layered RO
enforcement (file flag + cursor-level
BEGIN TRANSACTION READ ONLY) - write cells via
# @sql write=true, with per-statement status tables # @cache fingerprint | forever | session | ttl=N | snapshotpolicies# @after <cell>ordering-only DAG annotation- Connections panel + REST API for managing
[connections.<name>]blocks, with literal auth values blanked on disk during the write round-trip - schema-discovery sidebar enumerating tables and columns visible through each connection
sql_orders_reportexample notebook demonstrating a five-cell SQL pipeline
Module export and cross-cell library code¶
- cells that mix runtime work and library code (defs, classes, literal
constants) can now share the library code across cells; the planner
slices the cell's AST, keeps the shareable parts, and validates the
slice with
symtableso each kept def / class is self-contained module_export_blockeddiagnostic surfaces pre-flight on cell open and names the specific function and unresolved variablefrom __future__ import annotationscorrectly relaxes cross-cell type-hint references (PEP 563 stringifies annotations, so the free-variable check drops them)- module-level globals written from inside a function are detected
- comprehension elements walk with loop targets locally scoped
- Python 3.14 / PEP 749 deferred-annotation behavior: annotation references
go through an explicit AST walk so the cross-cell check is consistent
across
symtable's version-dependent free-variable reporting library_cellsexample notebook walking through cross-cell library code
Deployment¶
- local service-mode demo stack, smoke script, and deployment guide
- Fly-hosted notebook defaults use persistent notebook storage and a larger auto-extending volume configuration
- Docker builds reuse uv and Cargo caches more effectively for faster local iteration
Release infrastructure¶
pip install strata-notebook/uv add strata-notebook(the barestrataname on PyPI was held by an unrelated config framework)- wheel ships the frontend SPA bundled at
strata/_frontend/, sostrata-notebookworks out of the box without a separate frontend build - abi3-py312 wheel format - one wheel per platform covers Python 3.12+
- tag-driven release workflow with TestPyPI auto-publish and PyPI publish gated by a protected GitHub Environment
- post-build wheel smoke test (
wheel-testjob) installs the Linux x86_64 wheel into a fresh uv venv, exercises console scripts and/health, asserts the bundled SPA is served at/, and runs the matrix against Python 3.12 / 3.13 / 3.14 - packaging bugs fail the run before the artifact reaches the index
Changed¶
- markdown rendering uses
markdown-it+DOMPurifyrather than a hand-rolled renderer, with consistent output between in-place cell preview andMarkdown(...)display outputs - docs split into separate Strata Core and Strata Notebook quickstarts; the root README is an umbrella landing page
- notebook create bootstraps the initial environment asynchronously, making first open substantially faster
- notebook open / create flows reuse prefetched state and lazy-load secondary panels to reduce perceived latency
- add-cell UI replaces per-type buttons with a unified menu
- write-cell status table preserves per-statement rowcounts and is no longer truncated to a default cap
- connection-editor UI fixed for round-trip fidelity (auth blanking, driver-specific extras, theme correctness) and dark-mode parity
Fixed¶
- service-mode session discovery / reconnect policy and related UX regressions
- reconnect metadata loss for remote execution state
- run-all only executing the first cell
- missing-package install UX in the cell output area
- local service-mode browser routing and notebook creation flow
- relative connection paths now resolve against the notebook directory, not the server CWD
- recent-notebook list is server-validated on home-page load so deleted notebooks no longer land the user on a "session not found" toast; a Clear button next to the section title wipes the local list without touching on-disk notebooks
update_notebook_connectionsis now idempotent when no[connections]block exists and the request is empty - saves with no change no longer churnupdated_ator rewrite the on-disk TOML shape (invariant 6)- timing-based perf assertion in
test_concurrent_scans_dont_block_each_otherreplaced with a structural correctness check (no more CI flakes from runner load)
Security¶
- read-only enforcement for SQL cells is layered (file-handle flag + session-level guard) rather than SQL-text keyword filtering - a SQL cell cannot write to the database regardless of how the connection was specified
- BigQuery / Snowflake adapters route reads and writes through different
principals when configured (
write_credentials_path,write_role), withread_onlykwarg oncanonicalize_connection_idso changing the write principal does not invalidate read-cell caches
0.1.0a2 - 2026-05-20¶
Third release-validation dry-run. Four changes from 0.1.0a1:
- Wheel smoke-test job added to the release workflow. After the
five wheel matrix jobs finish, a new
wheel-testjob downloads the Linux x86_64 wheel, installs it into a fresh uv-managed venv, and exercises import + console scripts (strata,strata-worker) + server boot +/health+ the served SPA at/. The TestPyPI publish job now depends onwheel-test, so a packaging bug fails the CI run before the artifact reaches the index. Catches the class of bug we hit on0.1.0a0(missingpackagingdep would have been caught locally instead of in the smoke test we ran after the publish failed). GET /assertion in the smoke test.server.py::_mount_frontend()silently skips mounting the SPA whensrc/strata/_frontend/index.htmlis absent, so a wheel without the bundle would still pass/health. The smoke now also fetches/and asserts the response is the SPA index (grep for<!doctype html).abi3-py312forward-compat matrix onwheel-test. Same wheel is installed and smoke-tested against Python 3.12, 3.13, and 3.14 via a job-level matrix. The release contract is "one wheel per platform covers 3.12+"; this validates it against every minor uv knows about.workflow_dispatchrecovery now checks out the tagged ref. Previously the manual-rerun path checked out whatever branch the user dispatched from - ifmainhad moved since the tag, the rebuilt wheels would have the tagged version label butmain's source. Now every checkout uses${{ inputs.version }}→v${inputs.version}for dispatch, falling back togithub.reffor the tag-push path.
This alpha will approve the PyPI gate (unlike a0 / a1 which
rejected it) to validate the PyPI trusted-publisher config + the
GitHub Release creation job before claiming the stable 0.1.0 slot.
0.1.0a1 - 2026-05-19¶
Second release-validation dry-run. 0.1.0a0 uploaded all 5 platform
wheels to TestPyPI successfully but the sdist was rejected with
HTTP 400 ("License-File LICENSE does not exist in distribution
file") - maturin's sdist is built via cargo package rooted at
rust/ and didn't pick up LICENSE and README.md from the repo
root. Added both to [tool.maturin] include with format = ["sdist"]
so they land in the archive matching the PEP 639 metadata.
The pipeline never published to PyPI on 0.1.0a0 because the
TestPyPI failure short-circuited the run. 0.1.0a1 is the retry
with the fix; no other changes from 0.1.0a0.
0.1.0a0 - 2026-05-19¶
Release-validation dry-run. The first tagged release in the project's
history; exercises the full publish pipeline (multi-platform wheel
matrix, TestPyPI auto-publish, manually-gated PyPI publish) before
the stable 0.1.0 cut. The wheel content is identical to what 0.1.0
will ship; only the version label differs. Anyone installing
strata-notebook==0.1.0a0 from PyPI will get a working install with
the feature surface planned for 0.1.0 (described above); the alpha
label exists so the version slot can be discarded if the dry-run
surfaces any release-pipeline bugs.
The first stable release is still planned as 0.1.0. See the section above for the feature inventory; this dry-run aims to validate that the inventory ships correctly.