Files
quixos/CAMINO.md
T
2026-07-14 08:04:02 -07:00

45 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:

  • GetField and WatchField dedupe
  • 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.service resolves 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.
  • WatchStart input matches Get input unless an explicit exception is added.
  • Set exists only for writable custom fields.
  • rpc camino.impl refs 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 the same underlying graph concept. A ref-looking field in a class schema is a declaration site for a canonical edge type, not proof that the edge is owned by that class forever.

A simple ref can lower to a named edge with cardinality:

field assignee: Ref<User>

lowers to something like:

edge Assignee {
  from Task.assignee optionalOne
  to User.assigned_tasks many
}

A richer edge can have its own fields:

edge Membership {
  from User.memberships manyUnique
  to Org.members manyUnique
  role: Role
  joinedAt: Time
}

Metadata-free edges and metadata-bearing edges should use the same storage/subscription/indexing model. Schema controls:

  • edge type identity
  • endpoint projection names
  • endpoint class/interface constraints
  • per-endpoint cardinality
  • uniqueness
  • 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. The current implementation supports optional_one, exactly_one, many, many_unique, many_ordered, and many_unique_ordered. It checks cardinality on both endpoints, but only has one stored ordinal, so edges that are ordered on both endpoints are rejected for now.

Inline proto syntax is still preferred over a separate edge-schema file for ergonomics. The inline declaration should lower to a canonical edge type record with two endpoint projections:

camino.Ref for_object = 3 [
  (camino.edge) = {
    id: { namespace: "quixos.todo" name: "TaskReactComponent" version: "1" }
    this_endpoint: {
      interface: {
        namespace: "quixos.react"
        name: "ReactComponent"
        version: "1"
      }
      projection: "for_object"
      cardinality: EXACTLY_ONE
    }
    other_endpoint: {
      class: { namespace: "quixos.todo" name: "Task" version: "1" }
      projection: "admin_components"
      cardinality: MANY_UNIQUE_ORDERED
      indexed: true
    }
  }
];

This lets a schema add an extension projection to another schema without forking that other schema. For example, TodoReactComponent can define for_object, while Task can still show an admin_components projection in the admin UI by registry discovery.

Future edge work:

  • a first-class registry/query API instead of clients scanning all class schemas
  • anonymous or autogenerated edge IDs that reconcile to stable canonical edge identities
  • per-endpoint ordinal storage if ordered:ordered edges become necessary
  • edge interfaces/tags for semantic discovery, such as admin panel component edges, without baking every projection name into clients

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.