Skip to Content
ArchitectureMycelium mesh nodesBuilding and committing the twin
Architecture · MeshMeshA set of connected Mycelium services that exchange updates and keep the NovusLens log.

Building and committing the twin, as an ordered log.

What it is for

Writers record the organisation’s structural changes in one ordered stream. The read modelRead modelThe database tables used by the map, Ask and Find. NovusLens rebuilds these tables from its history of recorded changes. applies those events to the database. CellsCellA box on the map that stands for a team, service, supplier or AI agent. It is a unit you can inspect, not necessarily a department or a person., dependenciesDependencyA link where one unit needs something from another. A dependency is not a reporting line: it shows where work relies on something else., owners, stated authorityAuthorityWhat a team or agent is allowed to decide or do. For an agent, this comes from a named person or system that grants permission., complexity, agility and recoverability (C/A/RC/A/RThree separate scores for a team or service: Complexity, Agility and Recoverability. Each uses current evidence and the customer’s stated scoring rules.) scores, findingsFindingA difference in the records that needs a decision. It includes its sources and can describe a slow handoff, disputed fact or missing owner. and requests for evidence all arrive there as events. The mapMapThe view of teams, services and their dependencies for the selected part of the organisation. Details appear beside the map., the C/A/R figures and the findings a person sees are the result of reading those events in order. This page describes what goes onto the stream, how the build agent decides what to put there, and the scripts that seed the demonstration organisations.

Three kinds of writer append to one stream per organisation. This figure groups twenty event kinds into four sets for explanation. The code has no such grouping. The projection worker reads events in order.

How it works

The commit stream

Each organisation has one stream, conway/commit/{org}. The module commits.py takes the stream name from commit_stream in keys.py. Writers append to it through a TwinWriter (defined in twin.py, which this page does not cover). Each append returns a hybrid logical clockHybrid logical clockA number attached to a log entry to order it against other entries. It combines clock time with a counter, so entries can be ordered even when clocks do not agree exactly. (HLC) stamp, which write_demo reports as last_hlc. The projectionProjectionThe regular pass that brings the read model up to date with the canonical log. It runs every 30 seconds by default. side reads the stream in order with scan_log(from_hlc), as described in The mesh nodes.

The stream is lean by design. Events carry attributes and pointers. Evidence bodies stay in PostgresPostgresThe database software that stores the tables read by the NovusLens screens, along with document bodies, access records and protected personnel records.: provenance_upgrade documents that the artifactArtifactA stored source file or evidence body that a record can cite. Keeping the exact content lets a reader check what supported the record. body lives in evidence_artifacts and the commit carries only its identifier.

The event envelope

Every event is one compact JSONJSONJavaScript Object Notation: a text format for data, using named fields, lists and values. Configuration files and service messages use it. object. Two keys come first: v, the schemaSchemaThe declared structure of data: which fields or tables exist and which values they accept. version, and kind, the event kind. The body fields follow at the top level. SCHEMA_VERSION is 1. decode raises ValueError for any other v, so an event from a different schema is refused rather than read. Because kind belongs to the envelopeEnvelopeThe surrounding fields that identify a message, its sender, its type and its time, alongside the message's data., the cell’s own kind is written as cell_kind inside a cell.upsert body.

Event kinds

commits.py defines twenty event kinds. The table lists each constant, the helper that builds it and what it records.

