Working state — a note taken while the work happens, not a specification. The system as it is meant to be is in Architecture.

The memory arc — two numbers, one residency policy

QUEUED. This file is design capture, not an execution plan. It records John’s rulings of 2026-08-07, which are law for this design, plus the ground truth those rulings land on and the seam they consolidate. Nothing here has been built. When the arc is picked up, the phases still have to be written; what must not be re-litigated is §1.

Companion plans: plans/programs.md (the arc this follows), plans/carried.md (the ledger this arc’s items come from), plans/proposal-notes/builder-memory.md (the measurements §2 quotes).


1. Rulings — John, 2026-08-07

These are law for this design.

R1 — There are exactly two numbers

Actor max heap and max runtime memory. Not a family of knobs. Everything below is one of these two or the policy that sets them.

R2 — Actor max heap ALWAYS KILLS

When an actor reaches its max heap, the actor dies. No grace, no clamp, no degraded mode. An actor that wants more than its ceiling is abusive, and the kill is the correct answer, not a failure of the system.

In dev it is unbounded: an actor dies only when it wants more than the machine has. Development is where you find out what an actor actually needs; a ceiling there just hides it.

R3 — There is NO dev warn mode. The “warning” is a log-analysis tool

Explicitly ruled out: a second severity, a soft limit, a warn-then-kill ladder. GCs are already logged. So the warning is not a runtime feature at all — it is a tool you point at the last run’s logs:

  • per-actor peak heap over the run,
  • which actors exceeded a threshold you name,

reported after the fact. Just another tool you analyze the logs with. This matters because it keeps the runtime’s memory behavior binary — under the ceiling, or dead — while still answering “what would a ceiling of N have killed?” without ever running a mode that behaves differently.

R4 — Max runtime memory: the runtime NEVER dies

The runtime grows toward this number as it hands out actor heaps. On reaching it, the runtime does not die and does not refuse. It moves actors out of residency: an unused actor’s heap is frozen to disk.

Loading an actor back = bring its ROM/XIP stone pools into memory or into view, plus its heap, as needed. Only what is needed.

On a robust system neither number is ever reached. They are the answer to pathology, not the normal path. A design that makes freezing routine has mis-sized the machine.

R5 — Neither number is set in C

Both arrive through hooks from the shop actor — the customizable one. C provides the mechanism; policy is Pit, and it is replaceable.

But the boot actor must seed initial limits, because the builders and the clerk start before shop policy exists — and they are the biggest heaps in the system. mcode_link at ~1.45 GiB for one program is a named target of this arc (§2.3). A design where the largest consumers run before the policy that sizes them is not a design.

R6 — A machine-specs endowment, deniable

So the root shop can size max runtime memory from the machine it is on — e.g. half of system RAM. Deniable like every endowment: a program that is refused it falls back to a default rather than failing.

R7 — Per-actor setting in real deployments

Like max turn time and slow timers: a real deployment sets these per actor, not once globally. The two numbers are the model; per-actor is how the model is used.

R8 — ONE cart with explicitly laid-out memory, NOT multiple carts

This supersedes the multi-cart question. Residency is a layout problem inside a single cart, not a composition problem across several.

  • Gameboy: memory must fall in particular regions. The cart’s layout says where.
  • Playdate: split actor stone pools within the one cart — the split is internal to the cart, not a second cart.

plans/work-proposal.md §4 already pinned “multi-cart mounting does not exist” as confirmed-clean. R8 is the positive form of that: not merely absent, but ruled against, with explicit layout as the thing that replaces it.


2. Ground truth this lands on

Verified 2026-08-07. This is what the rulings change.

2.1 The actor limit exists and has no teeth

ACTOR_MEMORY_LIMIT is 1 GiB (source/pit_internal.h:2152), installed on every actor shell at source/scheduler.c:3674 via Pit_SetHeapMemoryLimit (a one-line store, source/runtime.c:1851-1853), overridable per actor at source/scheduler.c:3846-3856 and source/pit_actor.c:640.

It does not clamp growth, and the code says so in as many words (source/pit_gc_copy.c:1633-1636):

the stated heap limit is ADVISORY and does not clamp growth … the limit’s teeth are the warning at the end of GC.

R2 replaces exactly this. The comment’s stated reason for advisory behavior — clamping starves a needy actor into an OOM death — is answered by R2 rather than argued with: the kill IS the intended outcome, and dev is unbounded so that a genuinely needy actor is never mis-sized in the first place. The per-actor override hook at actor_shell_set_heap_limit is already the shape R5/R7 need; what is missing is the shop-actor policy above it.

Note that the existing “warning at the end of GC” is not R3’s tool. R3’s tool reads .pit/log/observe.jsonl after the fact. The in-GC warning is the mechanism that must go away when the limit grows teeth.

2.2 There is no runtime-wide cap at all

buddy.cap is 0 (pit ps mem reports capacity_bytes: 0), and buddy_max_block returns SIZE_MAX when cap is 0 (source/runtime.c:1500-1502). R4’s number does not exist yet in any form — it is new mechanism, not a re-parameterization.

2.3 The heaps R5 is about

Measured on the dev daemon (plans/proposal-notes/builder-memory.md):

actorused MBalloc MB (physical)peak MB
pit-shop/shop_actor120.2256.0256.0
pit-shop/clerk5.616.0128.0
pit-shop/logger1.78.016.0
TOTAL (9 actors)136.6296.1417.0

And the one that matters: the clerk’s press peak is 1450 MB cold (988/986 MB warm), owned by mcode_link.link_result’s streamline and literal pooling for one program, under pool_executable. Peak live ~1.45 GiB; the collector gave that actor a 4.0 GiB block (2.8×), which is why the ceiling sits at 8 GiB. The term is program-proportional — it is the P6 program-granularity ruling’s own cost.

