Releases: tompassarelli/fram
Release list
FINAL Fram release v0.8.0
Fram is absorbed into Beagle as beagle store.
v0.8.0 is the last Fram release.
This release was built and verified against pinned Beagle v0.23.0 (487191b25f2493a5e8b50df0bc9421b12c7843bc).
See the absorption header in the README.
No binaries are attached: the producing workflow did not leave release artifacts for this exact commit.
v0.7.0
Fram v0.7.0 is a breaking semantic release. It separates proposition content from the occurrences that assert or retract it, moves every maintained transport to FRAMRPC 2.0, completes the structured Beagle migration, and ships sealed Bun, Cloudflare, Wasm, and native artifacts.
Highlights
- Explicit occurrence history. A proposition remains a recursive
Triple, while an assertion or retraction is now anOperationOccurrencecontaining its coordinate, action, and proposition. System history no longer masquerades as ordinary user graph data. - FRAMRPC 2.0 across every maintained surface. The JVM and native servers, Bun client, Cloudflare adapter, C embedding boundary, and Wasm embed host now agree on the new occurrence and mutation records.
- Occurrence-correct state. Live scans preserve equal assertion multiplicity, Datalog keeps its structural set projection, and successful targeted retractions expose an explicit withdrawal from retraction occurrence to assertion occurrence.
- Structured Beagle authoring and compiler boundary. Fram's graph-authored sources use structural parameter declarations and the native builder consumes a versioned, fail-closed Beagle report with the deterministic-parallelism obligation.
- Sealed release artifacts. The release contains the Bun FRAMRPC client 0.5.0, Cloudflare Durable Object adapter 0.3.0, wasm32 embed archive, and x86_64 Linux musl native archive, each paired with a receipt.
- Faster reproducible publishing. Content-addressed Wasm and native identities are built once, checked from independent source roots, packaged independently, and cached for exact-input retries.
Breaking changes
- FRAMRPC wire version 1 is not negotiated or translated; stale v1 clients and servers fail closed. Consumers must upgrade together.
- Occurrence query rows are explicit
{ coordinate, action, proposition }records instead of manufactured history Terms. - Mutation results report
stateChangedand the operation coordinate instead of a changed flag plus manufactured history Terms. - Retired flat-log and dump migration paths, boundary compatibility aliases, historical cutover machinery, and obsolete code-edit tooling were removed. This pre-1.0 line breaks forward.
- The C embedding ABI remains independently versioned as ABI v1; that label is separate from FRAMRPC wire version 2.0.
Native build corrections
- Preserve reachable union and error definitions in Beagle entry projection.
- Recover complete fail-closed QBE refusal reports from their actual transport.
- Require the compiler's deterministic-parallelism obligation and versioned
beagle-native-report/v1marker.
Full Changelog: v0.6.0...v0.7.0
v0.6.0
FRAM v0.6.0 makes the native, Bun, and Cloudflare paths independently reproducible release surfaces, and adds the application-level safety needed to build on them without changing FRAM’s neutral recursive-Triple kernel.
Highlights
- Bun-first official client.
@tompassarelli/framrpc0.4.0 is a reproducible, zero-runtime-dependency package for Bun 1.3.13+. The root entry point provides TCP;@tompassarelli/framrpc/coreprovides the same thirteen-operation client over an injected exact-frame transport; generated TypeScript declarations ship with both. - Guarded application writes. The new
@tompassarelli/framrpc/schemaentry point provides single-value replacement, unique create/upsert, guarded single- and multi-subject updates, and combined create/update transactions. Every attempt reads one pinned snapshot, preflights the exact batch, and commits withexpected-versionOCC. - Deterministic ordered queries. Structured queries now support stable
orderBy/:order-byandlimit/:limit, with natural Term ordering, exact i64 behavior, a canonical full-row tie-breaker, and global top-K semantics. Bound-attribute text queries now build only the relevant text-index rows. - Native backup and restore.
bin/fram-backupcreates and verifies a checkpoint-bound backup of the authoritative FRAMLOG prefix, its SpaceId and served version, and the exact native artifact receipt. Restore is gated against fresh storage and the recorded artifact identity. - Cloudflare Durable Object release path.
@tompassarelli/fram-cloudflare-do0.2.0 shares the official portable FRAMRPC transport, ships typed adapter and seam declarations, and adds exact FRAMLOG export/restore with transactional publication and fail-closed recovery from interrupted replacement. The release also carries the matching deterministic Wasm envelope and a launch-blocking, wiki-shaped 128 MiB capacity gate. - Stronger mutation atomicity and native operation. Writes, batches, and successful lease mutations encode their predicted success response before append, so response depth or frame-size failure leaves state and version unchanged. Native storage/build-cache work reduces copying and preserves compiler/provenance closure across worktrees.
- Reproducible supply chain. Native, Bun, Cloudflare adapter, and Wasm archives are normalized and paired with closed receipts binding SHA-256, source commit, annotated tag object, package/target identity, and—where applicable—the pinned Beagle compiler and third-party notices.
Upgrade and compatibility notes
- The official checkout client moved from
clients/node(Node 20+) toclients/bun(Bun 1.3.13+). Install the shipped@tompassarelli/framrpc0.4.0 tarball, or use its runtime-neutralcoreentry point with an owned transport. - FRAMRPC remains the closed thirteen-operation v1 data surface, with checkpoint kept as a separately named native operator capability. Structured-query encoding expanded for ordering and limits, so upgrade v0.6 clients and servers together instead of assuming mixed-version query compatibility.
- Schema constraints remain application-layer guarantees. Direct FRAMRPC writes can bypass uniqueness, cardinality, and reference guards.
- Mutation batches are capped at 247 actions and may now refuse before commit when the exact predicted receipt exceeds the wire budget. Nontrivial reads should continue to paginate; the unpaged response ceiling is 248 rows.
- The Cloudflare adapter requires Bun 1.3.13 for its package toolchain, a Worker compatibility date of
2026-03-15or newer, a named Durable Object, and a SpaceId no longer than 1,024 UTF-8 bytes. Keep the raw namespace and export/restore behind a separately authorized backend/admin boundary; expose only the exchange service binding to applications. - FRAM remains pre-1.0 and has no engine access control. Keep the native socket private and isolate by process, network, SpaceId, and FRAMLOG.
Release assets
Each archive has a sibling receipt; verify the archive against the archive-sha256 recorded there.
tompassarelli-framrpc-0.4.0.tgz— Bun FRAMRPC client, portable core, schema client, declarations, backup module, and licenses.tompassarelli-framrpc-0.4.0.receipt.txt—fram-bun-release-receipt/v2provenance and archive digest.tompassarelli-fram-cloudflare-do-0.2.0.tgz— typed Durable Object adapter and seam package.tompassarelli-fram-cloudflare-do-0.2.0.receipt.txt—fram-cloudflare-do-release-receipt/v1provenance and archive digest.fram-v0.6.0-wasm32-wasm-embed.tar.gz— deterministic Wasm engine, seam ledger, native provenance, licenses, and third-party notices.fram-v0.6.0-wasm32-wasm-embed.receipt.txt—fram-cloudflare-wasm-release-receipt/v2Wasm, seam, toolchain, provenance, and archive digests.fram-v0.6.0-x86_64-linux-musl.tar.gz— static native server executable and licenses.fram-v0.6.0-x86_64-linux-musl.receipt.txt—fram-native-release-receipt/v2executable/provenance and archive digests.
Full Changelog: v0.5.3...v0.6.0
v0.5.3
v0.5.2
v0.5.1
v0.5.1
Fram v0.5.1 gives a store a second line: forking seals the active tail into a
content-addressed segment and hands parent and child a fresh tail each, so a
branch splits off without copying or merging anything. It also fixes two
native memory defects that made large stores unbootable or unsnapshottable,
revives codegraph onto the occurrence store behind a CI gate that runs it
instead of only reading its source, repairs a CI pipeline ordering bug that
had broken every push since the beagle pin landed (including the v0.5.0 tag
push itself), and declares three previously undeclared tests in the CI
manifest — each excluded with evidence, still not run — plus a fourth,
newly declared and running.
This is a patch release: the store format is unchanged for anything that has
never forked, and every fix below is a defect correction against v0.5.0
behavior, not a new surface.
Highlights
Store forking
A store forks by sealing its tail into a content-addressed segment and giving
both parent and child a fresh tail over the shared chain. Sealing is a
rename: a segment stays byte-identical to the FRAMLOG it was, so the existing
frame CRC and decode path cover it unchanged. Fork is O(1) in store size —
the cost is one SHA-256 of the active tail, a constant that seal policy
bounds — because no triple is copied and no log is rewritten. A never-forked
store gains no files and keeps its tail at flag 0, so it still boots down
exactly the path it did before forking existed.
Branch refs (<log>.refs/, framref/v1) carry the segment list explicitly,
since segments embed no parent pointer; adding one would change segment bytes
and cost the seal-is-a-rename property. Fork holds the lifetime lock for the
store and both tails and refuses when a writer holds one; every file it
installs is built at a pending name first, with a framfork/v1 marker
bracketing the rename sequence so a crash mid-fork is legible on open and
finished by replaying the remaining renames in order. Two consecutive forks
with no write between them refuse with :segment-already-sealed, because the
two empty continuation tails hash alike and a ref may not name one segment
twice.
The default branch — the one whose tail is the store file itself, unnamed in
<log>.branches/ — is spelled main, on the git prior for a checkout's
starting line. No store has forked before this release, so there is nothing
on disk the respelling migrates. Branch, fork, segment, and sealed are
ledgered in the naming ledger.
Tests: framref_codec_test.clj (38 checks), framlog_fork_test.clj (40),
and framlog_chain_boot_test.clj (15) all pass. The chain-boot suite pins
the equivalence oracle — a commit script folded through one file and through
a segment-plus-tail chain must produce the identical store image — and a
never-forked store's behavior is unchanged structurally, not just by
assertion.
Two native memory defects, found and fixed
Boot fold. fold-store! sliced the borrowed FRAMLOG vector into 1 MB
subvecs for the streaming API, and two store update paths — active-bucket
positions and derived operation liveness — copied a whole vector per
operation during a fold. On the 936k-operation, 138 MB reference framlog,
RSS grew convexly past 56–60 GB without binding; the process was killed.
After the fix (whole-vector fold, cell-backed positions and liveness during
the fold only), the same log binds at 7.28 GB peak / 6.25 GB serving RSS in
161.6 s. tests/native_stage6_compare.sh — the harness's own name for this
check — takes a pre-fix and a post-fix observation directory and byte-diffs
all 7 store artifacts each run produces (history, live occurrences, live
propositions, invalid-coordinate and malformed-term corpus records, the
term-store dump, and their digest manifest); all 7 agree, so the fold
rewrite changed how the fold runs, not what the store observes afterward.
Snapshot encode. rpc/checkpoint on the same class of store drove RSS
from 6.25 GB to over 20 GB in under 7 seconds while writing zero image bytes
— observed as high as 44 GB in production. frame-record! built the image
with swap! ... into, which lowers to a fresh arena vector holding the whole
accumulation on every record; a payload staged in its own growing vector cost
the arena a second full copy of the image on top of that. Row records now
declare their payload length and write straight into the output encoder,
with finish-record! CRCing the range it just wrote and refusing a writer
whose bytes disagree with its declaration. After the fix, the same
checkpoint costs +2.04 GB over serving baseline and writes a
100,863,531-byte image in 42.7 s, run against a store folded in the same
encode-fix session — the 159.7 s / 6.55 GB fold that is also the image-boot
comparison baseline below. The fixed encoder's image has
the same fingerprint as the main-tip encoder's image on the same store,
modulo the sidecar stamp — a cross-encoder check on the fixed generation,
not a before/after comparison against the pre-fix encoder, which produced no
usable image to compare against.
Snapshot image boot is now the fast restart path. On the same store,
image boot takes 117.4 s at 4.3 GB serving RSS afterward, against 159.7 s and
6.55 GB for a full fold measured in this same encode-fix run — a separate
fold pass from the 161.6 s / 6.25 GB boot-fold figure above, run to produce
a fresh comparison baseline rather than reused from it. Before the encode
fix, a checkpoint was not a viable way to reach that image at this store
size at all.
These numbers are measured on one reference corpus on one machine; they are
not the certified limits matrix and say nothing about other store shapes.
Codegraph revived onto the occurrence store
The five codegraph modules that rented the removed fact-and-CID store
(new-store / entity! / value! / fact! / by-lp / by-pr /
fact-of / live? / current-facts) had not run since it was removed. They
are ported onto the kernel that replaced it, where the unit of write is a
Triple of Terms committed in a transaction and the unit of read is a live
proposition. Node identity now comes from the thing each node already is —
the fact stream's own id, or a def's own name — instead of an opaque id the
old store minted. Supersession follows the kernel: a rename retracts the old
proposition and asserts the renamed one in one transaction, and the store's
withdrawal link stands in for the old reified supersedes fact.
The sixth module in the family, callgraph, never rented the removed store
and needed no port; it stays in scope here only because the new execution
gate loads and runs it alongside the five that did. All six build and run on
the occurrence store. Goldens are byte-identical to the pre-removal build
except three justified diffs: the elapsed-ms field, node-id allocation order
(now follows the input stream instead of the old store's allocation order),
and supersession output whose golden printed types the removal deleted.
The execution gate. No manifest row had ever run a codegraph module:
codegraph_seam_test.clj reads sources for forbidden namespaces but never
loads them, so when the fact-and-CID store was removed the five renting
modules died on load with no gate catching it. codegraph_exec_test.sh
closes that class of failure. Its unconditional tier loads all six
namespaces from out/ under bb — exactly the class that rotted, needing
nothing outside the repo. Its gated tier drives three modules end to end
against real oracles on FRAM_BEAGLE: roundtrip_fram against
beagle-roundtrip --verify; rename against both faith.rkt's rename trap
and its own rc=3 collision refusal; and supersession_check against its
live/withdrawn verdict. Three deliberately provoked failures fail loudly
under the gate, confirming it catches the class it was built for.
CI pipeline repair
Both CI workflows sourced Beagle's bin/_beagle-racket before raco pkg install --link registered the checkout, so the raco-make freshness gate
inside it ran against an unlinked collection and died on "collection not
found for module path: beagle/lang/reader-impl" — every push from
2026-08-05 through the repair, including the v0.5.0 tag push itself
(GitHub runs 31163165620 and 31219968815; first green after the fix,
31222644762). The fix links the
packages first and sources _beagle-racket after, so the gate resolves and
warms every .zo outside the per-row timeouts the suite applies
(.github/workflows/ci.yml, link-before-source step).
Separately, tests/fram_snapshot_boot_test.sh builds the complete native
server through bin/fram-native-build --host server, measured in the
release-readiness pass at 9m18s under concurrent load and 560 s uncontended
on a 24-core workstation; a 2-core hosted runner cannot fit that inside the
240s per-row timeout under any honest reading. The manifest now carries a new
disposition, exclude-runner, for a gate whose cost exceeds a hosted
runner: the row is excluded from the hosted CI run, its evidence is the
--host server build it drives, and it still runs in the flake devShell and
before a release (tests/occurrence_native_ci_manifest.txt, runner-capacity
row).
CI manifest exhaustiveness
cascade_test.clj, server_telemetry_shed_test.clj, and
snapshot_honesty_pass_test.clj existed in the tree but were undeclared in
the CI manifest, and all three fail for real against current code —
cascade_test.clj calls a socket harness that no longer exists,
server_telemetry_shed_test.clj references a function that was never
implemented, and snapshot_honesty_pass_test.clj depends on a load-order
side effect that no longer holds. Each is declared as an exclude with
evidence pointing at its own source: cascade_test.clj under
removed-socket-harness, server_telemetry_shed_test.clj under
removed-telemetry-shed, and snapshot_honesty_pass_test.clj under
`moved-graph...
v0.5.0
v0.5.0
Fram v0.5.0 completes the native cutover. The engine is compiled through
Beagle's release gate, published as one embedding ABI, and served from three
deployment shapes — a host process, a container, and a wasm module an isolate
embeds with no server at all. The wasm build answers byte-for-byte what the
native build answers, on the same frames and into the same FRAMLOG bytes.
It also fixes the two defects that made the previous engine unsafe to embed:
the commit wall that killed an instance around the 236th commit, and a since
window that quietly answered outside itself. A snapshot image lets a restart
resume from a watermark instead of folding from byte zero.
This is a minor release because the deployment routes and the embedding surface
change intentionally, and because a since-windowed query now returns different —
correct — rows.
Highlights
Native compilation through Beagle's release gate
bin/fram-native-buildtakes--host server | embed | wasm-embed | program
and a target--abi. The ABI profile joins the cache manifest, so two
profiles over one source closure can never share a cache entry, and each
profile takes its own QBE frontier scope. The gate requires C17 and ratchets
QBE refusals rather than degrading silently.- The generated-module contract is at ABI 3; the adapter verifies it before
the host runs. - Beagle's build-stage vocabulary crosses the seam as runtime string
contracts:stage source-freeze ACCEPTED, and the frontier refusal
native program is not frozen. The retired Worlds subsystem is deleted from
the tree, and the authoring checker speaks program and module-overlay. Both
verdicts are recorded in the naming ledger.
Three deployment shapes over one engine
- Host process —
bin/fram-serverrunning the native artifact, taking a
flockon the FRAMLOG. - Container — the Cloudflare route is now the static musl native artifact
onscratch, packaged from a completed content-addressed artifact and run
unprivileged as uid/gid 65534. The Graal image recipe is deleted; Graal
survives only as the differential oracle for native selfcheck, not as a
deployment route. - Embedded in an isolate —
--host wasm-embed --abi wasm32links the same
ABI intolib/libfram.wasm, a reactor with nine exports whose host table is
built from named imports. No socket, no filesystem, no ambient capability.
The shapes and the host contract are specified in
isolation and deployment.
Byte-identical engines
native/wasm-embed.seams pins every import with its resolved signature and
every export of the linked module; a build that differs dies with the diff, and
--regen-wasm-seams is the only way to move it. Nine fram_host_v1 imports
carry allocation, a clock, and storage, with two storage objects riding the
same hooks under a context argument. Seven WASI imports are linked and two are
live — the monotonic clock and environ_sizes_get; the other five record zero
calls over the whole frame matrix, measured rather than asserted.
tests/fram_wasm_embed_smoke.sh runs the frame matrix through an external
Python/wasmtime embedder and through the native embed library, and requires
identical response transcripts and identical FRAMLOG bytes.
The commit wall is fixed
FRAMLOG replay recursed once per frame in tail position. The C17 emitter lowers
that to a real self-call, and 256 bytes of wasm stack per frame overflowed the
64 KiB stack into the allocator's statics at about 238 frames — the 236-commit
open wall. Replay is now a loop, costing one frame regardless of log length.
Two changes harden the same area: --stack-first places the stack below every
static so an overflow traps at a memory boundary instead of corrupting
allocator state, and the embed boundary registers a trap reporter that names a
fram_status before the abort lands.
Measured after the fix, on one wasm instance with no memory cap: 620
single-triple commits from empty, all accepted. The store that killed the
pre-fix engine — 240 batched commits, which could not be opened at all in 480
seconds — opens in 533 ms.
The since window is fixed
requested-query-bounds! accepted a since lower bound for every plan, but only
the occurrence candidate source ever received it. A triple query under
since(L,U] therefore answered the whole relation, and so did every text
relation. Both candidate sources now filter by the same lower bound, so the
three plans read the same window. A windowed query that previously matched its
unwindowed twin byte for byte now answers the window; if you built anything on
the old behavior, it was reading outside its bounds.
Snapshot v1
rpc/checkpoint encodes the served store as an image beside the FRAMLOG: a
header, a flat stream of length-prefixed CRC-checked row records in position
order, and a trailer carrying sequence, watermark, log set, offsets, stamp, and
fingerprint. Position order is fold order, so re-loading the image assigns
handle for handle what a full fold assigns.
A boot with a valid image installs it and replays only the tail past its
watermark. An image that fails to install, or whose tail does not continue,
degrades to the full fold and reports it — the boot never fails on account of
an image, and a checkpoint appends nothing to the log.
Codec bounds derived, not guessed
Both list bounds now come from the codec's 256-deep Term budget: a request list
is capped at 250 values and an unpaged reply at 248 rows, each refusing typed
:term-depth-exceeded up front instead of letting the encoder discover the
cliff downstream.
The measured limits table
Certified against 9f00313 with the Beagle pin the flake declares. Toolchain:
wasi clang 21.1.8, wasm-tools 1.249.0, python-wasmtime 45.0.0. Wasm timings
ride wasmtime host calls from Python; in-isolate numbers are lower.
Read this section's provenance before quoting it. The budget and limits
matrix below was certified on the pre-fix engine, before the store
materialization fix (5721164, 3df8efe) landed. Boot cost was re-measured
after that fix, and the Boot table carries the new numbers. Every other row
here still describes the pre-fix engine, which makes each of them conservative:
the shipped engine boots faster and holds less memory at the same store size,
so the store sizes named below are floors rather than ceilings. Re-certifying
the full matrix on the fixed engine is queued and not done.
Write lifecycle — one wasm instance, single-triple commits from empty,
uncapped:
| Measure | Value |
|---|---|
| Commits accepted | 620 of 620 |
| Per-commit latency | p50 0.70 ms, p90 1.01 ms, p99 1.59 ms |
| Compaction crossings | survived as pauses: 37 ms at commit 257, 143 ms at commit 513 |
| Memory | 20 MiB → 150 MiB over 620 commits, stepping at compactions |
| Cold reopen of the 620-commit log | 203 ms fold, 199 ms snapshot+tail |
| Log density | 36.7 bytes per triple |
Commit count is not bounded by the engine. Capacity is bounded by memory.
Under a hard memory cap — wasm StoreLimits cap with
memory_budget_bytes set to the same number, page limit 16, lean 4 MiB
response arena:
| Budget | Writes | Long-lived reads |
|---|---|---|
| 96 MiB | 480 commits, then a trap on 481 | 300 triples good (2.0-2.3 ms/page steady, 62-98 ms compaction pauses, memory flat at 56 MiB); 500 fits but thrashes at the cap (~135 ms/page); 600 traps |
| 128 MiB | 512 commits, then a trap on 513 | 600 triples pass; 800 traps |
| 192 MiB | — | 1,000 triples still traps on read; rpc/status answers wherever the open fits (1,000 triples opens at 119-120 MiB) |
Page limit is not free: at a limit of 64 the first page cost +139 MiB at 300
triples and trapped at a 128 MiB cap. A page limit of 16 is the certified read
shape.
Boot — re-measured after the store materialization fix, so this table,
unlike the rest of this section, describes the engine that ships. Fold /
snapshot+tail, one store per size:
| Live triples | Native fold | Native snap | Wasm fold | Wasm snap | Wasm pages, fold | Native RSS, fold |
|---|---|---|---|---|---|---|
| 300 | 19.5 ms | 21.0 ms | 8.7 ms | 11.2 ms | 29 | 4.7 MB |
| 1,000 | 71.6 ms | 79.9 ms | 28.6 ms | 43.6 ms | 94 | 8.6 MB |
| 3,000 | 230.0 ms | 242.1 ms | 107.7 ms | 121.5 ms | 289 | 20.6 MB |
| 5,000 | 380.4 ms | 404.3 ms | 176.7 ms | 207.5 ms | 475 | 31.5 MB |
The fix moved the shape, not only the constant. Fitting log(y) on log(n) over
all four sizes, pre-fix against shipped:
| Quantity | Path | Before | After |
|---|---|---|---|
| Native time | fold | 2.05 | 1.06 |
| Native time | snap | 2.01 | 1.05 |
| Wasm time | fold | 2.04 | 1.09 |
| Wasm time | snap | 2.06 | 1.03 |
| Wasm pages | fold | 1.95 | 1.00 |
| Wasm pages | snap | 1.96 | 1.00 |
| Native RSS | fold | 1.90 | 0.68 |
| Native RSS | snap | 1.89 | 0.61 |
Both boot paths and both targets move from about n^2 to about n^1 in time, and
to n^1 or below in memory. The sub-linear memory exponents are the engine's
fixed floor — roughly 3 MB native, 322 wasm pages — still dominating at 300
triples. At 5,000 triples the shipped engine folds in 380 ms against 28.1 s,
in 31.5 MB of RSS against 3.85 GB, and in 475 wasm pages against 45,627.
Snapshot+tail still equals fold within run-to-run noise at every measured size,
because boot cost is store materialization: the fix made that materialization
linear, it did not take it off the boot path.
The ~6,100-triple wasm ceiling this table quoted before was a consequence
of the quadratic page growth, and it no longer describes the engine. A new
wasm32 ceiling has not been measured, and none is claimed here.
Provenance of this before/after pair: stores fed through fram-server-native
(assert-only, five triples per entity, arena space, batch 200), native taken as
the median of three runs and wasm as the faster of two under wasmtime. Before is
`ae1c...
v0.4.1
Highlights
- Adds opt-in per-generation Deflate compression for new FRAMLOG generations. Existing logs and migration writers remain unchanged.
- On the measured Gorskii-shaped corpus, storage fell from 17.65 MB to 0.99 MB (17.8x); write time was +1.8% and fold time +0.6%.
- CRC coverage, torn-tail repair, and bit-flip fail-closed behavior continue to cover the stored bytes.
- Aligns durable-append fault-injection seams with the new Deflate flag.
- Adds a release-only gate that rebuilds Graal from the exact tag and verifies a mutation across a container restart before publication.
Full Changelog: v0.4.0...v0.4.1
v0.4.0
v0.4.0
Fram v0.4.0 establishes the source-head production boundary: an
occurrence-native coordinator, binary FRAMLOG persistence, the closed FRAMRPC
v1 data plane, and an explicit no-JVM Graal deployment route while the Beagle
Native World daemon is completed.
This is a minor release because deployment and persisted-data compatibility
change intentionally. Calling it v0.3.9 would incorrectly imply a drop-in
patch for the deployed v0.3 flat-log and EDN-line runtime.
Highlights
bin/fram-daemonis native-first and fails closed. The default production
route requires a completed Beagle native artifact; it never falls back to a
JVM silently.FRAM_DAEMON_RUNTIME=graalselects a self-contained Graal coordinator when
FRAM_GRAAL_ARTIFACTnames its absolute executable path. This is the
transitional production route, not a Native World artifact or a change to
the Beagle-first architecture.native/build.shseparates the fast JVM/AOT preparation phase from Graal
image generation. The JVM remains the development and differential-oracle
surface; native-image compilation belongs in release builds.- The Cloudflare coordinator image now builds a static musl Graal executable
with GraalVM25.0.2-muslibpinned explicitly. The authenticated Babashka
HTTP/JSON shim remains a separate container and the private coordinator port
must not be published directly. - Reachability metadata is checked in and was produced from exercised daemon
paths. The image build is multi-stage and the final runtime contains the
coordinator executable rather than a JVM. - FRAMRPC v1 remains the closed thirteen-operation binary data plane for the
CLI, zero-dependency Node client, Cloudflare shim, and coordinator. SpaceId,
bounded framing, recursive Terms, paging, snapshots, and leases keep the
same source-head contract.
Compatibility and migration
- v0.3 flat logs are not served directly. Quiesce the old writer and run the
one-shot migration into a new binary FRAMLOG before cutover; retain the old
log as rollback input. The v0.4 daemon does not emulate the legacy EDN-line
wire protocol. - Existing service definitions that relied on an implicit JVM daemon must now
choose deliberately:graalplus an absoluteFRAM_GRAAL_ARTIFACT,
jvm-oracle, or checkout-onlyjvm-dev. The defaultnativeroute requires
an absoluteFRAM_NATIVE_ARTIFACT_DIRcontaining a validREADYmarker and
bin/fram-daemon-native. - Reminder for consumers upgrading from before v0.3.7: v0.3.7 already made a
separate breaking Node-client vocabulary change, renaming public
slot0/slot1/slot2parameters and object keys tot1/t2/t3.
That mechanical rename remains in force. It did not alter the binary wire,
where Triple positions are positional. - Do not expose FRAMRPC directly to untrusted networks. Authentication and TLS
remain the responsibility of the gateway or Cloudflare shim.
Release evidence
- JVM daemon regression suite passed.
- Native RPC boundary ratchet passed 15/15.
- The Node FRAMRPC integration suite passed 11/11 checks against the Graal
executable. - The Cloudflare Worker/JSON-to-Babashka-to-FRAMRPC integration suite passed
16/16 checks against the Graal executable. - The static release container accepted a mutation, reported version 1,
restarted, replayed the binary FRAMLOG, and returned the persisted fact. - The observed release image was 52,112,401 bytes. Its 64-bit x86 executable
had no ELF interpreter andlddreported that it was not dynamic.
Known limitations
- Graal is a deployment bridge. It removes the runtime JVM dependency but does
not replace the immutable Beagle Native World or complete the direct-native
cutover. - Native-image compilation is intentionally slower than the JVM development
loop. Ordinary development continues on JVM/Babashka paths; Graal builds are
reserved for release and deployment gates. - This release makes no generic startup, memory, or throughput claim. Those
properties remain workload- and deployment-specific and must be measured on
Fram rather than inferred from other Graal applications.
v0.3.8
New query relations — agent-authored retrieval beyond exact match. Datalog queries gain four additive relations beside text-match: text-phrase (ordered word sequences), text-substring (true infix via trigram postings), text-stem (English stemming), and text-search (ranked combination). All positive-only, same (entity, attribute, needle) shape, available through the MCP ask tool and the Node client's query — no client update required.
Performance. Analyzer indexes realize lazily on first use of their relations and are budget-checked against the snapshot cache limit, so text-match build and query costs are unchanged (verified under kernel-shielded isolation: 50k-row heap delta 3.5MiB, warm query p95 under 50ms). Known pre-existing debt, tracked: the 50k exact-index build exceeds its 1s bar (~1.9s).
Internal. Resolver identity widened Int → Term with canonical layouts; legacy ':-' annotations migrated repo-wide (3,425 sites); @upstream:graph markers removed — Beagle text is authoritative.
Engine wire: FRAMRPC v1 unchanged. Client: v0.3.7's 0.2.0 tarball remains current.