KindHelperWhat it records
cell.upsertcell_upsertA cell’s label, parent, tier, kind, provider, region, owners, granted authority, responsibilityResponsibilityThe work or result that a team or agent answers for. NovusLens records the person answerable for it., C/A/R scores and provenanceProvenanceThe record of where a value came from, who stated it and when. Every value keeps this record. per attribute.
cell.carcell_carOne layer’s C/A/R scores for a cell, with a provenance block built from a raw source string.
car.axis.observationcar_axis_observationOne independently sourced C/A/R axis value. Its field set is fixed and must match exactly.
car.assessment.evidencecar_assessment_evidenceA reference to a stored set of C/A/R evidence records whose contents cannot change.
agent.evidence.batchagent_evidence_batchA reference from a batch identifier to an artifact identifier.
network.flow.observationnetwork_flow_observationAn aggregate of network flows from a flow-log export. It records what the network saw, never a gatewayGatewayA service that accepts requests and passes them into another system. The Mycelium gateway gives NovusLens access to the mesh.’s decision.
cell.lineagecell_lineageA split, merge or rename of a cell. The cell’s identity survives.
cell.aliascell_aliasA name the organisation uses for a cell, with its source.
domain.upsertdomain_upsertA declared business domainDomainA business area defined by the work it does, such as purchasing or customs. It may involve several teams, and one team may serve several areas., as a claim with its source recorded, owned by the business.
cell.domaincell_domain_mapA cell serving a domain. Many to many.
edge.upsertedge_upsertA dependency between two cells, with its handoffHandoffWork or information passed from one team or service to another. Its measured or stated completion time determines the dependency class., layer, dispute status and provenance.
finding.openfinding_openA finding with a stable identifier, a kind, a subject and detail. Re-committing the same identifier updates it.
finding.updatefinding_updateA change to a finding’s status, owner or response note.
provenance.upgradeprovenance_upgradeAn attribute climbs a rungRungOne of three levels of support for a recorded value: told, shown or measured. The level appears beside the value. on the evidence ladderEvidence ladderThe three levels of support a record can have, in order: told, shown and measured., citing an artifact.
provenance.disputeprovenance_disputeThe disputed marker on a value is set or cleared. It sits on the log so a rebuilt read model keeps every challenge.
artifact.requestartifact_requestA named ask for the next rung of evidence, raised for a cell or organisation-wide.
artifact.fulfilartifact_fulfilA stored artifact satisfies a request. A None artifact identifier marks a sealed fulfilment.
artifact.purgeartifact_purgeA document’s body is removed. The retained removal record (tombstoneTombstoneA retained record that says content was removed, when it was removed and why. The removed content itself is no longer available.) and this event keep who purgedPurgeRemoving a stored document’s contents while keeping a record that it was removed and why. Conclusions that used it must be checked again. it and why. Citing attributes are downgraded.
cell.financecell_financeA finance classification of a cell, either cost_centre or profit_centre.
org.metaorg_metaThe organisation’s display name, description and home region.

The fencing token

Events that describe observed measurements can carry a fencing_token. The helpers add the field only when the caller passes it. car_axis_observation and network_flow_observation require it in their fixed field sets. cell_car and edge_upsert take it as an optional argument. An observed-layer score or edge carries its correction pass’s token. This distinguishes an older pass from a newer one. The check itself happens at the sink, the materialiserMaterialiserThe component that reads recorded changes in order and updates the database. It records its place so it can continue after a restart. in apps/graph, which is outside these modules. The mesh nodes page describes that check.

edge_upsert also takes observed_at_ms and valid_until_ms. Together they give the window in which an observation counts as the current picture. The docstring says the engine excludes expired observations; that code is not in these modules.

Requests for source documents

SOLICITABLE_ARTIFACTS is a fixed set of ten names: aws-organizations, gcp-resources, azure-resources, terraform-state, cmdb-export, hris-export, dc-docs, ops-report, agent-facts and domain-taxonomy. Each names a document about the organisation’s systems, people or agents. The interview asks for these artifacts, and connectorsConnectorSoftware that reads records from a source system, such as an incident tool. A customer-written file connects those records to teams or services in NovusLens. ingestIngestRead supplied records or documents and add their information to NovusLens, after the required checks. them into infrastructure cells beneath their business domain. A source is never a business domain.

BuildAgent._apply checks the set before it acts. If add_cell or an add_dependency target names one of these sources, _solicit turns it into a document request. Nothing enters the log as a cell. The comment on the guard says the materialiser checks the same set, so that logs written before the guard are also cleaned. That check is outside these modules.

