Documentation
¶
Overview ¶
Package mailbox is the intra-island agent message store — Lane 5, Phase 1 of docs/inter-island-exchange-spec.md. Agents in the SAME island exchange small typed messages through the daemon (a shared blackboard / mailbox). It is the low-risk layer: same-island agents are one trust domain (they already share /workspace + home), so intra-island messaging is allowed by default.
Cross-island exchange is deliberately NOT here — that is the brokered, operator-granted, audited "link" layer (Phase 2+), with a separate deny-all posture and an action-delegation gate. Keeping the two apart is the whole point.
Index ¶
- type Message
- type MessageAction
- type Origin
- type Store
- func (s *Store) DeliverAction(island, sourceIsland, from, fromLabel, to, topic, actionType, params string) Message
- func (s *Store) DeliverExternal(island, sourceIsland, from, fromLabel, to, topic, payload string) Message
- func (s *Store) Latest(island string) int64
- func (s *Store) Poll(island, agent string, since int64) []Message
- func (s *Store) Send(island, from, to, topic, payload string) Message
- func (s *Store) SetArrivalHook(fn func(m Message))
- func (s *Store) WaitArrivalHooks(d time.Duration) bool
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Message ¶
type Message struct {
Seq int64 `json:"seq"`
Island string `json:"island"`
From string `json:"from"` // sender agent id (literal)
// FromLabel / ToLabel are the human names for From / To, daemon-resolved from
// the island roster when a message is returned (read time, so a rename
// reflects). Empty for a broadcast (no To), an unknown/cross-island handle, or
// an unlabeled agent. Storage keeps only the ids; these are filled on the way
// out so consumers render names without a second roster fetch.
FromLabel string `json:"from_label,omitempty"`
To string `json:"to,omitempty"` // recipient agent id; empty = broadcast to the island
ToLabel string `json:"to_label,omitempty"`
Topic string `json:"topic,omitempty"` // optional channel within the island
Payload string `json:"payload"`
Time time.Time `json:"time"`
// Origin is daemon-stamped provenance, set ONLY for messages delivered from
// another island over a brokered link (Lane 5). nil for ordinary intra-island
// messages. Agents cannot set it — see DeliverExternal vs Send.
Origin *Origin `json:"origin,omitempty"`
// Action, when non-nil, marks this as a cross-island ACTION delegation (Lane 5
// Phase 3) rather than free-form info: a NAMED, typed operation the recipient
// island exposed, authorized by the daemon's action gate. Set only by
// DeliverAction. The recipient runs its handler for Action.Type — it must not
// interpret Payload as a free-form prompt.
Action *MessageAction `json:"action,omitempty"`
}
Message is one intra-island message.
type MessageAction ¶
MessageAction is a named, typed action invocation carried by a cross-island action delegation. Type is one of the recipient island's exposed action types.
type Origin ¶
type Origin struct {
SourceIsland string `json:"source_island"`
CrossIsland bool `json:"cross_island"`
// FromLabel is the sender agent's display label, stamped by the daemon at
// send time from the SOURCE island's roster. A receiving island can't resolve
// another island's roster (containment), so this is the only way it can show
// a sender name instead of a bare id. Display-only + omitempty: absent when
// the sender has no label, and consumers fall back to From (the id). Like the
// rest of Origin it's unforgeable — only the cross-island delivery path sets it.
FromLabel string `json:"from_label,omitempty"`
}
Origin marks a message that entered this island's mailbox from ANOTHER island over a brokered link. It's machine-read provenance (SDK / activity feed / audit tooling branch on CrossIsland) — a structured field rather than a parseable sender-string prefix, and unforgeable because only the daemon's cross-island delivery path sets it.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a per-island ring of recent messages. The ring bounds memory so a chatty island can't grow it unbounded. When opened with a path (see Open) the store also persists every append to disk so undelivered messages AND the seq cursor survive a daemon restart — without that, a restart wiped the ring and reset seq to 1, silently dropping unread cross-session coordination (the no-lost-work bar). NewStore keeps the pure in-memory form for tests.
func NewStore ¶
NewStore returns an in-memory store retaining up to maxPerIsland messages per island. Nothing is persisted — use Open for a restart-durable store.
func Open ¶ added in v0.6.9
Open returns a store backed by path: it loads any previously persisted messages + seq on startup, and persists every append back to path so they survive a daemon restart. A missing file starts empty; a corrupt/unreadable one is logged and started empty (the daemon must still come up). A nil logger is tolerated. maxPerIsland bounds each island's ring as in NewStore.
func (*Store) DeliverAction ¶
func (s *Store) DeliverAction(island, sourceIsland, from, fromLabel, to, topic, actionType, params string) Message
DeliverAction appends a cross-island ACTION delegation into `island`'s mailbox (Lane 5 Phase 3): a named, typed operation (actionType/params) the daemon's action gate authorized. Like DeliverExternal it stamps Origin; it additionally sets the structured Action field. Distinct from Send/DeliverExternal so only the gated action path can mark a message as an action.
func (*Store) DeliverExternal ¶
func (s *Store) DeliverExternal(island, sourceIsland, from, fromLabel, to, topic, payload string) Message
DeliverExternal appends a message delivered into `island`'s mailbox from another island (sourceIsland) over a brokered link, stamping daemon-controlled Origin (CrossIsland=true). `from` is the source agent's literal id, `fromLabel` its display label resolved by the caller from the source roster (may be ""), and `to` the local recipient agent. It is distinct from Send precisely so the intra-island path can never set Origin — provenance is the daemon's to assert.
func (*Store) Latest ¶
Latest returns the highest seq retained for an island (0 if none) — a cheap cursor for "everything after now".
func (*Store) Poll ¶
Poll returns the retained messages in island visible to agent `agent` with Seq > since, ordered by Seq. Visible = broadcasts (To == "") plus messages addressed To == agent. since == 0 returns all retained. An empty agent sees only broadcasts (an operator/observer view).
func (*Store) Send ¶
Send appends a message to an island's ring and returns it with Seq/Time set. from is the sender agent id; to is a recipient agent id, or "" to broadcast to every agent in the island.
func (*Store) SetArrivalHook ¶
SetArrivalHook registers a callback fired (in its own goroutine, so it never holds the store lock or blocks the sender) after every message is appended — the wake-on-message seam (Lane 5 Phase 3.5). nil disables it.
func (*Store) WaitArrivalHooks ¶ added in v0.8.66
WaitArrivalHooks blocks until every arrival hook started so far has returned, giving up after d. It reports whether they all finished.
For tests. The hook is deliberately detached — that is the point of it, and the daemon outlives any individual delivery — but a test process does not. A hook that runs on past the end of its test does filesystem work against whatever $HOME has become by then: the next test's t.TempDir (where creating a directory during RemoveAll produces "unlinkat: directory not empty") or, if no test is running, the developer's real home. Both were happening.
The deadline is not optional, so that a hook which blocks forever surfaces as a named failure in the test that started it, rather than as the whole binary panicking at the ten-minute mark with the wrong test's name on it. That misattribution is the exact shape of bug this method exists to close.