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):
| actor | used MB | alloc MB (physical) | peak MB |
|---|---|---|---|
pit-shop/shop_actor | 120.2 | 256.0 | 256.0 |
pit-shop/clerk | 5.6 | 16.0 | 128.0 |
pit-shop/logger | 1.7 | 8.0 | 16.0 |
| TOTAL (9 actors) | 136.6 | 296.1 | 417.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:
- 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.
- 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:
| piece | where | what it already is |
|---|---|---|
| placements | PitMachImagePlacement, rt->mach_image_placements | the resident-set table |
open_refs / pin_refs | image_provider.c:27-28 | reference accounting that decides what may be released |
PROVIDER_MAP_BORROWED | image_provider.c:68 | “cart mapping, ROM window, scratch image” — this is R4’s ROM/XIP case, already named |
provider_detach_unused_locked | image_provider.c:189 | eviction, already written |
| placement identity | image_provider.c:64-68, :136-156 | dedup 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:1132realizes every actor start with nolinkin the request,shop_realize.cm:1068therefore leavesbuild_msg.linkunset, andshop_build.cm’slink_modefalls 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.canonicalmust be KEPT — §5.2 is wrong about it. The inventory listsstatic_bindingsamong the deletions. Itsexportssub-map and the cart NAMES/IMPORTS rows are residue;canonicalis load-bearing: it is the only request→provider map covering NATIVE providers. The manifest’sbindings[]rows keep an edge only when the provider is itself a manifest unit (executable_manifest.cm:329-334), souse('blob')— every extension — has no row there. Covering the press from the manifest alone refuses on the firstblobimport. Whoever runs F4 must keepcanonicalor give the manifest a native binding row first.- What the flip looks like when tried:
link_mode = "pool"unconditionally inshop_build.prepare_simple_bundle(plus the two otherlinkreads),context.link/build_msg.linkconstant inshop_realize,mcode_cache_keyfolding the constant"pool"(keep it — dropping it collides with a pre-R7 shop’s static entries). The seed then fails at press 2 withmach_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.
- 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?
- 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.
- 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? - What the machine-specs endowment reports, exactly, and what a denied program falls back to.
- Whether R2’s kill is observable as a distinct death reason.
carried.mdalready 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