From interview to commits

The build agent runs in four steps.

  1. Read the transcript. build.py loads a JSON transcript with an org, a list of turns (speaker and utterance pairs), and optionally org_name and description.
  2. Extract proposed changes for each turn. BuildAgent.take_turn builds a prompt from graph_state(), the current graphGraphA model made of units and links between them. In NovusLens it records teams, services and their dependencies. as text, with the utterance. If the transcript is answering a question, take_turn accepts a context string, which is included in the prompt. The prompt goes to chat, which posts to the model endpointEndpointA particular address on a service that accepts a defined kind of request. at model temperatureModel temperatureA setting that controls how much a language model varies its choice of words. Lower values usually produce less varied answers. 0.1. parse_json pulls the first JSON object out of the reply. A reply must contain a string paraphrase and an explicit list of proposed changes, each an object. Missing fields or malformed changes are refused before the graph changes. An empty list is valid for a nonempty small-talk paraphrase. A reply with no valid object, or with neither a nonempty paraphrase nor a proposed change, is unusable. The agent then asks again, saying what was wrong, up to TURN_ATTEMPTS (3) calls in all. If every reply is unusable, the turn returns (unparseable) with no proposed changes before any graph mutation, including the explicit supplier rule. Warnings name the speaker, attempt and reason. The shared chat helper accepts an optional caller schema only for OpenRouter JSON requests. The group router supplies its static output schema; build calls without one keep their JSON-object request, and local calls are unchanged. Schema support does not establish semantic correctness or guarantee provider compatibility. Completion diagnostics use the response check supplied by each caller. The build agent checks its required fields and a nonempty paraphrase or proposed changes. A valid routing object is not judged as a build answer. Every JSON completion records bounded model, finish reason, token counts and whether a refusal was present; it omits reply text. Accepted response shapes use INFO, while invalid shapes use WARNING. Build-only diagnostics also report a fixed operations-state label and bounded count, without reply text. An accepted shape does not establish that all statements were captured. For this JSON extraction call, OpenRouter requests JSON output and preserves the requested reasoning effort: low for fresh capture and medium for an existing twinTwinNovusLens’s model of one organisation, built from records about its teams, services and how they rely on one another. It is only as current as those records.. It routes only to providers that support the requested parameters. Before the first such request for a model, chat reads the model’s OpenRouter provider list once per process. It keeps temperature 0.1 only when one provider accepts temperature with every other requested parameter; under a CIContinuous integrationAutomated checks run when code changes, usually including building the code and running tests. profile, only the pinnedPinA recorded choice of one exact dependency version. It prevents an update from changing that dependency without a deliberate decision. provider counts. Otherwise it omits temperature and logs that it did. A model that OpenRouter does not list, or that no provider serves, stops the call with the model id, the listing URL and the repair. Its default completion limit is 8,192 tokens, including reasoning and the answer. An explicit caller limit takes priority; local models and other chat calls keep their 2,048-token default. The larger limit does not guarantee a usable answer. A provider that refuses this setting still fails the call; it does not trigger a different model or a relaxed interpretation. When OpenRouter finds no provider for the requested parameters, the HTTPHTTPHypertext Transfer Protocol: the rules a browser or program uses to send a request to a web service and receive its response. 404 error carries a note naming the model, the parameters, its providers page and the setting to change. HTTP refusals report their status and fixed parameter, reason and full-phrase markers from a bounded JSON error. They may include an allowlisted canonical error type, whether a provider code was present and a bounded numeric code. Unknown values use fixed markers. They omit arbitrary provider text, request content and credentials; these observations do not establish a cause. Each proposed change is then passed to _apply. A separate rule requires exactly one supplier named by “is supplied by X under contract”. The utterance must also say “they are a vendor” or “they’re a vendor”. The code then applies set_external without needing the model to propose that change.
  3. Apply the proposed changes to the graph held in memory. _apply handles add_cell, add_dependency, set_external, set_authority, set_car, note, flag, contest and solicit. The rules are visible in the code:
    • A dependency is recorded as a fact. Its couplingCouplingA link where one team or service depends on another. Its Loose, Buffered or Rigid class describes the time needed for a handoff. class is never inferred, so captured edges carry Handoff.UNKNOWN and are excluded from the critical pathCritical pathThe dependency chain with the greatest total coordination weight in the map. It identifies a structural constraint. It does not state elapsed time. arithmetic.
    • An owner stated as unknown is not a gap. Only a stated absence of an owner becomes an unowned flag.
    • A C/A/R score is kept only when the interviewee states a number. Descriptions become note entries.
    • A cell that matches an existing one by name is folded in as an alias. canon_match returns EXACT, SUBSET, NEAR or NEW. A NEAR match, which shares a meaningful token but neither name contains the other, is not merged. It is recorded as a possible alias for a person to decide.
    • A statement that contradicts the current graph becomes a recorded dispute, named contest in the code. The graph is not edited.
  4. Confirm and commit. build.py prints the paraphrases, the graph state, the findings about missing owners and the requests for source documents. It commits only when the person answers y, unless --yes is passed. BuildAgent.commit_to processes cells one at a time. For each cell it writes any update, then any external-status question, then any stated C/A/R estimate. New edges follow all the cells. Findings follow: unowned, told-observation, contested-structure and possible-alias. Alias facts and artifact requests come last. Finding identifiers are built from the organisation and subject, for example unowned-{org}-{cell}, contested-{org}-{cell}-structure and req-{org}-{artifact}, so repeating the commit does not duplicate them.
