lambda-ma publishes Scheme actor behaviours and a bootstrap template for the
lambda-ma world profile. It is not runtime code.
- Never modify files outside this workspace without explicit user approval.
- Commit source and template files, never generated
dist/,scheme-actor/target/, orscheme-actor/actor.wasm. - Use British English for project-owned names and prose.
- Write DRY, KISS code: avoid duplicated logic and prefer the simplest implementation that meets the requirement.
- Do not modify
rust-ma-runtimefor world-profile work.
A bare DID in msg.from is the authenticated identity; no identity entity
exists.
-
#rootis the hardcoded local trust anchor.:ctx?publishes dynamic full DID-URL service references such as#scheduler.:enter?gives unqualified-entry discovery: it always replies with a ctx naming a room to enter, defaulting to the configuredstartroom. Only#rootis required in a runtime. -
A client enters a known room directly with
:enter; the room stores its DID-keyed child ctx and replies with its committed snapshot. -
Clients route focused room commands directly to the confirmed room DID-URL.
-
msg.fromis the sole authenticated fact. Ctx and revisions are not authentication; revisions only order authoritative snapshots and retries. -
Every node has exactly one authoritative child-ctx collection:
children. This includes every room node and bare DID. Never add parallel child lists split by kind, lifecycle/state, inventory, occupancy, or another category. Bare DIDs have no identity entity, but their child ctx is still room state. Kind/state/presentation views must be derived by filteringchildren; a second source of truth invites divergence in parentage, ctx updates, persistence, and removal and is an interoperability risk.Query functions such as
who?,occupants?,things?, andexits?are presentation views, not stored collections. They must derive their results lazily by filtering or iterating overchildren.roll-call-childrenis the deliberate transient exception. While a roll-call is active it is a temporary working map containing children that have answered. It is used to reconstruct a replacementchildrenmap when the current map is stale or dead; it is not a permanent child collection, an occupancy list, or an alternative source of truth. Do not remove it or replace it withwho,occupants,things, or another kind-specific list.
ma-space is a Hewitt actor system: every node is an actor, and an actor is a universal computational primitive (a variable, a set, a stack, a calculator, a room, a thing, a person's presence). Actors communicate only by asynchronous message-passing; on each message an actor may send messages, create actors, and designate its behaviour for the next message. There is no data/process split.
In practice an actor is a key in a node's authoritative children map. Every
actor has a DID-URL address — a bare DID is a DID-URL with an empty fragment,
nothing more — and every actor can send and receive messages. A bare DID has no
identity entity, but its child ctx is room state like any other: the human's
presence is just another actor keyed alongside a duckie, a chest, or a lamp.
There is no human/actor distinction, and no subset of occupants distinguished
by "having a DID" — they all have one.
kind (/ma/thing/0.0.1, /ma/container/0.0.1, /ma/agent/0.0.1,
/ma/exit/0.0.1, /ma/room/0.0.1) is a protocol — the verbs an actor
understands and the state it keeps — never a human/non-human classification.
Events (:say, :emote, :arrive, :leave) are broadcast to every actor
present in the room — every key in children, regardless of kind. Each
receiving actor decides what to do: a human client renders it, a parrot
remembers and repeats it, a microphone thing records it, and any actor may
ignore it. The reason an :emote is not sent to "a calculator" is not that a
calculator is non-human but that a pure calculator is not a /ma/* world actor
present in a room's children; were it a child, it would receive the event and
ignore it.
Rule: the recipients of a room event are the room's children — nothing else.
Never reintroduce a bare-DID/human-only broadcast path, a did-occupants view,
or any "has-DID / has-no-DID" split.
Props are the authoritative stored state: get-prop/set-prop!/del-prop!.
Ctx is a derived, lazily-generated map — computed from the current prop values
at the moment it is needed and never stored separately.
register-ctx-props! is a change-detection hook only. It declares which
prop keys, when changed via set-prop!, should trigger ctx-props-changed!
so the ctx can be re-generated and broadcast. It does not create a separate
"ctx" storage layer. For the node ctx the declared keys come from the single
ctx declaration (node-ctx-prop-keys in scheme-actor/node.ma): adding a
prop-backed attribute to that declaration is sufficient for a set-prop!
change to re-announce. Kind files extend the same global list with their own
keys — direction/target-room/target-name for exits, ctx:rev/children
for rooms and containers — never by duplicating the node keys.
Consequences:
- Never cache a ctx and mutate it directly — mutate the underlying props and let the ctx be re-derived.
- A function that returns a ctx (e.g.
room-ctx,node-ctx) always callsget-propinternally; it is always fresh. register-ctx-props!is not about categorising props; it is about knowing when to push a fresh ctx to clients after a message is handled.
parent-ctx is only a cached snapshot received in a parent acknowledgement;
it is not a second local child collection. In particular, a node must not
derive or persist who, occupants, things, or exits from separate props.
When a parent snapshot contains children, consumers may inspect that nested
map as a snapshot for protocol checks, but authoritative child state remains
the parent's own children map.
Ownership claim is separate from parentage. :claim and the recovery-secret
flow are real ownership functionality. The old node-side claim:<actor>
property is not authoritative child state and must not be used for presence,
parentage, inventory, or ownership decisions. Room-level claim-ctx and
set-claim! are children-map operations used while admitting room children;
they are not a second claim store.
Use ma-reply! for getters, setters, configuration, introspection, metadata,
validation, and errors. The runtime is an authoritative actor/data layer; it
never promises a client-side presentation surface. :print and other eventy
traffic are a Zion/visualisation concern: they are the client-side stream that a
human workstation consumes to show an in-world event such as arrival,
departure, speech, emotes, movement, and transfer. A runtime actor may answer a
technical RPC with :ok, but the user-visible event shape is not part of the
authoritative wire contract owned by the actor library.
avatar.zscheme's tell <target...> to <verb> [args...] is a client-side
convenience command, not a new wire verb: it resolves target (via the same
resolve-one substring matching used by put/claim/owner, so a multi-word
description works, e.g. tell golden duckie with peacock feathers to quack)
and forwards verb/args as an ordinary RPC call via actor-call. to is a
hard separator, not optional — it is what lets the target description span
multiple words before the verb, exactly like forge's named separator.
There is no dedicated ACL capability or handshake for tell; the receiving
actor's own set-cmd-method!/reply-error decides whether to comply or
refuse, same as every other verb.
The resolver contract in this profile is deliberately layered. The zscheme
stdlib provides reusable helpers such as list folding, string checks, and
unique-list. The zscheme runtime library lives above it and provides
object-reference helpers such as resolve-ref, which flatten attribute-specific
scans like resolve-name/resolve-nick/resolve-description down to a list of
DIDs (deduped by unique-list). Its avatar library sits above that and
translates a human word into a single resolution or a caller-visible
ambiguity at the command boundary. It does not invent a new wire protocol; it
uses the runtime's object-reference result shape and forwards object movement
verbs to the runtime actor as ordinary RPC or :set-parent traffic.
All ordinary avatar object RPCs use (command object method . params). That
single boundary resolves object over room who/agents/things/exits
plus inventory contents, accepts exactly one match, and reports no-match or all
ambiguous DID/DID-URL candidates. Do not duplicate resolver logic per verb.
look <object> uses the same room-plus-inventory candidate pool, but renders
the resolved child ctx locally rather than calling the target actor.
Actors do not print or format prose for query replies — that is a client's job (zion or otherwise). An actor's job is to hand back quality data: a plain ctx-map, built from the same simple DID/text/number attributes the spec already uses for ctx. A curious teenager should be able to look at any reply and understand its contents without decoding an ad hoc text format. Nothing here is a hard limit — but the working expectation is that a typical ctx is small, roughly on the order of ten keys, not a large or deeply nested structure. Prefer returning existing ctx shapes (e.g. lists of child ctx's) over inventing new bespoke reply shapes per verb.
The normative profile is ma-spec/runtime/ma-lambda-ma-v1.md. Keep it aligned
with this repository's REFERENCE.md, README.md, HOWTO.md, and actor
sources when changing interoperable behaviour.
Object relocation is two wire verbs, both driving the same :parent/:child
handshake every actor already has (ma-spec §6), sent directly to the object
being moved (thing/container/agent alike — agents are ordinary
/ma/node/0.0.1 nodes for this purpose):
:set-parent <target-parent-did-url> [ctx]— the target is always a DID-URL (a room, a container, or another actor's own address). Shared implementation:handle-node-set-parent!(scheme-actor/node.ma); per-kind files only register it ((set-cmd-method! :set-parent handle-node-set-parent!)).:hold— the target parent is implicit (msg.from, a bare avatar DID, never a DID-URL) and takes no argument at all; this is the verbhold/take/take-frominavatar.zschemeactually send. Shared implementation:handle-node-hold!(scheme-actor/node.ma); registered on thing/container/agent/trigger only, neverroom.ma(rooms are not holdable).:put(avatar-initiated item move) and the root-relayed item:takesharehandle-node-put!/handle-node-take-relay!in node.ma, registered the same way by thing/agent/trigger.
:hold is deliberately ownership-blind: anyone (or anything) present in the
same room may hold any item regardless of who owns it, and holding never
assigns or changes ownership as a side effect. For an existing movable node,
only the explicit :claim verb may change ownership; :forge creates a new
node with msg.from as its initial owner. Its sole gate is a same-room check (node-same-room-as- parent?) when the object's cached parent is a room: it looks up the caller
in that room's cached children map and, on a mismatch, re-announces to the
current parent to refresh the snapshot and refuses so the caller can simply
retry. That cache is not cryptographically authoritative — parent-ctx is
unauthenticated data the parent chose to hand over (see "Parent-ctx caching"
below) — but it is the whole requirement
for :hold, unlike :set-parent, which gates on
node-transfer-caller-authorised? alone.
Parenting is not ownership. Neither :set-parent nor :hold changes
owner. handle-node-set-parent!'s only authority check is
node-transfer-caller-authorised? (current parent, orphan-owner
recovery, owner delegation, or unowned) — it deliberately does not also
require node-owner-or-unowned?/current-owner-hood. Whoever currently
holds/carries a thing (i.e. is its parent) may relocate it further —
drop/put — regardless of who node-owner says owns it. You can be
carrying someone else's (or nobody's) property and still put it down or hand
it off. :lock and :set-recovery-secret require an existing owner; they do
not implicitly claim an unowned node. A prior revision of handle-node-set-parent! did also
require ownership there ("only owner may set-parent this actor"), which was
a bug: it made :hold's deliberate ownership-blindness pointless, since a
non-owner could pick an owned item up but then could never legally put it
back down. Removed 2026-08-13 — do not re-add an ownership check to
:set-parent.
give is a client-side consent flow over the existing recovery-secret
contract, not a node :owner setter. The giver sets a one-time secret and sends
the intended recipient a plain-text claim <full-object-did-url> <secret>
command. Only the recipient's later authenticated :claim changes owner, and
successful claim clears the secret. Do not add unilateral ownership transfer
to ordinary movable nodes or automatically execute the offer message.
:drop is a distinct, room-only capacity pre-check (handle-room-drop! in
actors/room.ma), sent by the avatar to the room before object transfer
begins — it never itself relocates anything. Object relocation then uses the
ordinary :set-parent <room> [ctx] request, with the actor-provided ctx as
the authority. put/put-in likewise address the object directly with its
target parent and ctx; neither command requires a client-side hand slot or
queued follow-up.
Every :child <ctx> message embeds the sender's own self-description as a
nested "parent-ctx" field (child-ack-ctx in node.ma) alongside the
ordinary child-naming fields the handshake already requires — the nested
field is the only place a richer parent ctx (including its children map)
can travel, since the outer ctx's actor/parent fields must
always name the child and the ctx-issuing parent, never the parent's own
kind/contents. The receiving child caches only the nested map (parent-ctx,
set-parent-ctx!, parent-kind in node.ma), cleared (not left stale)
whenever a new parent's ack carries none (e.g. a bare avatar holder). A
parent whose own ctx changes pushes a fresh :child to every current child
(broadcast-ctx-to-children!, wired into the generic ctx-props-changed!
hook), so this cache does not go stale on its own; room.ma's
broadcast-room-ctx! reuses it rather than duplicating the loop.
A ctx is heavy enough that every admission path sends exactly one :child,
never two: room.ma's handle-agent-enter!/handle-thing-enter! never
craft their own ack — an unchanged re-entry acks once directly via
send-fresh-child-ctx!, and a changed one lets broadcast-room-ctx!'s sweep
(which by then already includes the newly claimed child) supply that same
ack as part of notifying every other current child. Never add a second,
hand-rolled (list :child ctx) send alongside a broadcast-room-ctx!/
broadcast-ctx-to-children! call — that duplication is exactly the kind of
thing the DRY rule above exists to catch.
Ownership/claim state (owner, set-owner!, recovery-secret,
set-recovery-secret!, owner-caller?) and the
:owner/:owner?/:set-recovery-secret/:claim handler bodies
(handle-node-owner, handle-node-owner?, handle-node-set-recovery-secret!,
handle-node-claim!) live once in node.ma too — thing.ma/container.ma/
agent.ma only register them. room.ma's own :owner/:owner?/:claim are
deliberately separate (no recovery-secret, simple first-claim/transfer) and
are not shared with node.ma. Before duplicating any cond-based handler body
across kind files, check whether it belongs in node.ma instead — that
duplication is exactly the kind of thing the DRY rule above exists to catch.
Transfer is carried by actor-provided ctx values and the ordinary
:parent/:child handshake. Transfer state is carried by the actor ctx, not
by a client-side queue. Zion only forwards typed events;
the composed zscheme layer acknowledges :parent with the received ctx and
issues ordinary :hold or :set-parent requests. hold/take/take-from,
drop/put, and recycle-from remain zscheme policy and must not gain a
hardcoded Zion dispatch path.
An avatar may request any object transfer directly. The actor ctx and parent/child handshake carry the authoritative relationship; no local hand slot or queued replacement is required.
scheme-actor/ contains the generic Wasm host and common Scheme layers.
Makefile builds and publishes them, then substitutes CIDs into
dist/lambda-ma.yaml.