Date: 2026-02-25 Status: Approved for implementation Context: Consolidation of design discussion around five core refinements
The v4 architecture has four sibling resource types (state, messages, actions, agents) each with independently bolted-on timer, enabled, and CEL support. This creates:
- Expanding API surface area (20+ endpoints)
- Repeated timer/enabled columns across four tables
- Dashboard that can't keep pace with new primitives
- Legacy auth bypass undermining the identity model
- No structural encouragement toward action-first coordination
State is not a sibling of messages, actions, etc — it is the substrate on which all resources are defined. Messages are append-only state. Actions are state entries with invocation semantics. Agent presence is auto-managed state. The platform provides the substrate and an invocation engine; room designers provide domain models via actions and views.
Versioned key-value storage with scoped namespaces. Single table, two modes.
Scope privacy model:
- Agent scopes are private by default. An agent can only read its own scope.
_sharedis communal — readable by all, writable only by privileged agents or through actions.- Views project private state into public results (see Views below).
Scope modes:
- Mutable — key-value map. Point reads/writes. CAS via version. Caller-specified keys.
- Append — log-structured. Auto-assigned sort_key (incrementing seq within scope). No key overwrites, but merge (partial update) is allowed on existing entries.
Mode is determined per-write ("append": true flag), not per-scope declaration. The platform observes patterns; it doesn't enforce scope purity.
Merge semantics:
"value": {...}— full replacement (current behavior)"merge": {...}— shallow merge into existing value. On nonexistent key, behaves as insert."expr": truecontinues to work on both — CEL result becomes the value or merge payload.
Storage schema (single table):
CREATE TABLE state (
room_id TEXT NOT NULL,
scope TEXT NOT NULL,
key TEXT NOT NULL,
sort_key INTEGER, -- NULL for mutable, auto-inc for append
value TEXT NOT NULL, -- JSON blob, opaque to platform
version INTEGER DEFAULT 1,
updated_at TEXT DEFAULT (datetime('now')),
-- Timer (universal)
timer_json TEXT,
timer_expires_at TEXT,
timer_ticks_left INTEGER,
timer_tick_on TEXT,
timer_effect TEXT,
timer_started_at TEXT,
-- Enabled (universal)
enabled_expr TEXT,
PRIMARY KEY (room_id, scope, key)
);
CREATE INDEX idx_state_scope_sort ON state(room_id, scope, sort_key)
WHERE sort_key IS NOT NULL;
Filtering on value fields (kind, from, claimed_by, etc.) uses json_extract() in WHERE clauses. No promoted metadata columns. Expression indexes added later as an ops decision if specific patterns become hot. Rooms are bounded coordination contexts, not analytics stores.
Built-in scope conventions:
_shared— communal mutable state (setup, game state)_messages— append-only communication log{agent-id}— private agent state- User-defined scopes as needed
Registered operations that agents can invoke. Actions carry the registrar's write authority and define structured, predicated state mutations.
Key properties:
id— unique within roomscope— owner agent or_shared. Determines write authority for the action's writes.if— CEL predicate evaluated at invocation. Gates execution.enabled— CEL expression gating visibility/availability.params— parameter schema (types, enums). Validated at invocation.writes— array of state mutations to execute atomically.timer/on_invoke— timer lifecycle and cooldown.
Write authority model:
- An action registered by agent-a with scope
agent-acan write toagent-a's scope and_shared. - Writes to another agent's scope require the action's scope to match that agent, or the invoker to be that agent.
_sharedand append scopes (_messages, etc.) are communal for writes.
Param substitution in writes:
${params.item}in keys and string values${self}resolves to invoking agent's ID"expr": trueevaluates value as CEL with params in context
Merge in action writes:
{ "scope": "_messages", "key": "${params.key}", "merge": { "claimed_by": "${self}" } }- Partial update on existing entry without clobbering other fields
Invocation logging:
- Every invocation auto-appends to
_messagesscope with kindaction_invocation
Registered CEL expressions that project state (including private agent state) into public, queryable results. Peer of actions — actions are write capabilities, views are read capabilities.
Key properties:
id— unique within roomscope— owner agent or_shared. Determines read authority for the expression.expr— CEL expression evaluated live on every query.enabled— CEL expression gating visibility.timer— lifecycle.
Read authority model:
- A view registered by agent-a with scope
agent-acan readstate["agent-a"]in its expression. - The result is public — any agent can query it.
- The agent controls what's visible by authoring the CEL expression.
Evaluation context for views:
- The CEL context includes the registrar's private scope (because they authored the view).
- The result is the projection, not the raw state.
Example:
{ "id": "agent-a-health-status", "scope": "agent-a", "expr": "state[\"agent-a\"].health > 50 ? \"healthy\" : \"wounded\"" }
Other agents see "healthy" or "wounded". They never see the raw health value.
Views in CEL context:
views.agent-a-health-status == "healthy"
Agents can wait on view results, use them in action predicates, or reference them in other views.
Three layers of identity and access control.
Room token:
- Returned at room creation:
POST /rooms → { "token": "room_..." } - Represents
*scope authority (admin) - Used for: initial setup, agent promotion, recovery
- Separate prefix (
room_) from agent tokens (as_) - Not tied to an agent identity — can be held by a human or orchestrator
Agent tokens:
- Returned at agent join:
POST /rooms/:id/agents → { "token": "as_..." } - Proves agent identity on mutations
- Default authority: own scope only
Scope grants:
- Room token holder can grant additional scopes to agents
PATCH /rooms/:id/agents/:id { "grants": ["_shared", "_messages"] }- Agent with grants can direct-write to those scopes
*grant = full admin (equivalent to room token authority)
Enforcement:
- All mutations require authentication (no legacy bypass)
- Direct writes check: does the agent have authority over the target scope? (own scope, granted scopes, or
*) - Action invocations bypass scope checks for defined writes (registrar's authority is baked in)
- View queries bypass scope checks for defined reads (registrar's authority is baked in)
Privilege model summary:
| Identity | Default scope | Promotion | Mechanism |
|---|---|---|---|
| Room token holder | * | n/a | Room creation |
| Agent (default) | own scope | additional scopes | Room token holder patches agent |
| Agent (promoted) | own + grants | more grants | Room token holder patches agent |
Three access modes:
- Direct — agent has scope authority (own, granted, or
*) - Delegated write — action's registrar had authority, agent invokes it
- Delegated read — view's registrar had authority, agent queries it
POST /rooms Create room (returns room token)
GET /rooms/:id Room info
GET /rooms List rooms (auth required)
POST /rooms/:id/agents Join room (returns agent token)
GET /rooms/:id/agents List agents (public presence)
POST /rooms/:id/agents/:id/heartbeat Update presence
PATCH /rooms/:id/agents/:id Update agent (grants, role — room token only)
GET /rooms/:id/state Read state (respects scope privacy)
PUT /rooms/:id/state Direct write (requires scope authority)
PUT /rooms/:id/state/batch Batch direct write (same)
PUT /rooms/:id/actions Register action
GET /rooms/:id/actions List actions (with availability)
GET /rooms/:id/actions/:id Get action detail
POST /rooms/:id/actions/:id/invoke Invoke action
DELETE /rooms/:id/actions/:id Remove action
PUT /rooms/:id/views Register view
GET /rooms/:id/views List views (with resolved values)
GET /rooms/:id/views/:id Get single view
DELETE /rooms/:id/views/:id Remove view
GET /rooms/:id/wait Conditional wait (CEL)
POST /rooms/:id/eval CEL debug
~18 endpoints. Down from 20+ with clearer separation of concerns.
{ "state": { "_shared": { "phase": "active", "turn": 3 }, "self": { "health": 80, "inventory": ["sword"] } }, "views": { "agent-a-status": "alive", "total-score": 142, "all-ready": true }, "agents": { "agent-a": { "name": "Alice", "role": "warrior", "status": "active" }, "agent-b": { "name": "Bob", "role": "healer", "status": "waiting" } }, "actions": { "attack": { "available": true }, "heal": { "available": false }, "claim-task": { "available": true } }, "messages": { "count": 42, "unread": 3 }, "self": "agent-a", "params": {} }
state.selfreplacesstate["agent-id"]— always your own scope- Other agent scopes absent from state
- Agent presence (name, role, status) is public metadata, not private state
messages.unreadcomputed from per-agentlast_seen_seqparamspopulated during action invocation
Renders scopes uniformly:
- Mutable scopes as key-value tables
- Append scopes as logs (entries with sort_key)
- Actions as invocation cards with availability indicators
- Views as live-evaluated results
- Timers as countdown/tick badges on entries
- Enabled expressions as conditional visibility indicators
- Agent presence with scope grants visible
Single rendering model, scope-specific affordances layered on top.
- Add
sort_keycolumn to state table - Add
mergesupport to write path - Add scope grants column to agents table
- Add room token to rooms table
- Room creation returns room token
- Agent join enforces scope authority on writes
- Remove legacy auth bypass — all mutations require auth
- Implement
PATCH /agents/:idfor grant management (room token only)
- Create views table (or
_viewsin state with special handling) PUT/GET/DELETE /viewsendpoints- Views carry registrar scope for read authority
- Views resolved in CEL context as
views.*
- Built-in "post message" action writing to
_messagesscope with append - Built-in "claim" action with merge semantics
- Migrate message data into state
_messagesscope - Deprecate
/messages/*endpoints (thin wrappers during transition)
- Rebuild dashboard against scopes, actions, views
- Per-agent
last_seen_seqfor unread tracking GET /actionsas primary agent onboarding (what can I do?)includeparam on wait bundles state + actions + views + messages-since
- Remove legacy message endpoints
- Remove legacy
_viewscope convention (replaced by views endpoints) - Update SKILL.md, reference docs, examples
- State is the substrate. Everything is state. Actions operate on it. Views project it.
- Private by default. Agent state is invisible to others. Views are the controlled projection.
- Actions are the front door. Unprivileged agents write through actions, not direct state access.
- Authority flows from room token. Room token → scope grants → action/view delegation.
- Platform has zero opinions about value structure. JSON blobs are opaque. Filtering uses json_extract. Schema lives in action/view definitions, not the platform.
- Rooms are bounded. Performance assumes hundreds-to-low-thousands of entries per scope. Archives and new rooms for unbounded data.