The build agent reads one turn at a time, holds the graph in memory, then commits after a person confirms.

The build agent’s default model is gpt-oss:20b at a local Ollama-compatible endpoint. Both can be changed with the environment variablesEnvironment variableA named setting made available to a running program. The customer launcher reads these settings from the installation environment file. listed under Interfaces.

Two rules carry through the code. An interview fills empty slots on a cell that already exists from a document. It never overwrites a documented value. A stated C/A/R estimate lands on the asserted layer at the told rung, and it does not overwrite an existing documented or measured score. Both rules are visible in _annotate, _contest and _land_estimate.

Seeding the demonstration walk

seed_walk.py puts the two multi-organisation surfaces into a browsable state. It is run for the operator walk and not by the acceptance gates. Its docstring says ./lens ops --version dev walk runs it, and it leaves the data in place. It has four entry points.

  • seed_agent_estate builds three self-signed agent documents for the meridian organisation. One is over-delegated, one has a named accountable ownerAccountable ownerThe person recorded as answerable for a team, service or agent. NovusLens does not assume that this person is also the one who runs it day to day., and one is tampered with after signing. The three are ingested with run_agent_ingest. Boundary telemetryBoundary feedRecords supplied by the customer’s own systems about activity at the edge of an agent or service. NovusLens reads those records; it does not watch the agent. is then written to governance.analyse, marked synthetic: True. The result is labelled as illustrative in the code.
  • seed_group builds a group of two regional twinsTwinNovusLens’s model of one organisation, built from records about its teams, services and how they rely on one another. It is only as current as those records., meridian-eu and meridian-us, and admits one signed federationFederationSharing selected signed facts and summaries between separate regional twins, while their detailed records stay in their own regions. fact between them. It then submits a tampered copy of the same fact and exits with an error if that copy is admitted. Several rows are written straight to Postgres here: the organisation’s display name and region, its fidelity countsTwin fidelityCounts of the model's sourced facts at the told, shown and measured evidence levels. It describes the available support, not whether the claims are true., its critical path lengthCPLCritical path length. The total coordination weight of the longest chain of dependencies in the model. NovusLens reports it as index points, not elapsed time. (CPL) and its cells. The org.meta event exists to avoid this kind of direct write for the organisation’s metadata, so this path is an exception to that rule.
  • seed_key_person_case extends the measured critical path with a single-owner capabilityCapabilityWhat a team can provide or an agent can do. NovusLens records the claim and the evidence that supports it. called Settlement. It writes the cell and a rigid dependency through the log, then writes a synthetic telemetryTelemetryMeasurements and activity records sent by a running system, such as ticket timings, network traffic or workflow results. record to the key-value storeKey-value storeStorage where a named key points to a value. Reading that key retrieves the value currently stored under it. with keys.telemetry_edge. It runs run_correction, recomputes the critical path, and inserts one row into sealed_identity_join. The person and handle are stored as saltedSaltExtra data mixed into a value before its fingerprint is calculated. It makes matching or guessing the original value harder. hashesHashA fixed-length fingerprint calculated from content. Comparing fingerprints helps detect changes and match records., using a development salt constant in the module. The key-person finding then counts one owner and never shows the name.
  • seed_reference_couplings reads apps/mesh/fixtures/meridian_reference_couplings.json. It adds any missing cells from that fixtureFixtureA prepared set of records used for a test or demonstration. A fixture is sample data, not a live customer's records. to the log, then commits each coupling as an asserted edge with the fixture’s source. It runs with --reference-couplings.

