43 KiB
Camino
Camino is the proposed database/object runtime for the next direction of Quixos. This is a working design note, not a spec.
For status tracking across implementation passes, see CAMINO_LEDGER.md.
Core Idea
Camino is a live object graph database for Quixos-style programs.
Instead of "packages with state contexts", the system becomes:
- classes define schemas, methods, subscriptions, and UI affordances
- instances are durable stateful objects
- packages provide executable behavior for methods/channels/functions
- objects and edges form a graph
- state is live, subscribable, inspectable, and eventually collaborative
The Smalltalk-ish version:
- browse objects
- inspect state
- send messages
- watch updates
- edit schemas
- compose programs out of durable live instances
The old state context ID maps roughly to the new Camino object instance ID.
A "program" becomes a root object instance of a root class, with references/edges to lower-level objects.
Quixos Execution Philosophy
Build the general-purpose path first.
Quixos should prefer the Turing-complete package/runtime mechanism as the canonical execution substrate. Higher-level conveniences such as lambda-style functions, step-function-like orchestration, declarative queries, GraphQL-ish views, UI adapters, or schema-editor shortcuts should compile down to, wrap, or cleanly eject back to package-backed execution.
The core question for a new feature is:
Can this be represented as ordinary package code with explicit schema,
objects, operations, and provenance?
If not, the shortcut is probably too magical for the kernel.
This means:
- method calls are package-backed, not built into Camino
- derived state is maintained or resolved by packages
- declarative query/view systems are later adapters over package-backed materialization
- package refs and resolved package artifacts are important provenance
- "easy" special forms should have an escape hatch to general package code
CRDT reconciliation is the main known exception. CRDT fields are specialized state machinery because their merge semantics are part of their storage type, not arbitrary application execution.
V0 Kernel
Keep the first implementation small:
- class schema
- object instance
- typed field values
- named edges
- object subscriptions
- method call -> package execution
- operation provenance
- schema migration placeholder
Explicitly defer:
- global graph query language
- query subscriptions
- privacy/sharing model
- private fields/write permissions
- root-scope GC details
- jujutsu backend
- cutover-stale redesign
- package-slice optimizer
- declarative adapter packages
- full derived state execution
- full React schema editor
Object Model
An object instance has:
- global hard-to-guess ID
- class ID
- schema version
- field values
- outgoing/incoming edges
- operation/provenance history
- subscriptions
- optional UI components/views later
Useful object envelope:
ObjectInstance {
id
classId
schemaVersion
fields
edges
createdAt
updatedAt
provenanceLog?
}
The object envelope is Camino's center of gravity. CRDTs, blobs, mounts, packages, indexes, and UI should plug into it rather than define it.
Classes
A class defines:
- fields
- edge types
- methods/functions
- async callbacks/channels
- constructor/default initialization
- schema migrations
- optional conflict-resolution hooks later
- optional React component exports later
Methods are backed by packages today. Later, individual functions/channels may be backed by smaller slice packages or lambda-style execution units.
Constructor functions are schema-declared package functions. In v0 Camino first creates the base object, then invokes the constructor through orch. Constructors must be idempotent and tolerate retry. If the constructor fails today, the base object may remain; transactional constructors/rollback are an explicit future requirement.
Schema Interfaces
Camino schema interfaces are globally named contracts over object value surfaces.
They are not protobuf inheritance. A .camino.proto message can be marked with
option (camino.interface) and fields can declare interface requirements such
as required, readable, writable, watchable, and a concrete type or type
parameter.
Classes declare option (camino.implements) with a namespaced interface ID and
optional type bindings. Schema validation checks that the class has the required
fields and operations. For stored fields, Camino's built-in storage supplies
read/watch/write semantics. For custom fields, validation checks the declared
operation service, using Get as the value type and WatchStart/WatchStop as
the watchability contract.
The first built-in interface is quixos.react.ReactComponent:
props: watchable readable value surface of type Props
source: readable string
Generated TypeScript refs should carry implemented-interface information, e.g.
TaskCardRef = ObjectRef<...> & ReactComponentRef<Props>.
source is static-final for the initial React component path: it is attached to
the schema/package implementation, not mutable per instance. The current Todo
React runtime serves the compiled JavaScript module from its package build
artifact. This is intentionally a v0 pull model. Later we may replace the string
with a blob/artifact ref, package output ref, or static cache strategy, and may
add parameterized static-final fields. Do not add mutable static state.
Fields
Fields have declared semantics, not just storage representation. More precisely, a field is a named value surface on an object. It may be a stored variable, but it does not have to be.
Likely axes:
- representation: scalar, json, blob, doc, ref, edge-set, mount
- conflict behavior: replace, preserve_conflicts, crdt
- implementation: built-in stored value, built-in CRDT, package-backed get/set, static-final package value, external substrate, later optimized built-in strategies
- indexing: none, value index, inverse edge index, custom later
- lifecycle: owned, borrowed/global link, external/mounted
Do not make every object field a deep CRDT. Instead, make CRDT document primitives easy and default-feeling where they fit.
Good CRDT field candidates:
- rich text
- collaborative docs
- canvas-ish data
- comments
- shared lists/maps where generic merge is meaningful
- counters/sets/maps when their CRDT semantics fit the domain
Suspicious CRDT field candidates:
- finite state machine status
- money/accounting state
- locks/leases/ownership
- package boot lifecycle
- migration status
- external process handles
- "exactly one active X"
- multi-field invariants
For invariant-heavy state, prefer method-backed mutation and class/package logic.
Value Surfaces
The cleaner model is operation-based:
field
named value surface on an object
may support get(params?) -> value
may support set(params?, value, extra_args?) -> ack/result
may support watch(params?) -> value updates
signal/channel
named event surface on an object
may support subscribe(params?) -> events
does not promise a retained current value
Stored Camino fields get these operations from Camino itself:
stored title
get = read current stored value
set = write through field conflict/storage policy
watch = object/field op subscription
CRDT fields are built-in value surfaces whose setter is a merge operation:
crdt notes
get = read current/materialized document state
set = merge CRDT update
watch = CRDT/document update subscription
Package-backed/custom fields use marker field types and point at a service that declares their typed operations:
camino.CustomField chest_contents = 1 [
(camino.field_ops) = {
implementation: SERVICE
service: ".quixos.minecraft.MinecraftWorldChestContents"
}
];
For stored and built-in fields, use normal protobuf field types where they make the schema easier to read:
string title = 1;
camino.CrdtRichText notes = 2;
For custom/served fields, the marker type says "this is a value surface whose
operations are served elsewhere." The service's Get return type is the value
type. This avoids duplicating a value type in both the field declaration and the
operation declaration.
This is an intentional bifurcation:
stored/built-in field
protobuf field type is the value type
custom/served field
marker field type points to a service
service Get return type is the value type
The current v0 schema still says LAZY resolver; conceptually this should move
toward camino.CustomField plus implementation: SERVICE.
Parameterized field accessors are allowed. A field name can represent a family of values indexed by input parameters rather than a single stored variable:
MinecraftWorld.chest_contents({ x, y, z })
MinecraftWorld.player_location({ player_id })
TaskList.filtered_list({ status, tag, text })
This is still a field because the result is value-like: it has a type, can be read, may be watched, may sometimes be written through a declared setter, and can appear in UI/query composition.
Signals/channels are parallel but event-like:
message MinecraftWorld {
option (camino.channel) = {
name: "player_events"
implementation: SERVICE
service: ".quixos.minecraft.MinecraftWorldPlayerEvents"
};
}
service MinecraftWorldPlayerEvents {
rpc Subscribe(PlayerEventsQuery) returns (camino.StreamStartResult);
rpc Unsubscribe(camino.StreamStopRequest) returns (camino.StreamStopResult);
}
Older sketches may use direct function refs:
option (camino.channel) = {
name: "activity"
subscribe: { symbol: "watchActivity" }
input_type: "quixos.todo.ActivityFilter"
};
Prefer the service form because protobuf validates the operation payload types.
Signals do not need a current value. Examples:
- task activity events
- build log lines
- chat messages
- Minecraft player joined/left
- payment webhook events
- UI gesture streams
The old Quixos on-channel-subscribe idea maps to two schema hooks:
field.watch(params?)
channel.subscribe(params?)
Both are demand-driven. When a client watches a field or subscribes to a channel, Camino records demand and asks orch to activate the package-backed handler for that object, slot, and parameter set. When demand drops to zero, Camino/orch should close the watch activation and let the package tear down its internal subscriptions.
Streams may become a separate runtime-level resource later:
signal/channel = schema-level event surface
stream = concrete runtime subscription ID with cursor/replay/backpressure
Do not introduce stream semantics until we have real pressure for cursors, backpressure, replay, fanout, or retention.
Operation Services
Use protobuf service/rpc declarations as a typed operation IDL, not as a
promise that Camino exposes those operations directly as gRPC services.
Canonical pattern:
message MinecraftWorld {
option (camino.class) = {
id: {
namespace: "quixos.minecraft"
name: "MinecraftWorld"
version: "1"
}
};
camino.CustomField chest_contents = 1 [
(camino.field_ops) = {
implementation: SERVICE
service: ".quixos.minecraft.MinecraftWorldChestContents"
}
];
}
message ChestQuery {
int32 x = 1;
int32 y = 2;
int32 z = 3;
}
message ChestContents {
repeated string item_ids = 1;
}
service MinecraftWorldChestContents {
rpc Get(ChestQuery) returns (ChestContents) {
option (camino.impl) = {
package_namespace: "quixos.minecraft"
package_name: "minecraft-runtime"
symbol: "getChestContents"
};
}
rpc WatchStart(ChestQuery) returns (camino.WatchStartResult) {
option (camino.impl) = {
package_namespace: "quixos.minecraft"
package_name: "minecraft-runtime"
symbol: "startChestContentsWatch"
};
}
rpc WatchStop(camino.WatchStopRequest) returns (camino.WatchStopResult) {
option (camino.impl) = {
package_namespace: "quixos.minecraft"
package_name: "minecraft-runtime"
symbol: "stopChestContentsWatch"
};
}
}
Use one service per field or channel/signal surface. Standard method names are:
field service
Get
Set
WatchStart
WatchStop
channel/signal service
Subscribe
Unsubscribe
The class field points to the operation service. Avoid service-level backrefs to the class/field as the primary binding; the object schema remains the source of truth for which fields/channels exist.
Implementation refs live on the service RPCs, not on the class field. This keeps the object schema service-shaped: the field says which typed operation surface it uses, and each operation says which package symbol handles it.
Package runtimes may expose a single operation object and let generated glue map standard RPC names onto methods of that object. In that case the implementation ref should identify both the exported symbol and the operation method:
service TaskDisplayLabel {
rpc Get(google.protobuf.Empty) returns (google.protobuf.StringValue) {
option (camino.impl) = {
package_namespace: "quixos.todo"
package_name: "todo-runtime"
symbol: "displayLabel"
operation: "get"
};
}
rpc WatchStart(google.protobuf.Empty) returns (camino.WatchStartResult) {
option (camino.impl) = {
package_namespace: "quixos.todo"
package_name: "todo-runtime"
symbol: "displayLabel"
operation: "watchStart"
};
}
rpc WatchStop(camino.WatchStopRequest) returns (camino.WatchStopResult) {
option (camino.impl) = {
package_namespace: "quixos.todo"
package_name: "todo-runtime"
symbol: "displayLabel"
operation: "watchStop"
};
}
}
For package authors, derivedWithDeps is package-side sugar for producing such
an operation object. The schema does not say "derived with deps"; it only asks
for Get, WatchStart, and WatchStop. The runtime helper can synthesize
watch operations by dependency-tracking the get function:
export const displayLabel = derivedWithDeps({
async get(ctx) {
const status = await ctx.object.status.get();
const priority = await ctx.object.priority.get();
const title = await ctx.object.title.get();
return `${status} ${priority} ${title}`;
},
});
Manual implementations use the same schema shape and export the same operation surface directly:
export const chestContents = fieldOps({
async get(ctx, coords) {
return readChest(ctx, coords);
},
async watchStart(ctx, coords, sink) {
return watchChest(ctx, coords, sink);
},
async watchStop(ctx, handle) {
await handle.close();
},
});
Generated TypeScript should make this obvious enough that agents can see which
operations are required. A later pass may use satisfies or generated operation
interfaces to make the get/watchStart/watchStop contract explicit in
package source.
Open concern: if GraphQL-like adapters and derived read provenance become the
main reactive surface, explicit watch operations may become lower-level runtime
plumbing rather than the API most package authors use directly. Return values may
eventually need provenance metadata: which object fields, parameterized fields,
channels, or adapter subqueries contributed to this value. A client that watches
the returned value could then implicitly register upstream watchers from that
provenance graph, similar in spirit to automatic differentiation tracking a
calculation graph. This may attach to watch IDs or derived evaluation IDs. Do not
over-design this yet; keep WatchStart/WatchStop as the concrete operation
surface for v0 while leaving room for provenance-driven reactivity to replace
some manual watcher declarations later.
For non-parameterized fields, use google.protobuf.Empty as the input type:
service TaskComputedLabel {
rpc Get(google.protobuf.Empty) returns (google.protobuf.StringValue);
}
Conceptually, Empty is the singleton parameter set. Parameter sets must be
serializable and canonicalizable because they become part of read/watch/cache
identity:
field operation key = object_id + field_name + canonical_param_bytes
Canonical parameter bytes are semantic identity, not display formatting. Two
parameter objects with the same protobuf/JSON value but different map key order
must produce the same key. V0 can canonicalize by lowering the Camino Value
map to normalized JSON with sorted object keys, then encoding that canonical
string. Later, when typed protobuf field APIs move into Camino, the preferred
form should be deterministic protobuf bytes for the operation input message.
This key should eventually be reused by:
GetFieldandWatchFielddedupe- package-backed watch demand leases
- derived/adaptor cache entries
- provenance/debug dependency records
- cycle and fanout accounting
- parameterized UI props subscriptions
protoc validates message types used by rpc input/output, which avoids
stringly input_type/output_type metadata. Custom options still contain a
string service name because protobuf options do not have a native
ServiceDescriptorRef value type. A Camino validator must resolve that service
name against the descriptor set.
Validator/linter rules should include:
field_ops.serviceresolves to exactly one service.- field service methods are limited to
Get,Set,WatchStart,WatchStop. - channel service methods are limited to
Subscribe,Unsubscribe. - custom field services define at least one useful operation.
WatchStartinput matchesGetinput unless an explicit exception is added.Setexists only for writable custom fields.- rpc
camino.implrefs resolve to package descriptor exports. - descriptor capability kind matches the operation kind.
- package/runtime codegen agrees with the service method signatures.
Scalar returns may use standard protobuf wrappers. Camino and generated code can manually unwrap these:
google.protobuf.StringValue -> string
google.protobuf.BoolValue -> bool
google.protobuf.Int64Value -> int64
...
Otherwise, the RPC return message is the value type.
Custom Setters
Computed/custom fields may declare setters. By default they are not writable, but a setter is useful when a write to a value-like surface needs domain logic instead of a blind register update.
Example: MinecraftWorld.player_location(player_id) can be read from a running
server, watched while a player moves, and set by external code to request a
teleport. The setter can reconcile contested writes:
server says player moved to A
external controller requests teleport to B
setter decides whether B overrides A based on package/server causal state
This is the general-purpose path. If a custom setter pattern becomes common and optimizable, Camino can later grow a built-in storage/merge strategy for it, just as CRDT fields are built-in merge strategies today.
Cache Semantics
For v0, package-backed computed/custom reads should behave as if cache is always disabled. Reads should be side-effect-free where possible.
Later cache policy can become explicit:
none
ttl(duration)
stale_while_revalidate(duration)
retain_latest(priority?)
package_invalidated
Possible future cache controls:
- TTL/freshness metadata
- stale/error/unavailable states
- package-sent invalidation/bust signal
- retention hints such as LRU priority or "cheap to recompute"
- provenance for the package/function/input that produced a cached value
Do not let cache become invisible state. A client or inspector should be able to tell whether a value is fresh, stale, unresolved, unavailable, or failed.
Conflict Semantics
Use a small set of conflict strategies for v0:
replace
preserve_conflicts
crdt
replace means concurrent writes are allowed and one value becomes canonical by
deterministic operation ordering. This is LWW-ish, but should not depend on wall
clock time. Use for low-stakes scalars/preferences where losing a concurrent
write is acceptable.
preserve_conflicts means concurrent writes do not get silently erased. Normal
reads can expose a canonical value, but conflict-aware code can inspect competing
values and resolve them. This is the Camino-friendly name for multi-value
register behavior. It is a good default for human-authored scalar fields and
single refs where overwrite loss would be surprising.
crdt means the field's own CRDT type defines merge behavior. Use for CRDT
text, rich text, json docs, lists, maps, sets, counters, etc.
Private/method-only/package-only mutation is orthogonal to conflict strategy and is deferred.
Possible future axes:
commit mode:
async | sync_authoritative
mutation policy:
direct | method | package_only
CRDT Documents
CRDTs should be first-class doc-type primitives, not necessarily the merge logic backing every field.
Automerge is the preferred first backend unless implementation work reveals a strong reason not to use it. It supports collaborative text, rich text marks and block markers, lists, maps, counters, and JSON-like documents.
Prefer explicit CRDT field types:
- CrdtText
- CrdtRichText
- CrdtJson
- CrdtList
- CrdtMap
- CrdtSet
- CrdtCounter
Avoid leaking too much CRDT taxonomy such as g_counter into app schemas unless
there is a concrete reason.
Structured Documents
Do not make a whole structured app object one giant CRDT JSON document by default. Use "CRDT islands":
- Camino object graph owns durable structure, identity, lifecycle, edges, methods, and migrations.
- CRDT fields own collaborative local content inside an object.
Example: for a Google Forms-like app, prefer:
Form
title: CrdtText
description: CrdtRichText
questions: ordered edge list -> Question
Question
prompt: CrdtRichText
helpText: CrdtRichText
kind: replace/preserve enum
options: ordered edges -> Option, or a CRDT list if that is enough
Option
label: CrdtText
stable value/id
Inside a conflict: crdt field, the nested values are governed by that CRDT
field type's rules. The policy stops at the field boundary.
Edges And Refs
Refs and edges are probably the same underlying concept.
A simple ref can lower to a named edge with cardinality:
field assignee: Ref<User>
lowers to something like:
edge Assignee from Task to User {
cardinality: optionalOne
}
A richer edge can have its own fields:
edge Membership from User to Org {
role: Role
joinedAt: Time
}
Metadata-free edges and metadata-bearing edges should use the same storage/subscription/indexing model. Schema controls:
- edge type identity
- cardinality
- uniqueness
- inverse association names
- indexing
Edge identity and cardinality matter. It should be possible to express whether there can be more than one edge of a given type between the same source/target.
Document-Local Refs
CRDT documents should not splice raw global UUIDs into content without Camino knowing they are references. Use document-local aliases plus a sidecar.
Doc content contains a small local alias:
{ "type": "mention", "ref": "r17" }
The sidecar maps aliases to Camino refs/edges:
{
"r17": {
"decl": "note_mentions",
"target": "camino://Task/018f..."
}
}
The schema maps the local declaration to a real namespaced/versioned type.
Distinguish:
- soft ref: backlink/index/search/rendering reference; no ownership/lifecycle implication
- edge: proper Camino edge projected from the sidecar; participates in graph semantics
Prefer alias-per-occurrence. Multiple aliases may target the same object.
Benefits:
- docs stay portable/readable
- UUIDs are not invisible substrings
- indexing has one canonical place to inspect
- references can have types
- references can be validated
- references can be remapped during clone/import
- references can be marked dangling when targets are deleted
For now, avoid heavy sugar here. Raw strings like "Mentions" are too loose for
indexing/migration/graph semantics. Use structured namespaced/versioned refs in
the canonical schema IR.
Schema Authoring
Use protobuf as the initial authoring syntax. Do not build a custom parser for v0.
Pipeline:
.camino.proto files
-> protoc parses them
-> FileDescriptorSet
-> Camino compiler reads descriptors + custom options
-> ClassSchema IR
-> runtime/storage/codegen
Protobuf gives:
- parser
- descriptors/reflection
- field numbers
- generated types
- cross-language method payloads
- wire-compatible evolution discipline
But protobuf descriptors alone are not Camino's full schema, because Camino also needs:
- field conflict/storage policy
- CRDT/doc kind
- edge declarations
- edge cardinality
- inverse associations
- method declarations
- package execution bindings
- subscriptions/channels
- migration functions
- document-local ref declarations
- mounted external state
- UI/component exports later
So the canonical internal schema should be Camino's own IR, likely itself encoded as protobuf messages.
Sketch:
message ClassSchema {
string id = 1;
uint32 version = 2;
repeated FieldSchema fields = 3;
repeated EdgeSchema edges = 4;
repeated MethodSchema methods = 5;
repeated Migration migrations = 6;
}
message FieldSchema {
string name = 1;
TypeRef type = 2;
StorageKind storage = 3;
ConflictStrategy conflict = 4;
}
message EdgeSchema {
string name = 1;
TypeRef from_class = 2;
TypeRef to_class = 3;
Cardinality cardinality = 4;
repeated FieldSchema fields = 5;
}
In v0, write .proto files with Camino custom options and lower them to
ClassSchema. Later, a visual editor or nicer schema syntax can also emit the
same IR.
Namespaced And Versioned References
Use structured refs for semantic identity. Avoid bare strings for anything that matters to indexing, migrations, execution, or graph semantics.
Useful general shape:
message SymbolRef {
string namespace = 1;
string name = 2;
string version = 3;
optional string hash = 4;
}
Use this shape, or a close relative, for:
- class IDs
- edge type IDs
- doc ref type IDs
- method function IDs
- migration function IDs
- CRDT doc kinds if needed
- component export IDs later
Local strings are still fine for local names:
- field name
- method name
- alias ID like
r17 - schema-local ref declaration like
note_mentions
But local names should lower to structured refs in the canonical schema IR.
Migrations
Every class has schema versions and migrations.
For now, migrations can be named function hooks, not a declarative migration language:
message Task {
option (camino.class) = {
version: 3
migrations: [
{ from: 1, to: 2, function: ... },
{ from: 2, to: 3, function: ... }
]
};
}
The function ref should be structured and resolve to a package/function/symbol.
Future migration specs may handle common cases declaratively:
- rename field
- split field
- field -> edge
- edge -> field
- default new field
- materialize derived value
- start new CRDT epoch
For ordinary fields, migration transforms current materialized values.
For edges, migration transforms relation records.
For CRDT docs, migration probably needs epoching or materialized migration:
- read current accessible state
- produce new doc/state version
- future writes target new version
- offline stale devices may need repair/import/rejection
This is acceptable for now. CRDT migration is known-risk, not a blocker.
Packages And Function Refs
Packages stop being the conceptual center. Schemas/classes are the semantic center; packages become execution providers.
A class schema declares executable slots and points each slot at structured Quixos code refs:
Task.assign -> quixos.todo/task-runtime#task.assign
Document.renderPreview -> quixos.docs/document-ui#document.render_preview
MinecraftWorld.start -> quixos.minecraft/minecraft-server#world.start
Function refs should be structured, not just strings:
package quixos;
message FunctionRef {
string package_namespace = 1;
string package_name = 2;
string symbol = 3;
optional string version_ref = 4;
}
version_ref might be a VCS hash, a flake hash, or a content-addressed package
artifact ref.
Important distinction:
- declared ref: what the schema slot asks for
- resolved ref: exact package artifact that was actually executed
For migrations especially, provenance should record the exact resolved ref, not only the declared one.
Package descriptors are Quixos runnable capability manifests. They prove that a code ref exists, what kind of slot it can satisfy, how to run it, and what build or runtime protocol it needs. They do not own the class contract.
For functions, descriptor capabilities should at least distinguish methods, field resolvers, and migrations so schema validation can catch a symbol that exists but is wired into the wrong kind of slot.
For v0, a schema's executable slots should generally point into one package lineage for that class. If a workflow coordinates many object types, model that as a coordinator class with refs/edges to other objects, not as one package implementing unrelated classes.
Eventually dependencies may be declared at function/channel/field granularity, but the schema remains the place where slot refs are declared.
Orchestrator And Object Activation
Camino should request work, not process lifecycle.
The normal Camino-to-orchestrator path is:
InvokeFunction(functionRef, objectId, input)
WatchFunction(functionRef, objectId, input)
The orchestrator decides whether that work requires starting a package process, reusing one, attaching an object, keeping state warm, or shutting something down later. Camino should not need to know whether a package is already running.
Object activation is the replacement for old state-context opening, but it is an orchestrator-internal lifecycle model rather than a normal Camino API call:
old: open state context(package, stateContextId)
new: invoke/watch/render/follow object -> orch activates package for object
Following, watching, rendering, resolving a lazy field, invoking a method, or otherwise using an object ref can create demand for object-scoped package code. The package may receive an object id, object snapshot, invocation input, and an object-scoped working directory similar to today's state-context directories.
Activation has an explicit end. The exact policy is not settled, but v0 should assume:
demand/refcount + idle timeout + explicit close
When demand drops to zero, orch can start an idle timer, ask the package to detach/close, and eventually kill it if it leaks side effects or memory.
Some objects may declare package-backed sidecars or "babysitters" later: code that is expected to run while the object is active because the object represents external/mounted state, a UI surface, a server process, or another side-effectful runtime. This should still lower to function/watch/render work plus orch activation, not a separate execution substrate.
Open questions:
- exact close/deactivation semantics
- how object working directories are named and migrated
- whether activation state is ever durable or only orch-local
- how to represent "keep warm" policy without making it a second runtime model
- how packages report readiness, freshness, and graceful shutdown progress
Method Calls
Make method calls explicit operations. Avoid "random code mutates JSON" as the main model.
Method call record should include:
- method name
- target object
- args
- package/function
- actor/provenance
- result or error
- state changes caused
Open semantic question: should objects be actor-like?
Current leaning: each object should at least conceptually serialize its own method calls. This makes invariant-heavy logic easier. Multi-object consistency can remain explicit/heavier later.
Direct field writes can still exist for simple data.
Subscriptions
Initial subscription model can stay object-oriented:
- subscribe to object
- maybe subscribe to field/deep doc
- maybe subscribe to edge set
Query subscriptions can wait. They are useful but not needed for step one.
Be careful with deep graph subscriptions. It is easy to accidentally pull in a large object graph without explicit scope boundaries.
Derived State And Adapters
Derived state should not reintroduce old Quixos "values" as a separate ontology. If something is subscribable, debuggable, referenceable, migratable, or worth showing in an inspector, it should usually have Camino object identity.
The default pattern:
source objects
-> watched by package process
-> package writes or resolves derived object/field
-> UI/other packages subscribe to that object/field
Distinguish:
- authoritative objects: source of truth for user/system state
- materialized derived objects: package-maintained outputs with provenance
- custom/computed fields: resolved only when read or watched
- external-backed objects/fields: canonical state lives inside package-owned substrate such as a Minecraft server, filesystem, Postgres, or process memory
Parameterized computed fields handle cases where there may be infinitely many possible values to ask for. Example:
MinecraftWorld
level/process handle
ChestView
world -> MinecraftWorld
x/y/z coordinates
contents: custom field resolved by minecraft-runtime#getChestContents
ChestView.contents({ x, y, z }) may not be computed until something reads or
watches that parameter set. On watch, Camino should ask the package for a
watcher. When no watchers/read leases remain, the resolver can detach.
Non-stored fields need explicit freshness/error semantics:
unresolved | resolving | fresh | stale | error | unavailable
That metadata should make it clear whether an empty value is real, stale, uncomputed, or blocked by a resolver failure.
Declarative tools such as GraphQL-ish state mapping should come later as shortcuts that generate or configure package-backed derived objects/fields. They must cleanly fall back to package code.
GraphQL-ish Adapters
GraphQL-like adapters should start life as ordinary packages, not Camino kernel features. A future built-in query engine can be an optimized replacement for the same pattern.
Sketch:
TaskList.filtered_list(filters)
-> TaskList package resolves/constructs a GraphQLAdapter for this TaskList
-> GraphQLAdapter reads TaskList(ID).items[*].type/status/title/etc.
-> adapter subscribes to the subfields needed for the query
-> adapter publishes filtered list updates
The adapter may be:
- constructed by the owning object's constructor when all inputs are known
- constructed lazily when a parameterized field is first read/watched
- reused across subscribers with the same object/parameter set
- owned by a normal Camino object if it needs identity, state, or inspection
Watching a field like TaskList.filtered_list(filters) may therefore activate
the TaskList package first, so it can decide how to construct or route through
the adapter. This is acceptable: it keeps the declarative shortcut ejectable to
ordinary package code.
Evaluation graphs can contain cycles:
field A depends on adapter B
adapter B reads field C
field C indirectly watches field A
V0 should not try to prove all cycles impossible declaratively. It should protect the runtime:
- track evaluation stack/object/field/params during reads
- fail or warn on direct cycles
- impose depth and fanout limits
- record dependency edges for debugging
- expose cycle warnings in the admin inspector
- avoid hidden eager evaluation of large graph scopes
Transparent Writes Through Adapters
Some adapters can forward writes to underlying state. A derived field does not have to be read-only if its schema declares a setter.
Example: a query result may expose a CRDT document field that is merely passed through from an underlying object. If the adapter can prove the path is a simple projection, a write to the derived surface can be forwarded to the original CRDT field instead of becoming a separate mutation API.
This gives adapters a "transparent" feel:
write TaskList.filtered_list(filters)[3].notes
-> adapter recognizes notes as pass-through
-> forwards CRDT update to Task.notes
Do this carefully. A transparent write is only valid when the adapter can unambiguously map the write target back to an underlying object/field and when the target field's setter/merge policy accepts that write. Otherwise the write must fail or route through an explicit method/custom setter.
Provenance
Track operation provenance early:
- actor
- package/function
- logical timestamp/op ID
- schema version
- causal dependencies
- correlation/cause ID
- exact resolved package/function ref where applicable
This helps debugging, replay, migrations, future permissions, and agent-written programs.
Package Runtime Identity
Package code must not self-assert actor identity by sending arbitrary actor
fields or package headers to Camino. Orch owns runtime activation and should mint
short-lived capability tokens for package runtime processes. Package runtime
helpers attach those tokens when calling Camino. Camino verifies the token with a
shared secret or future stronger mechanism and derives package provenance from
the signed claims.
V0 implementation uses CAMINO_RUNTIME_AUTH_SECRET as the shared signing
secret. The dev start scripts for Camino and orch create/read the same local
secret under quixos-instance/.var when the env var is not already set. Orch
mints CAMINO_RUNTIME_AUTH_TOKEN for spawned package processes, and the
TypeScript package runtime helper attaches it to Camino RPCs. If orch has a
secret, spawned package processes also receive CAMINO_RUNTIME_AUTH_REQUIRED=1
so helper startup fails if the token is missing.
V0 token claims are intentionally small:
package_namespace
package_name
descriptor_version
runtime_key
issued_at
The token proves only "this request came through a runtime process spawned for this package descriptor." It is not yet a user permission model and does not solve package sandboxing. A compromised package can still use its own package token, but it cannot claim to be a different package without orch's signing secret.
Camino rejects invalid runtime-token headers. Requests with no runtime token are
still accepted as local-rpc in v0 so CLI/dev tooling can mutate objects before
we have a real user/session auth layer. That means this is runtime provenance
enforcement, not full write authorization.
Later this should grow into:
- invocation/watch correlation IDs
- exact function ref and schema slot provenance
- expiry and rotation
- per-object/field capability scope where useful
- a user/session auth path distinct from package runtime auth
- Camino-side rejection for writes whose caller identity cannot be proven
- audit records that distinguish user intent from package-side effects
Mounted/External State
Some fields may represent external or mounted state:
- filesystem-like state
- Postgres instance
- Minecraft server world
- process-owned runtime data
- large blob/doc store
These should not pretend to be ordinary CRDT fields. They are object fields with special storage/activation semantics.
Possible V0 Protocol
Tiny first protocol, exposed through Connect RPC rather than REST:
createObject(classId, initialFields)
getObject(id)
setField(id, field, value)
addEdge(from, type, to, fields?)
removeEdge(edgeId)
callMethod(id, method, args)
subscribeObject(id)
migrateObject(id, targetSchemaVersion)
If this protocol feels good, the rest has a place to attach.
Plain HTTP should be reserved for health checks and future metrics. The Camino RPC shape should also inform future orchestrator/package RPC cleanup so Quixos does not accumulate several unrelated control protocols.
Running Example: Todo
Keep the fixture small. No users, no permissions, no saved query/list views.
Classes:
TodoApp
root object for one todo program instance
owns projects
stores app-level settings later
Project
collection of tasks
can be archived
owns task ordering
Task
main work item
title, notes, status, due date, priority
can link to tags, comments, subtasks
Comment
discussion/note object attached to a task
no author for now
useful CRDT doc example
Tag
reusable label object
linked to many tasks
Graph:
TodoApp -[HasProject]-> Project
Project -[HasTask]-> Task
Task -[HasSubtask]-> Task
Task -[TaggedWith]-> Tag
Task -[HasComment]-> Comment
Sketch:
message TodoApp {
option (camino.class) = {
version: 1
};
string name = 1 [
(camino.field) = { conflict: PRESERVE_CONFLICTS }
];
repeated Ref projects = 2 [
(camino.edge) = {
name: "HasProject"
to: "Project"
cardinality: MANY_ORDERED
inverse: "apps"
}
];
}
message Project {
option (camino.class) = {
version: 1
};
string name = 1 [
(camino.field) = { conflict: PRESERVE_CONFLICTS }
];
string description = 2 [
(camino.field) = { conflict: PRESERVE_CONFLICTS }
];
bool archived = 3 [
(camino.field) = { conflict: REPLACE }
];
repeated Ref tasks = 4 [
(camino.edge) = {
name: "HasTask"
to: "Task"
cardinality: MANY_ORDERED
inverse: "project"
}
];
}
message Task {
option (camino.class) = {
version: 1
methods: [
{ name: "complete", function: ... },
{ name: "reopen", function: ... },
{ name: "archive", function: ... }
]
};
string title = 1 [
(camino.field) = { conflict: PRESERVE_CONFLICTS }
];
CrdtRichText notes = 2 [
(camino.field) = { conflict: CRDT }
];
TaskStatus status = 3 [
(camino.field) = { conflict: PRESERVE_CONFLICTS }
];
int64 due_at = 4 [
(camino.field) = { conflict: PRESERVE_CONFLICTS }
];
Priority priority = 5 [
(camino.field) = { conflict: REPLACE }
];
repeated Ref tags = 6 [
(camino.edge) = {
name: "TaggedWith"
to: "Tag"
cardinality: MANY_UNIQUE
inverse: "tasks"
}
];
repeated Ref comments = 7 [
(camino.edge) = {
name: "HasComment"
to: "Comment"
cardinality: MANY_ORDERED
inverse: "task"
}
];
repeated Ref subtasks = 8 [
(camino.edge) = {
name: "HasSubtask"
to: "Task"
cardinality: MANY_ORDERED
inverse: "parentTasks"
}
];
}
message Comment {
option (camino.class) = {
version: 1
};
CrdtRichText body = 1 [
(camino.field) = { conflict: CRDT }
];
int64 created_at = 2 [
(camino.field) = { conflict: REPLACE }
];
}
message Tag {
option (camino.class) = {
version: 1
};
string name = 1 [
(camino.field) = { conflict: PRESERVE_CONFLICTS }
];
string color = 2 [
(camino.field) = { conflict: REPLACE }
];
}
This fixture tests:
- root object
- object creation
- ordered edges
- unique edges
- self-referential edges
- CRDT doc fields
- preserve-conflicts scalar fields
- method-backed mutations
- inverse associations
- schema migration later
One-Sentence Version
Camino is a live, schema-aware object graph database where durable instances have typed fields and typed edges, packages provide behavior for class methods and channels, CRDT document primitives are available where they fit, and the whole system is inspectable, subscribable, migratable, and eventually editable through generated React/admin views.