Two consequences for this arc:

  1. This is the heap R5’s boot-seeded limits must accommodate, and it starts before any shop policy exists. Seeding it wrong is not a warning, it is a dead clerk under R2.
  2. Bringing this term down is the arc’s own compilation-side work (§4). 6 GiB is the lowest limit that leaves today’s observed block room; 4 GiB clamps it, and only becomes safe once the linker term comes down.

3. The consolidation — image_provider.c is already half of this

Direction ruled: build the heap half, then put ONE residency policy above both. Do not build a second, parallel residency mechanism.

source/image_provider.c already is the executable-residency half. Its own header says it owns “the irreducible address-lifetime seam: read-only placement, exact-byte/profile/ABI validation, runtime pin accounting, PoolBinding storage, and entry-function construction.” Concretely:

piecewherewhat it already is
placementsPitMachImagePlacement, rt->mach_image_placementsthe resident-set table
open_refs / pin_refsimage_provider.c:27-28reference accounting that decides what may be released
PROVIDER_MAP_BORROWEDimage_provider.c:68“cart mapping, ROM window, scratch image”this is R4’s ROM/XIP case, already named
provider_detach_unused_lockedimage_provider.c:189eviction, already written
placement identityimage_provider.c:64-68, :136-156dedup key = (placement id, profile hash, ABI hash, mapping mode); mapping mode is part of identity, not just teardown

What is missing is the heap half: freeze and thaw. An actor’s heap has no placement, no ref accounting, and no path to or from disk.

The unifying shape:

  • evict = unpin + freeze
  • load = pin + thaw

One policy object decides both, for executables and heaps alike. open_refs / pin_refs is the vocabulary it should speak — extended to heaps rather than duplicated for them.

R7’s binding fix lives in this same seam

source/image_provider.c:1138 (resident_function) refuses cross-image imports outright:

a start-plan window may not declare cross-image imports

Every unit, dev lane or cart, goes through that door (source/start_plan.c plan_run_pool). Per plans/carried.md, R7’s actual granularity work is: let the boot walker bind a unit’s cross-image import rows from the providers it has already produced — it holds every one of them in values[]before resident_function checks them.

That is C work in the boot floor, in the same file, touching the same placement table. It should ride this arc rather than be scheduled against it. F4 (the static-lane deletion) waits on it.


4. Carried in: the R7 / static-lane deletion work list

From plans/carried.md (“Carried out of the pool-lane press landing”). This is the arc’s compilation-side item, carried here because R7 is the unlock and R7 is §3’s seam.

  • F4 (delete the static link lane) DID NOT RUN, and the blocker is a live consumer, not caution. plans/proposal-notes/deletion-inventory.md §5.1 names the static lane’s two keepers: the press (R6 flips it — built and measured, not landed) and the dev realize lane (R7 moves it — not done). The second is the whole dev loop: pit-shop/clerk.ce:1132 realizes every actor start with no link in the request, shop_realize.cm:1068 therefore leaves build_msg.link unset, and shop_build.cm’s link_mode falls through to null. The seeded QOP universe (seed_fleet_entry’s first realization) is a second consumer for the same reason. F4 unblocks when R7’s granularity work moves the dev lane onto pools, and not before. Everything §5.2 lists is still correct as a list; the precondition is what is unmet.
  • static_bindings.canonical must be KEPT — §5.2 is wrong about it. The inventory lists static_bindings among the deletions. Its exports sub-map and the cart NAMES/IMPORTS rows are residue; canonical is load-bearing: it is the only request→provider map covering NATIVE providers. The manifest’s bindings[] rows keep an edge only when the provider is itself a manifest unit (executable_manifest.cm:329-334), so use('blob') — every extension — has no row there. Covering the press from the manifest alone refuses on the first blob import. Whoever runs F4 must keep canonical or give the manifest a native binding row first.
  • What the flip looks like when tried: link_mode = "pool" unconditionally in shop_build.prepare_simple_bundle (plus the two other link reads), context.link / build_msg.link constant in shop_realize, mcode_cache_key folding the constant "pool" (keep it — dropping it collides with a pre-R7 shop’s static entries). The seed then fails at press 2 with mach_pool_emit: unit 0 import 0 (lang::messaging) was not covered — the press placed no provider for it, which is the emitter being right. The patch is short and worth re-deriving rather than saving.
  • Bring mcode_link’s ~1.45 GiB/program term down (§2.3). This is the arc’s own number, not a side effect: it is what decides whether the boot-seeded builder ceiling can come below 6 GiB.
  • Side effect worth naming: R7’s static-lane deletion resolves the “lane” glossary ambiguity carried in plans/carried.md, by removing one of the two meanings of the word.

5. Open, for when the arc is picked up

Not ruled. Do not invent answers to these — ask.

  1. Freeze format and location. Is a frozen heap a new artifact kind, or does it reuse an existing one? Where does it live — the object store, a per-daemon scratch area, the cart?
  2. What makes an actor “unused.” R4 says the runtime moves unused actors out. Idle turns? Time since last letter? Explicit? The policy is Pit (R5), so this may simply be the shop actor’s business — but the C side must expose whatever the policy reads.
  3. Freezing an actor with C-held resources. Actor-owned opaque records (sockets, child processes, file handles — see CLAUDE.md) cannot be frozen to disk. Does holding one make an actor non-evictable, or is there a detach/reattach protocol?
  4. What the machine-specs endowment reports, exactly, and what a denied program falls back to.
  5. Whether R2’s kill is observable as a distinct death reason. carried.md already carries a deferred ruling that a reap should carry its own reason; a heap-ceiling kill is a third thing again, and the same argument applies.

Source: plans/memory-arc.md