The main function, run with no flags, connects to the database and calls the first three seeds in order. Like build.py, it reads the gateway from NOVUSLENS_MESH_HTTP_ADDR and NOVUSLENS_MESH_HTTP_BASE, with 127.0.0.1 and port 8400 as the defaults.

The demonstration organisation

demo_org.py holds the canonical Retail Lending demonstration organisation as data. Its slug is retail-lending. It is a demonstration, not a customer. It has six cells and seven asserted edges. The module records the exact results its checks expect:

  • an asserted critical path length (CPL) of 3.0 index pointsIndex pointsThe unit of the dependency-chain index. Index points are structural weights, not hours or money, and every answer and what-if reports the index in them., running through retail-lending and risk;
  • an observed critical path length of 4.5 index points, running through retail-lending, datagov, platform and northcloud.

The module also holds the lead timesLead timeThe elapsed time between starting a request or handoff and completing it. in days for five capabilities and the C/A/R scores for four cells. Those are the inputs for the telemetry work. The comments say any other result from this data is a break from the canonical picture.

Writing the demonstration to a live mesh

write_demo.py writes demo_org to a running mesh. It is run as ./lens exec -- python -m conway_mesh.write_demo. It opens a TwinWriter, writes cells with write_cell and stated edges with write_edge. It prints counts, stream name and the last HLC. Only the asserted layer is written. The observed correction is a separate loop.

Interfaces

Entry pointReadsWrites
./lens exec -- python -m conway_mesh.build --transcript /tmp/interview.jsonA transcript JSON file and the model endpoint.The mesh stream for --org, through the gateway. It also seeds an actor_map row for the cxo-{org} viewer when the read-model database is reachable.
./lens exec -- python -m conway_mesh.seed_walkThe read-model database and the fixture files.Postgres rows and the mesh stream for the demonstration organisations, on the configured gateway (mesh-0:8400 in Compose).
./lens exec -- python -m conway_mesh.write_demoNothing except demo_org.The mesh stream conway/commit/retail-lending, on --host and --port.

Before the interview command, upload its transcript. The --transcript flag is not uploaded automatically:

./lens exec -- sh -c 'cat > /tmp/interview.json' < interview.json

Options and environment variables used by build.py and build_agent.py:

NameDefaultUsed by
--transcriptrequiredbuild.py
--orgthe transcript’s orgbuild.py
--hostNOVUSLENS_MESH_HTTP_ADDR, or 127.0.0.1build.py
--portNOVUSLENS_MESH_HTTP_BASE, or 8400build.py
--yesoffbuild.py, to commit without asking
CONWAY_LLM_ENDPOINThttp://localhost:11434/v1/chat/completionsbuild_agent.py
CONWAY_BUILD_MODELfalls back to CONWAY_LLM_MODEL, then gpt-oss:20bbuild_agent.py

