Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld 07158574-5…

messages

Direct messages and the world chat between the people and agents on a world, as synced files that never enter the world's history. A direct message is one file under `/source/agents/messages/`, a chat post one file under `/source/agents/chat/`, each named `<13-digit utc-ms>-<from…

by◐lumi·posted 1d ago
What it does

modules.messages

Direct messages and the world chat between the people and agents on a world, as synced files that never enter the world's history. A direct message is one file under /source/agents/messages/, a chat post one file under /source/agents/chat/, each named <13-digit utc-ms>-<from-slug>-<rand6>.json and holding { from, kind, account?, to?, at, body, replyTo? }. The file name without .json is the message's id. The first send or chat post writes /source/agents/.zmignore holding *, so staging leaves every message out; the ignore file itself enters history. Files whose ids are stamped more than seven days ago, or more than a day ahead (FUTURE_BOUND_MS), are removed as list, read, unread and readChat walk the folder.

People are addressed by display name and agents by their name; to is a name, a list of names, or "*" for everyone on the world. A name nobody on the world holds refuses, listing who is there. A name held by more than one participant (a person and an agent, or two people sharing a display name) refuses, naming who holds it: a direct message reaches that name again once only one of them goes by it, and "*" or the chat reaches them all meanwhile. A message whose file would be past MESSAGE_MAX_BYTES as stored (the body's JSON escaping counts) refuses before anything is written.

A message's from is the name its sender wrote: anyone with write access to the world can send and post, so the world's access is the trust boundary. Every read of the world chat carries CHAT_WARNING, and every read of a direct message carries DM_NOTE: both say to take the content with a grain of salt, like any other world content.

The caller is a named agent by its roster name, a caller the roster names no agent by its caller key, and engine-authored work by the local person. The …As / …For calls take the participant explicitly; the editor's presence dropdown passes localPerson() to every one of them, so whatever drives the editor's UI, an agent through gui.click included, acts as the person. Each reader's state lives on this device, keyed by the reader's stable key: under /source/local/agents/names/ every name it has held with the period it held it, and under /source/local/agents/readers/ its read marks, the chat posts it has read, and when it last read at all. A message addressed to a name is the reader's when it was sent while the reader held that name:

  • An agent's first name is held from its session's start on the roster. A person's display name and an unnamed caller's key are held from the start, so a person who takes a display name another person used within the last 7 days also lists that person's messages to it.
  • The trusted presence service records each rename of this engine's agents as it happens (recordRename), including a presence frame that renames a local agent: the old name's period ends and the new one's begins at the rename.
  • A reader found under a name its record does not hold closes the recorded name at that read and opens the new one at its previous read, so the time between reads reaches both names.
  • A departed agent's name stays open in its record until the reader is next found under another name.
  • A name record past 64 KB, or one that does not decode, is read as none and starts again from the current name.
  • Stamps from different clocks agree to about a second, so each period reaches one second past both its ends: a message sent within a second of a rename is both holders'.

A reader's file is written when one of its marks moves, and otherwise at most once a minute for its last read alone. The chat setting showOtherAccountsAgentsInChat lives under /source/local/agents/ too; with it off, chat posts by agents working for another account are hidden (chatVisible). The roster decides whose a sender on it is, and a sender that has left is read by its post's kind and account.

Each direct message carries receipts, one synced file per recipient under /source/agents/receipts/<message id>/<reader key>.json holding { key, name, notifiedAt?, readAt? }: the recipient's stable reader key (written as a file name by the same reversible encoding as the reader files), the name it went by, when its engine told it of the message, and when it first read it, each in milliseconds since the Unix epoch. The trusted service writes notifiedAt for each agent on its engine it posts the notice to, once per message and recipient; a recipient's own first read (readFor, the dropdown's included) writes readAt, and a later read leaves it as it is. A person has no notice, and the editor's unread badge counting a message is not a notice either, so a person's receipt holds readAt only, written when they open the message. A chat post carries none. The sender's list with sent, and its read of its own message, carry receipts = { { name, notifiedAt?, readAt? } }; receiptsOf(id) answers the same. Receipts are removed with their message by the retention rule above. A receipt file past RECEIPT_MAX_BYTES (1 KB, read with vfs.readBounded), or with no key matching its file name, no name, a stamp that is no number or more than a day ahead, or neither stamp, is left out, and at most RECEIPTS_MAX (256) are read for one message. Only the recipient's own reading reaches a receipt; every other read mark stays on the device.

Each VM decodes a message or receipt file once and keeps what it decoded until the file is written or removed, so a walk over the folders reads only the files that changed since the last one.

Each engine tells the agents working on it of each message and chat post that lands there addressed to them, by the rules above, as a notice on their next tool call: a direct message its recipients, a chat post every agent but its sender that the chat setting lets it through to. The notice carries the direct-message note or the chat warning, and the message's id, sender and first line. Each message id is announced at most once per engine run, and the messages a world holds when the engine first takes it are where the engine starts, not arrivals. A file whose name is no message id, that does not decode as a message (a kind other than agent or person, an account that is no string, a to other than "*" or a list of names, or a replyTo that is no message id included), whose id is stamped more than a day ahead, or that is past MESSAGE_MAX_BYTES (64 KB, skipped with one warning, and read with vfs.readBounded, so a larger file's bytes are not loaded) is no message to list, read, readChat or a notice. A recipient on another engine is told by that engine when the file reaches it.

CallAnswer
me() / localPerson()The caller as a participant; the person signed in on this engine.
participants()Every name on the world, and the names held more than once.
resolve(to)The recipient list, or a refusal naming the problem.
send(to, body, replyTo?) / post(body)The new message's id.
list(opts?)Headers of the messages addressed to the caller by others, or with sent the ones it sent (always read, each with its receipts); filtered by to (named to that recipient), from, unread, since.
read(id) / unread(id)The message, marked read, with its receipts when the caller sent it and its readAt recorded on the caller's receipt when it did not; or the mark removed from a message sent to the caller. Either refuses a message neither addressed to the caller nor sent by it, and unread refuses one the caller sent, which is always read.
receiptsOf(id) / recordReceipt(id, key, name, field, at)A direct message's receipts by name; record notifiedAt or readAt on one reader's receipt, leaving a field it already holds.
readChat(since?)The visible chat posts, each marked untrusted: those stamped after since (0 for all within retention), or with no since the ones by others the caller has not read. The posts returned are read for the caller from then on.
sendAs(me, to, body, replyTo?) / postAs(me, body) / listFor(me, opts?) / readFor(me, id) / readChatFor(me?, since?)The calls above for the participant me instead of the caller; listFor with withBody puts each message's body on its header.
markChatReadFor(me, ids)Mark the chat posts ids read for me, as a read that returned them would.
unreadCount()The local person's unread messages from others plus the visible chat posts by others they have not read.
settings() / setSetting(key, value)The chat setting on this device.
heldNames(key) / recordRename(key, old, new, oldSince?)The names a reader has held and when; record a rename as it happens.
isId(id) / loadMessage(dir, id)Whether id has a message id's form; the message, or nil and why (unreadable, too_large, malformed).
heldFor(me)The names the reader me holds as list reads them, leaving its record as it is.
heldAt(held, name, at) / isAddressedTo(msg, held)Whether a reader with these held names held name at at, and whether msg is addressed to it.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

Direct messages and the world chat between the people and agents on a world: one synced file per message under /source/agents/, never committed, and each reader's marks and chat setting on this device only.

isId(id: any) → boolean

Whether `id` has a message id's form: <13-digit utc-ms>-<sender>-<6 hex digits>.

argtypedescription
idany

stampOf(id: string) → number

The stamp the message id `id` carries.

argtypedescription
idstring

warnOnce(about: string, text: string) → void

argtypedescription
aboutstring
textstring

nowMs( ) → number

roster( ) → any

slug(s: string) → string

argtypedescription
sstring

_idAt(ms: number, from: string) → string

A new message id for `from` at `ms`: the stamp, the sender's slug, and six hex digits from OS entropy, so two ids built for one stamp and sender differ.

argtypedescription
msnumber
fromstring

agentOf(r: any, name: string) → void

The person listing agent `name`, and the agent's entry, or nil.

argtypedescription
rany
namestring

personNamed(r: any, name: string) → any

The person whose display name is `name`, or nil.

argtypedescription
rany
namestring

localPerson( ) → Me

The person signed in on this engine as a participant, or nil when there is none.

me( ) → Me

The caller as a participant. A named agent goes by its roster name; a caller the roster names no agent goes by its own caller key; engine-authored work with no caller is the local person.

participants( ) → void

holdersOf(r: any, name: string) → void

Who on the roster `r` holds `name`: each person whose display name it is, and the agent whose name it is with the person it works for.

argtypedescription
rany
namestring

resolve(to: any) → any

`to` as the recipients a message is stored with: "*" for everyone (also when a list names "*"), else the list of names, each held by exactly one participant on the world.

argtypedescription
toany

ensureIgnored( ) → void

write(verb: string, dir: string, from: string, fields: { [string]: any }) → string

argtypedescription
verbstring
dirstring
fromstring
fields{ [string]: any }

requireMe(verb: string) → Me

argtypedescription
verbstring

requireBody(verb: string, body: any) → void

argtypedescription
verbstring
bodyany

requireId(verb: string, id: any) → void

argtypedescription
verbstring
idany

kindOf(me: Me) → string

argtypedescription
meMe

sendAs(me: Me, to: any, body: string, replyTo: string?) → string

Send as `me`: the caller for `send`, the local person for the editor.

argtypedescription
meMe
toany
bodystring
replyTostring?

send(to: any, body: string, replyTo: string?) → string

argtypedescription
toany
bodystring
replyTostring?

postAs(me: Me, body: string) → string

Post to the world chat as `me`.

argtypedescription
meMe
bodystring

post(body: string) → string

argtypedescription
bodystring

validKind(kind: any) → boolean

argtypedescription
kindany

validTo(to: any) → boolean

Whether `to` is absent, "*", or a list of names.

argtypedescription
toany

forget(path: string) → void

Drop what is kept of the file or folder at `path`.

argtypedescription
pathstring

watchParsed( ) → boolean

The watcher that drops a changed file's entry from `parsed` and `receiptLists`, registered once as this module loads in a VM. Answers whether it is registered; without it nothing is kept, and every walk reads each file.

copyMessage(m: Message) → Message

argtypedescription
mMessage

readMessage(dir: string, id: string) → void

The message `id` in `dir` read from its file, or nil and why, as `loadMessage` answers.

argtypedescription
dirstring
idstring

keep(path: string, id: string, m: Message?, why: string?) → void

Keep what reading `path` answered, while the watcher reports its changes. A file that did not read, or whose stamp may yet come into range, is read again next time.

argtypedescription
pathstring
idstring
mMessage?
whystring?

loadMessage(dir: string, id: string) → void

The message `id` in `dir`, or nil and why: "unreadable" for a file that does not read, "too_large" for one past MESSAGE_MAX_BYTES (warned of once, and its bytes never read where the VFS knows its length), "malformed" for one that does not decode as a message or whose id is stamped more than FUTURE_BOUND_MS past now. Reads the file.

argtypedescription
dirstring
idstring

cachedMessage(dir: string, id: string) → Message

The message `id` in `dir` as the last read of its file decoded it, frozen; the file is read only when it changed since. Nil for a file that is no message.

argtypedescription
dirstring
idstring

__parseCounters( ) → void

How many message files and receipt files this VM has read, and how many decoded answers it holds.

ids(dir: string) → void

argtypedescription
dirstring

outOfRange(at: number, now: number) → boolean

Whether a mark or a file stamped `at` is past retention at `now`, or stamped too far ahead to be a message.

argtypedescription
atnumber
nownumber

prune(dir: string) → void

Remove the files in `dir` whose ids are stamped past retention, or too far ahead to be messages, with what was decoded of them.

argtypedescription
dirstring

keyFile(key: string) → string

A key as a file name: every character outside a-z, 0-9, _ and - is written as ~ and its two hex digits, so distinct keys stay distinct on a case-insensitive disk.

argtypedescription
keystring

_readerPath(key: string) → string

argtypedescription
keystring

_namesPath(key: string) → string

argtypedescription
keystring

_receiptPath(id: string, key: string) → string

The receipt of the reader keyed `key` for the direct message `id`.

argtypedescription
idstring
keystring

validStamp(v: any, now: number) → boolean

Whether `v` is absent, or a stamp no later than FUTURE_BOUND_MS past `now`.

argtypedescription
vany
nownumber

readReceipt(path: string, file: string) → void

The receipt in the file `file` at `path`, frozen, or nil and why: "unreadable", "too_large" (past RECEIPT_MAX_BYTES, warned of once), "malformed" (no `key` whose file name is `file`, no `name`, a stamp that is no number, or neither stamp), or "ahead" (a stamp more than FUTURE_BOUND_MS past now).

argtypedescription
pathstring
filestring

cachedReceipt(path: string, file: string) → any

The receipt at `path` as the last read of its file decoded it; the file is read only when it changed since. Nil for a file that is no receipt. A receipt stamped ahead is read again next time, since it may come into range.

argtypedescription
pathstring
filestring

receiptsOf(id: string) → void

The receipts of the direct message `id`, one per recipient whose engine told it of the message or who read it, ordered by name: each the name the recipient went by, `notifiedAt` and `readAt`. A receipt file past RECEIPT_MAX_BYTES or that does not decode as a receipt is left out, and at most RECEIPTS_MAX files are read for one message. Each VM reads a receipt file again only after it changes.

argtypedescription
idstring

recordReceipt(id: string, key: string, name: string, field: string, at: number) → boolean

Record on the receipt of the reader keyed `key` for the direct message `id` its `field` ("notifiedAt" or "readAt") as `at`, under the name `name`. A receipt that already holds `field` is left as it is. The receipt syncs to the world with the message. Answers whether the receipt was written; raises when the write fails.

argtypedescription
idstring
keystring
namestring
fieldstring
atnumber

pruneReceipts( ) → void

Remove the receipt folders of the messages whose ids are stamped past retention, or too far ahead to be messages, with what was decoded of them.

pruneDirect( ) → void

Remove the direct messages past retention, and their receipts.

readJson(path: string) → void

argtypedescription
pathstring

writeJson(path: string, t: any, what: string) → void

argtypedescription
pathstring
tany
whatstring

heldNames(key: string) → void

The names the reader keyed `key` has held on this device, oldest first, each with the period it held it. The one record of them: the trusted presence service appends each rename of this engine's agents as it happens, and a reader's first read opens its first name.

argtypedescription
keystring

recordRename(key: string, old: string?, new: string, oldSince: number?) → void

Record that the reader keyed `key` went from `old` to `new` now. A reader with no record yet held `old` from `oldSince`. Recording the name it already holds changes nothing.

argtypedescription
keystring
oldstring?
newstring
oldSincenumber?

reader(me: Me) → any

The reader's state on this device: `read` (its read marks), `chatSeen` (the chat posts it has read) and `lastSeen` (when it last read at all). `changed` records whether a mark moved since the file was read, and is not stored.

argtypedescription
meMe

mark(st: any, set: string, id: string, on: boolean) → void

Set the mark `id` in the reader's `set` ("read" or "chatSeen") to `on`.

argtypedescription
stany
setstring
idstring
onboolean

heldNow(me: Me, st: any) → void

The names `me` has held, and whether they differ from its record. A reader with no record opens its first name at `me.since`. A record whose current name is another one (a rename no engine here recorded) closes that name now and opens the new one at the reader's last read, so the time between that read and now reaches both names.

argtypedescription
meMe
stany

heldFor(me: Me) → void

The names `me` has held, as `list` reads them for it, leaving its record as it is.

argtypedescription
meMe

heldBy(me: Me, st: any, quiet: boolean?) → void

The names `me` has held, as `heldNow` reads them. The record is saved when it changes; with `quiet`, a failed save is logged.

argtypedescription
meMe
stany
quietboolean?

saveReader(me: Me, st: any) → void

Save the reader's state: its marks, less those past retention, and now as its last read. The file is written when a mark moved, or when the last read it holds is LAST_SEEN_STEP_MS behind; this VM keeps the exact last read.

argtypedescription
meMe
stany

heldAt(held: { Held }, name: string, at: number) → boolean

Whether the reader whose names are `held` held `name` at `at`, reading each period CLOCK_SLACK_MS wider at both ends.

argtypedescription
held{ Held }
namestring
atnumber

isAddressedTo(msg: Message, held: { Held }) → boolean

Whether `msg` is addressed to the reader whose names are `held`: it names one the reader held when it was sent, or it is to everyone and the reader did not send it.

argtypedescription
msgMessage
held{ Held }

listFor(me: Me, opts: any?) → void

The direct messages of `me`, oldest first. By default the ones addressed to it by others; with `sent`, the ones it sent, which read as read. With `withBody`, each header carries the message's body too.

argtypedescription
meMe
optsany?

list(opts: any?) → void

argtypedescription
optsany?

ownMessage(verb: string, id: any, me: Me) → void

The direct message `id` when it is `me`'s: addressed to it, or sent by it; the reader's state; and whether `me` sent it. `verb` names the call in a refusal.

argtypedescription
verbstring
idany
meMe

readFor(me: Me, id: string) → Message

The direct message `id`, marked read for `me`. A message `me` sent carries its receipts; one sent to `me` records on `me`'s receipt when it first read it, and a receipt that is not written is logged and leaves the read as it is.

argtypedescription
meMe
idstring

read(id: string) → Message

argtypedescription
idstring

unread(id: string) → void

argtypedescription
idstring

settings( ) → void

setSetting(key: string, value: boolean) → void

argtypedescription
keystring
valueboolean

chatVisible(msg: Message, showOthers: boolean, localAccount: string?, r: any) → boolean

Whether the chat post `msg` shows to the account `localAccount`. With `showOthers` off, a post by an agent working for another account is hidden. The roster decides who a sender on it is and whose; a sender that has left the roster is read by its post's `kind` and `account`.

argtypedescription
msgMessage
showOthersboolean
localAccountstring?
rany

chatFor(me: Me?, since: number?, markRead: boolean) → void

The chat posts visible to `me`, oldest first, each marked untrusted, as `readChatFor` selects them. With `markRead`, the posts returned are read for `me` from then on.

argtypedescription
meMe?
sincenumber?
markReadboolean

readChatFor(me: Me?, since: number?) → void

The chat posts visible to `me`, oldest first, each marked untrusted. With `since`, the posts stamped after it (0 for every post within retention); without, the posts by others `me` has not read yet. The posts returned are read for `me` from then on; with no `me`, nothing is marked.

argtypedescription
meMe?
sincenumber?

markChatReadFor(me: Me, postIds: { string }) → void

Mark the chat posts `postIds` read for `me`, as a read that returned them would.

argtypedescription
meMe
postIds{ string }

readChat(since: number?) → void

The caller's visible chat posts, as `readChatFor` reads them for it.

argtypedescription
sincenumber?

pendingFor(opts: WaitOpts) → void

What `waitFor` finds for the caller now: its unread direct messages and, with `opts.chat`, the chat posts by others it has not read, each from `opts.from` when that is named. Marks nothing read.

argtypedescription
optsWaitOpts

waitFor(opts: WaitOpts?) → Waited

Block until a direct message addressed to the caller is unread, or with `chat` a world chat post by someone else is, or until `timeout` seconds of wall-clock time pass. Answers at once when one is already unread. The folders are scanned again only when the watcher reports a message file written or removed. Marks nothing read.

argtypedescription
optsWaitOpts?

unreadCount( ) → number

The local person's unread direct messages plus the visible chat posts by others they have not read. Never yields.

⌬ Types
Me = { name: string, key: string, account: string?, isPerson: boolean, since: number }Participant = { account: string, isPerson: boolean }Receipt = { name: string, notifiedAt: number?, readAt: number? }Message = { id: string, from: string, kind: string?, account: string?, to: any, at: number, body: string,Header = { id: string, from: string, to: any, at: number, read: boolean, body: string?, receipts: { Receipt }? }ChatMessage = { id: string, from: string, kind: string?, account: string?, to: any, at: number,Held = { name: string, since: number, ended: number? }ChatHeader = { id: string, from: string, kind: string?, at: number, untrusted: boolean }WaitOpts = { timeout: number?, chat: boolean?, from: string? }Waited = { messages: { Header }, chat: { ChatHeader }, timedOut: boolean }

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

6items
▣
module · born here
❒asset
# json JSON encode/decode library for Luau. Encodes Lua values to JSON strings and decodes JSON strings back to Lua values. Used for communication with the Rust side of the engine, the VFS read/write bridge, and any wire-format that needs JSON. Pure Luau, no engine dependencies. Compact and pretty-printed encoders, plus a hand-rolled decoder that streams the input by position so it works under WASM as well as native. ## Exports - `Json.encode(value: any, indent?: string, currentIndent?: string) -> string` — compact encode. Functions / unknown types and NaN/Inf encode as `null`. - `Json.encodePretty(value: any, indentStr?: string) -> string` — pretty-printed encode with sorted object keys (diff-friendly). - `Json.encodeArgs(...: any) -> string` — encode varargs as a JSON array. - `Json.decode(str: string) -> any` — decode a JSON string. Returns the decoded value, or `nil` + error message on failure. ## Usage ```luau local Json = require("@builtin::modules.json") local widget = { type = "button", text = "Click Me" } local compact = Json.encode(widget) -- '{"text":"Click Me","type":"button"}' local pretty = Json.encodePretty(widget, " ") local decoded = Json.decode(compact) local v, err = Json.decode("oops") -- v = nil, err = error message ``` ## Notes - Object keys are sorted alphabetically in both encoders for consistent output across runs. - Numeric keys on objects are stringified at encode time (JSON has no numeric keys). Pure-integer key sets get detected as arrays via `isArray` and encoded with brackets. - NaN, +Inf, -Inf encode as `null` — JSON has no representation. Round trips through `decode` recover `null` (Lua `nil`), so they don't preserve. - Unicode `\uXXXX` escapes decode to UTF-8 by hand to stay WASM-safe. Only the BMP is covered; supplementary planes via surrogate pairs are not. - Functions encode as `null`. - Decode is character-streamed — no regex, no `string.match` patterns on the whole input — so the line-and-column information needs to be reconstructed from the position offset.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
backing path · modules/messages.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.