The non-LLM bit
Canic uses a lot of ic-stable-structures memory ids. A veritable smorgasbords worth.
This is the current breakdown, we’ve got hardcoded IDs that tend to change, fragment, and obviously one mismatched ID and you’ve got a major version bump. It’s something that has churned, and not really been improved as we’re pre 1.0 and its fine if we reinstall canisters.
Problem Statement
Canic stable memory IDs are persistent disk layout, but the current model does not treat them as a durable ABI contract.
Memory ownership is derived from runtime declarations and implicit (owner crate, Rust type label) identity, which can drift during refactors, package moves, renames, or framework reorganization. That creates risk of accidental memory reassignment, stranded state, or silent corruption across upgrades.
The current runtime registry is also ephemeral. Historical allocations, retired IDs, and prior ownership are not durably authoritative, which means a runtime reset or declaration change can lose critical ABI history.
Once downstream canisters depend on stable memory layout, incorrect reuse or movement of IDs becomes a permanent compatibility failure, not a normal implementation bug.
Canic therefore needs a strict, persisted stable-memory ABI ledger that:
-
treats
stable_key -> memory_idas permanent identity -
preserves historical ownership and retired allocations forever
-
reserves framework ranges explicitly
-
fails closed during bootstrap before endpoint execution if any memory-layout invariant is violated
Canic v0.38.0 Tentative Design Document
Canic 0.38 Stable Memory ABI
Status
Draft started: 2026-05-16
0.38 is the stable-memory ABI hardening minor. It is a deliberate hard ABI cut.
Backward compatibility with implicit owner/label identity is not a goal.
Stable memory IDs are durable canister disk layout. Treat them like:
-
filesystem block ownership
-
database page ownership
-
protocol field numbers
A memory ID that ever held one logical store must not silently become another logical store.
Core Position
The persisted ledger is authoritative.
Runtime declarations are claims against the ledger.
The only allocation identity is:
stable_key → memory_id forever
Owner crate, package name, Rust type path, module path, and local label are metadata only. They may drift.
Allocation identity must not drift.
Goals
-
Define a durable stable-memory ABI model now, not piecemeal.
-
Reserve
0-79globally and unconditionally for Canic. -
Give applications one clear safe range:
80-254. -
Keep ID
255reserved foric-stable-structures. -
Persist all range and memory ID history in stable memory.
-
Distinguish never-allocated IDs from retired IDs.
-
Keep retired IDs permanently poisoned.
-
Validate runtime declarations against the ledger before endpoint execution.
-
Prevent stable-memory opening or mutation before ABI validation succeeds.
-
Require explicit stable keys for all Canic-owned framework memories.
-
Separate allocation ABI from schema ABI.
-
Reserve space for future allocator, migration, snapshot, and generation metadata without implementing dynamic allocation in 0.38.
Non-Goals
-
No compatibility shim for owner/label-derived identity.
-
No automatic data migration between memory IDs.
-
No application dynamic allocation in 0.38.
-
No reuse of retired IDs, ever.
-
No “best effort” bootstrap if the ledger detects ABI drift.
-
No profile-dependent app ranges.
Global Memory Map
This map is unconditional for Canic-managed canisters:
| Range | Owner | Meaning |
|---|---|---|
0 |
canic-memory |
persisted ABI ledger |
1-4 |
canic-memory |
allocator, migration, generation, and snapshot metadata reserve |
5-10 |
canic-control-plane |
root / wasm-store control-plane state |
11-79 |
canic-core |
core runtime state and future framework growth |
80-254 |
applications | downstream explicit memory IDs |
255 |
ic-stable-structures |
internal stable-structures memory |
There is no full-stack versus standalone app range distinction in the Canic runtime contract.
Applications should use 80-254.
Standalone canic-memory consumers that do not use the Canic runtime may choose their own policy, but Canic framework code and Canic documentation should teach 80-254 as the safe application range.
Ledger Authority
The ledger is stored at stable memory ID 0.
It is the historical ABI record.
Runtime declarations are not the source of truth. They are current-binary claims that must be checked against the ledger:
declaration: “I claim stable_key X is memory_id Y”
ledger: “stable_key X is memory_id Y forever”
If the declaration matches the ledger, bootstrap may proceed.
If the ledger has never seen the key or ID, the declaration may allocate a new active record.
If the declaration conflicts with history, bootstrap must fail closed.
The ledger must never be reconstructed from current declarations.
Current declarations can be incomplete, renamed, removed, or reordered.
Historical ABI state must survive those changes.
Stable Identity
Stable keys are explicit ABI names.
Examples:
canic.control_plane.template_manifest.v1
canic.control_plane.template_chunk_payloads.v1
canic.core.auth_state.v1
canic.core.root_replay.v1
canic.core.cycle_tracker.v1
The key namespace should be descriptive and versioned.
The suffix is the ABI generation of that logical memory, not the crate version.
Allowed metadata drift:
same stable_key, same memory_id, owner changed
same stable_key, same memory_id, label changed
same stable_key, same memory_id, Rust type moved
same stable_key, same memory_id, Rust type renamed
same stable_key, same memory_id, package renamed
Fatal allocation drift:
same stable_key, different memory_id
same memory_id, different stable_key
retired memory_id claimed by any stable_key
retired stable_key claimed by any memory_id
range overlaps a historical range owned by another allocation authority
app declaration claims any ID below 80
any declaration claims ID 255
Permanent Tombstones
The ledger must distinguish:
never_allocated
active
reserved
retired
never_allocated means the ID has no historical ABI meaning.
retired means the ID had historical ABI meaning and is now permanently poisoned.
A retired ID is not available capacity. It must never be reused.
Retirement is a tombstone, not deletion.
Tombstones are permanent because:
-
stable memory contents may still exist
-
future rollback may reintroduce old binaries
-
tooling must be able to explain historical ownership
Ledger Records
Header
The ledger header should include:
ledger_schema_version
layout_epoch
current_generation
created_by_canic_version
last_updated_by_canic_version
ledger_schema_version describes the serialization shape of the ledger itself.
layout_epoch is a deliberate, human-reviewed ABI epoch. It is not a migration counter and must not change automatically.
current_generation increments when a validated runtime layout snapshot is committed.
Range Record
Range records should include:
owner
start
end
status: active | reserved | retired
first_seen_version
last_seen_version
first_generation
last_generation
purpose
Ranges are allocation authority boundaries.
They do not replace per-ID stable-key records.
Memory ID Record
Memory ID records should include:
id
stable_key
owner
label
status: active | reserved | retired
first_seen_version
last_seen_version
first_generation
last_generation
schema_version
schema_fingerprint
purpose
Only stable_key and id are allocation identity.
Owner, label, purpose, and version fields are diagnostic metadata.
Allocation ABI vs Schema ABI
0.38 must separate allocation ABI from schema ABI.
Allocation ABI answers:
Which logical memory owns this stable memory ID?
Schema ABI answers:
What data shape lives inside that logical memory?
schema_version is the declared version of the data format inside the memory.
It is owned by the logical store, not by the ledger.
schema_fingerprint is a stable digest or descriptor of the store schema.
It should be stored from day one so tooling has a place to report and compare it.
For 0.38, schema fingerprints should be informational, not fatal.
The fatal checks are allocation checks:
-
key/ID drift
-
ID/key drift
-
tombstone drift
Schema enforcement can become stricter later once schema ownership and migration hooks are fully designed.
Future schema migrations should be explicit store-level workflows.
They must not move memory IDs as a side effect.
Source API
Canic-owned framework state must use explicit keys:
canic_memory::ic_memory_key!(
"canic.core.auth_state.v1",
AuthState,
AUTH_STATE_ID,
)
Framework code must not use implicit owner/label-derived identity.
Legacy convenience APIs:
ic_memory!(Type, ID)
MemoryApi::register(id, owner, label)
These may remain for non-framework explicit-ID users, but they should be documented as convenience APIs that derive a stable key from unstable metadata.
They are not acceptable for Canic-owned framework state.
Preferred runtime-selected API:
MemoryApi::register_with_key(id, owner, label, stable_key)
Long term, downstream applications should also prefer explicit stable keys.
0.38 should move Canic first and document the downstream direction clearly.
Fail-Closed Bootstrap
Bootstrap must guarantee:
-
no endpoint dispatch
-
no stable-memory mutation
-
no stable-memory open
before ABI validation succeeds.
Required ordering:
open only the ledger memory at ID 0
load ledger header and historical records
validate ledger schema version
reserve/validate 0-79 framework ranges
drain runtime range declarations as claims
drain runtime memory declarations as claims
validate every claim against historical ledger records
reject all fatal drift
commit a new registry snapshot generation
publish runtime registry snapshot
allow other stable memories to open
allow endpoint execution
Opening ID 0 is the only allowed pre-validation stable-memory open.
It is the root of validation.
No framework store should open its own stable memory until its declaration has been validated against the ledger.
If this requires delaying memory handle creation behind bootstrap, that is part of 0.38’s correctness work.
Generation Semantics
The ledger should model generations explicitly.
A generation is a validated runtime layout snapshot.
It records that a specific binary declared a specific set of ranges and stable-key mappings and that those claims passed ledger validation.
Generation fields support:
-
interrupted upgrade diagnosis
-
rollback diagnosis
-
detecting whether a binary reached post-validation commit
-
explaining when a memory was first or last seen
-
future migration journal correlation
Interrupted upgrade expectation:
-
If validation fails before generation commit, no new generation is recorded.
-
If validation succeeds but later application startup fails, the committed generation remains useful evidence that the memory ABI was valid.
-
Rollback to an older binary must validate against tombstones and historical key mappings in the same way as any other upgrade.
Recovery expectation:
-
The ledger must be readable even if runtime registry reconstruction fails.
-
Diagnostics should be able to report the last committed generation.
-
Repair tooling may inspect the ledger, but must not mutate allocation history casually.
Future Metadata Reserve
IDs 1-4 are reserved for future canic-memory metadata.
The reserved semantics are:
1 allocator metadata
2 migration journals
3 snapshot / export indexes
4 generation and recovery metadata expansion
0.38 does not need to implement all of these stores.
It must reserve the space and document the intended ownership so future features do not compete with application memory or framework stores.
Future Dynamic Allocation
0.38 should not implement dynamic allocation for applications.
Future dynamic allocation, if added, must still be based on:
stable_key → memory_id forever
The allocator may choose a new ID only for a never-allocated stable key.
It must never reclaim retired IDs.
It must never compact by moving live IDs.
Threat Model
The following are catastrophic ABI violations.
Same Stable Key, Different ID
This means the current binary is trying to open the logical store through a different disk slot.
The old data would be stranded, and the new slot may be empty or contain unrelated data.
Same ID, Different Stable Key
This means the current binary may interpret existing bytes as a different logical store.
That is direct state corruption risk.
Retired ID Reuse
Retired does not mean free.
It means “historically owned, no longer active”.
Reusing it destroys the ability to:
-
roll back
-
audit
-
safely inspect old stable memory
Range Overlap
Range overlap means two allocation authorities believe they may assign the same ID.
Even if no collision exists today, future additions become unsafe.
Runtime Declaration Drift After Persistence
Current declarations are claims, not history.
If a declaration changes after the ledger has persisted historical ownership, the change must be judged against the ledger.
It must not rewrite history.
CI Guardrails
0.38 should add source and runtime guardrails:
-
Fail if Canic-owned framework storage uses
ic_memory!(. -
Fail if Canic-owned stable keys are duplicated in source.
-
Fail if Canic-owned stable keys do not match accepted prefixes:
-
canic.core.*.vN -
canic.control_plane.*.vN
-
-
Fail if any Canic-owned declaration claims ID
80-254. -
Fail if any application-facing documentation says full Canic apps should use IDs below
80. -
Test:
-
key/ID drift
-
ID/key drift
-
tombstones
-
range overlap
-
ID
255 -
owner/label metadata drift
-
Useful source scan:
rg "ic_memory!\(" crates/canic-core/src/storage/stable crates/canic-control-plane/src/storage/stable
This should return no framework declarations.
Runtime Introspection
The ledger should be exportable in a machine-readable shape for diagnostics.
The command/API shape can be decided during implementation, but the output should include:
ledger header
range records
memory ID records
current generation
runtime declaration snapshot
validation result
This is diagnostic output, not a second source of truth.
Migration Philosophy
0.38 is the ABI-hardening release.
Compatibility shims for unsafe historical conventions are intentionally avoided.
Correctness and future stability are more important than preserving implicit owner/label behavior.
The hard cut is justified because future storage breaks are much more expensive than a strict minor now.
First Release Scope
0.38.0 should include the complete registry foundation:
-
Ledger at memory ID
0 -
Global Canic reservation
0-79 -
Application range
80-254 -
Permanent tombstone model
-
Explicit stable-key identity
-
Schema metadata fields, stored but not fatal
-
Layout epoch and generation fields
-
Canic-owned
ic_memory_key!declarations -
Framework source guardrails
-
Runtime fail-closed validation
-
Machine-readable ledger export if implementation cost stays small
Do not split the core ABI model across multiple releases.
Later 0.38 patches may refine tooling, but the irreversible invariants should land in 0.38.0.
Please, developers: it would really help me if you could copy and paste this into your LLM of choice and help me refine the design, because it’s got to be… perfect.