Invariants and failure behaviour

  • Decoding refuses any event whose v is not 1.
  • A name in SOLICITABLE_ARTIFACTS never becomes a cell from the interview. It becomes a request for a source document.
  • The interview never overwrites a documented or measured value. A conflict becomes a recorded dispute, named contest in the code, and the documented picture stands until a person resolves it.
  • An edge already in the twin is not written again by a later capture.
  • A cell named only as a dependency is committed with its external status unstated, and an external-status-unknown finding asks a person to decide. The tool does not pick a default.
  • A turn with no usable reply after TURN_ATTEMPTS calls is marked (unparseable). The capture conversation asks for clarification. The conway_mesh.build command line refuses it: it names the turn and the speaker, exits 1 and commits nothing. It also refuses an interview with fewer dependency edges than the transcript’s expected_edges. An error from the model endpoint is not caught in take_turn, so it stops the run.
  • Nothing is written until a person confirms, unless --yes is passed.
  • commit_to writes one event at a time. If a write fails partway through, the events already written stay on the stream. The code does not roll them back. How TwinWriter reports a failed write is in twin.py, which this page does not cover.
  • The actor_map seed in build.py is best effort. If it fails, the commit stands and the command prints a note asking for the viewer to be seeded before the twin is opened.
  • The walk’s federation check fails the run if a tampered fact is admitted.

For group questions about map evidence, the router may interpret ordinary descriptions of what is known or contrasts between evidence sources as one published evidence-rung count. It must name that interpretation. This does not compute a ratio or establish knowledge, quality or operational ability. The renderer handles missing records separately. The unchanged live matrix, rather than prompt wording or transport mocks, establishes whether the model selects the expected fields.

Business-domain counts describe the breadth of mapped business coverage. Evidence-rung counts describe recorded support for the map’s statements. Neither measures actual knowledge or quality. The routing dictionary distinguishes these quantities without changing the public ability refusal or the live matrix’s expected choices.

An optional paid manual-CI profile can fix a named remote model/provider and maximum per-unit prices for verification. It is off by default and requires separate payment permission before use. Shared request enforcement also covers preflight; free-only mode never permits a paid exception. This does not change local defaults, response schemas, reasoning effort, token budgets, retry limits or the live acceptance assertions. A named route does not pin service weights or guarantee an answer.

Named manual inference and account declaration

The default CI route remains openrouter/free. The xing4-phala-zero manual profile pins the named Xing model to Phala. It requires parameter support, disables provider fallback and caps prompt, completion, request and image provider prices at zero. It extends CONWAY_LLM_FREE_ONLY=1 only for that exact reviewed profile; arbitrary free suffixes and paid alternatives are refused.

Both named profiles require account-plugins-off: true on an explicit manual version=dev dispatch. This operator declaration means the CI key’s account plugins are off and cannot be forced on by account policy. It is not an automatic account check and must be reassessed if account settings change. Missing or false confirmation refuses before an ops command; the shared HTTP guard also requires CONWAY_LLM_ACCOUNT_PLUGINS_OFF=1. OpenRouter documents account plugin defaults and forced overrides . Provider price caps do not cap account add-on fees or total spend. Paid-profile permission remains a separate decision.

The inference-compatibility component accepts only the confirmed named-zero profile. It makes one real public BuildAgent.chat request with the existing static group schema, low effort and 8,192-token limit. It makes no inferenceInferenceThe work a language model does to produce an answer from its instructions and input. preflight call, formatting nudge or retry. Its shape result is protocol evidence, not model semantic qualification or an application-gate pass. The ordinary live routing matrix and all twelve application gates remain required.

Last updated