Memory & intelligence
Team & developer intelligence
A team platform layered on the index: it turns signals the ledger already computes — the
quality rubric, recurring finding classes, retrieval misses, session outcomes, per-call telemetry — into team-
and per-developer insight. One principle runs through all of it: attribution is free until identity
tokens are issued. Everything defaults to a single "shared" bucket and degrades
gracefully, so the platform is purely additive opt-in.
Per-developer identity — the MCP token registry
Each developer gets their own issue-once bearer token, so every MCP call and session attributes to a real
person and team instead of one shared identity. AgentIdentity is a
(DeveloperId, Team) struct with default
AgentIdentity.Shared = ("shared","shared") (Services/TokenRegistry.cs:20-24).
TokenRegistry maps token → {developerId, team, displayName, issuedUtc, revoked}, backed by JSON
dev-tokens.json under Tokens:Dir so it survives restarts and the deploy overlay-rsync
(:54-71). Security is issue-once: the raw token is a 256-bit URL-safe random string
(NewRawToken :173-179), only its SHA-256 hex hash is persisted (HashToken :74-78),
Issue returns the raw value exactly once (:85-111), and Resolve is O(1)
by hash and never holds the raw token. At the edge, POST /mcp extracts the bearer and succeeds when
it equals the shared token (Mcp:BearerToken) OR resolves to a registered, non-revoked token — a
registered token always wins over the shared token; else 401, shared compared in constant time
(Program.cs:852-894). Admin: POST /api/admin/tokens issues and returns raw once
(:1169-1206), GET /api/admin/tokens lists hashes+metadata never raw (:1209),
POST /api/admin/tokens/revoke (:1226) — all loopback/tailnet-gated.
It's the foundation the whole platform reports through: zero tokens is byte-identical to the old single-bearer world, and each token lights up real per-developer telemetry, durability, and learning-needs — with no credential-leak risk, since tokens are hashed at rest and shown once.
TokenRegistry and POST /api/admin/tokens exist and are wired end-to-end, but nothing
in the deployed system calls Issue — so every query row, audit record and
team-metrics attribution today reads developer: "shared", team: "shared". Everything
on this page describing what happens once tokens are issued is describing a real, tested mechanism
that is switched on the moment an operator issues the first token — not a claim about the present. Until
then, git-authorship attribution (below) is the only per-developer signal that actually varies.Git-authorship attribution — the fallback identity signal
Before tokens exist, every indexed file is mapped to its primary git author so team analysis can still
attribute quality gaps and findings. DeveloperAttribution reads git log from local
checkouts under LearningNeeds:GitRepositories (falling back to Git:Repositories, then
Scanning:Repositories) (Services/DeveloperAttribution.cs:56-90):
git log --no-merges --format=@@A:%an --name-only -n 4000 tallies commits per (repoFolder, relPath)
→ author. The per-file author is the most frequent human committer; PickAuthor excludes automation
identities unless a file was only ever bot-touched (:103-117). Repo-composite keying
("<repoFolder>\x1F<relPath>") stops the same relPath in two repos from being conflated
(:56-62,84). It is best-effort, cached with a 30-minute TTL, and returns "unknown" when
unresolvable.
sources/current/*) are STRIPPED of
.git by the deploy, so they resolve "unknown". To light up real per-developer data, point
LearningNeeds:GitRepositories at full clones with .git history.It is the only per-developer signal available until tokens are issued — the bootstrap that lets learning- needs and durability analysis produce attributed output from day one.
Team- & per-developer learning-needs — the flagship (learning_needs)
Turns signals the ledger already computes into a ranked, evidence-backed set of "where should we get
stronger" recommendations — per developer and per team — and tracks whether interventions moved the number.
LearningNeedsService.AnalyzeAsync(repo, developer)
(Services/LearningNeedsService.cs:60-174) fuses four independent signals, each with its own pure,
unit-tested scoring:
| Signal | Score | Scope |
|---|---|---|
Quality-rubric gaps — per-chunk q_* aggregated by (developer, repo, layer); a
dimension below LearningNeeds:RubricTarget (default 2.7) over enough chunks is a gap. |
max(0, target−mean)×log10(samples+1) — deficit damped by sample size. |
developer (= git author) |
| Finding-pattern gaps — a defect class recurring for a developer in a repo; dismissed findings excluded. | count + 2×highSev | developer |
| Query-miss gaps — recurring failed/low-hit query clusters. | count |
team only (not attributable to one person) |
| Revert gaps — reverted/abandoned sessions, attributed to real identity if tokens issued else the dominant git author. | reverts×3 |
the exact point where issuing tokens sharpens attribution (IsRealIdentity :182-184) |
All gaps are ranked by score and capped at MaxNeeds (default 12), optionally filtered to one
developer. Each recommendation gets a concise LLM-synthesised rewrite via Groq, budget-guarded (checks the cap
before every call, records spend) — aggregation is $0, only synthesis costs cents. For trend,
LearningNeedsStore saves each run as a timestamped JSON snapshot and computes a pure diff between
the two most recent runs, classifying each gap as New/Resolved/Improved/Worsened
(Services/LearningNeedsStore.cs:36-92), keeping ~26 snapshots (~6 months weekly).
LearningNeedsHostedService runs whole-team analysis weekly (IntervalHours default
168). It surfaces via the MCP tool learning_needs (McpService.cs:3840-3894) and the
public GET /api/learning-needs (Program.cs:602-621).
It converts passive quality/coverage telemetry into an actionable upskilling plan attributed to the right people, with a measurable before/after loop — and it costs almost nothing to run continuously.
Team-metrics tile — adoption, durability, efficiency
TeamMetricsService.Rollup(...) is a PURE aggregation over in-memory
AgentQueryRecord + SessionOutcome lists (deterministic, no I/O, unit-testable)
(Services/TeamMetricsService.cs:148-242); GetRollupAsync does only the Qdrant scroll
then delegates. Adoption: groups queries by Team→Developer, computing calls, cache-served,
cache-hit-rate, and median top-hit score per dev and per team; ActiveDevelopers counts distinct
non-"shared" devs, and AllShared flags when no per-dev tokens exist yet.
Durability: global counts of merged/reverted/abandoned/unknown, with
RevertedRate = reverted/(merged+reverted+abandoned) — "unknown" excluded from the denominator.
Efficiency: the read-only global LEDGER÷DIRECT token ratio from bench_*, null when nothing is
benchmarked. It is STRICTLY read-only; legacy points without identity default to "shared". Endpoint
GET /api/admin/metrics/team?days=30, loopback-gated (Program.cs:1777).
d.allShared,
t.developers, dvp.cacheHitRate) saw every field undefined and showed "0/shared only".
Metrics that would need git-provenance or runtime-log fusion (per-developer durability, chunk-level incident
rate) are deliberately absent with a DeferredNote rather than faked.It's a manager's single read-only view of who uses the brain, how well it serves them, whether AI changes stick, and system-wide token savings — without inventing numbers to fill gaps.
Cohort clustering — families by design vector (cluster_cohort / cohort_count)
Given a facet (repo/layer/pattern/path/concept), reveal the natural families inside that sub-population by
clustering over the language-neutral design (intent) vector — "which sprocs are similar in nature?" without an
anchor. CohortFilter.Build translates a CohortFacet into a Qdrant filter, with
PathContains applied client-side (Cohorts/CohortFilter.cs:39-64).
CohortClusterService.ClusterAsync scrolls members with vectors, path-filters, then
collapses chunk-level points to one representative per file so families are counted in files
not chunks (Cohorts/CohortClusterService.cs:59-107). CohortAggregator.ByFile
element-wise averages vectors across a file's chunks, unions pattern signatures, and EXCLUDES non-source assets
(ONNX/PDF/tokenizer blobs under /models/) that would be spurious outliers
(Cohorts/CohortAggregator.cs:19-79). k is auto-chosen Clamp(n/8, 2, 12)
unless overridden; k-means over design vectors is seeded and deterministic; each cluster reports size, an
exemplar (closest to centroid), sample paths, and repos spanned. CountAsync groups the
file-collapsed population by repo/layer/pattern for "how many X by Y".
An architect sees the shape of a slice — how many implementation families a concept fractured into and which repos each spans — without having to pick a seed file first.
Cohort consistency — the odd-one-out (cohort_consistency)
Within a cohort that should be implemented alike, flag the outliers.
CohortConsistencyService.AnalyzeAsync
(Cohorts/Consistency/CohortConsistencyService.cs:50-160) selects and file-collapses the cohort,
guards for a minimum size of 4 (false-positive guard), picks the most-central member as the exemplar ("how we
normally do it"), runs the requested lenses, dedups one finding per (file, lens), and produces a best-effort
Groq summary (with a deterministic fallback). Four lenses, each ranked by deviation:
- DesignLens — cross-language design divergence: a member whose design/intent vector points away from the centroid realises the concept differently — architectural mis-alignment, not a style nit; evidence includes the cohort's language mix.
- CodeLens — structural / code-vector divergence.
- NamingLens — naming divergence.
- SignatureLens — divergence over pattern-signature flags (e.g.
cs_configureawait_false,sql_capture_error).
All use an adaptive outlier threshold mean(distances) + 1.5·stdev(distances)
(VectorMath.OutlierThreshold) so a loosely-clustered cohort yields few or no findings.
It's automated architectural review — surfacing the odd sibling a human catches only by reading every one. The design lens catches conceptual drift across languages (a C# service and its TS twin solving the same problem differently), and the adaptive threshold plus LLM summary flag likely-intentional exceptions so reviewers aren't drowned.
The learning loop — retrieval signals to ranked actions
Reads negative feedback and failed-query clusters and turns them into ranked, human-reviewable "learning
actions" — closing a gap where useful=false was written but never read.
LearningLoopService.RunAsync reads failed clusters + raw negative feedback + pending (unapproved)
synonym suggestions and builds actions read-only end to end
(Services/LearningLoopService.cs:46-75). BuildActions is pure and deterministic: each
cluster → one action weighted clusterSize + 2×negHits (a thumbs-down outranks a merely low-scoring
query); if a parked synonym suggestion matches the cluster exemplar the action is a
SynonymSuggestion (carrying the id for one-click approve), else a RecurringMiss
awaiting triage; unclustered thumbs-downs become standalone actions so none is dropped. It is PROPOSE-ONLY by
contract: it never mutates the index, synonyms, or retrieval, routing approvals through the existing
POST /api/admin/synonyms/approve. Endpoint Program.cs:1148.
It converts the exhaust of everyday retrieval into a prioritised backlog of concrete fixes, with a human always in the approval loop — so the brain gets better at the questions it actually keeps failing.
Enforcement — risk-tiered policy-as-code (policy_check)
Classifies every MCP tool into a risk tier and returns allow | review | deny, the way an MCP
gateway would, so a hook can consult it before a risky action runs. PolicyService is pure with
three tiers (Services/PolicyService.cs:30-88): ReadOnly → allow freely (every
discovery tool; unknown tools default here); Write → allow but flagged/audited (feedback,
mark_visited, bench_record, fix_mark_status); HighRisk → review (sync_ledger + configured).
The tier map is config-overridable — Policy:WriteTools/HighRiskTools are UNIONed onto
defaults and Policy:DeniedTools hard-denies. Evaluate returns a
PolicyVerdict with canonical lower-case wire strings
(readonly|write|high_risk, allow|deny|review) so the JSON contract is stable.
Proportional control — reads stay frictionless, writes are cheap and audited, and only risky or irreversible operations demand a human — with governance centralised, testable, and operator-tunable without a redeploy.
Audit — the who-ran-what trail
One audit record per MCP tool dispatch. AuditLog.RecordAsync writes one
AuditRecord into the Audit Qdrant collection as a ZERO-VECTOR point (payload-filter
lookups, not ANN) (Services/AuditLog.cs:90-147). For privacy, raw arguments are never stored —
only ComputeArgsDigest, a truncated 64-bit SHA-256 hex of the args JSON, so the trail proves a call
happened and correlates identical calls without leaking values. The payload is ts, developer, team, tool, tier,
ok, args_digest, error. It is best-effort (swallows exceptions, degrades if Qdrant is down, disabled via
Audit:Enabled=false) and wired fire-and-forget after the tool runs, stamped with the resolved
identity and tier (McpService.cs:1304-1332).
It aligns with MCP-gateway / EU-AI-Act patterns — agents inherit human controls plus an immutable audit — and the digest-not-args design keeps the trail leak-proof while still correlating repeat calls.
Identity flow & OTel telemetry
Identity flow: POST /mcp resolves the bearer to an AgentIdentity
and passes it to DispatchAsync(rawJson, identity) (Program.cs:863-886);
McpService stores it in an AsyncLocal<AgentIdentity> so concurrent calls don't
bleed identity (:32-42); and a single choke-point LogQuery stamps
rec.Developer/rec.Team onto every AgentQueryRecord (:162-170)
— filling the keys TeamMetrics and audit read. OTel: GenAiTelemetry defines one
ActivitySource (motledger.mcp) and emits a span per tool dispatch tagged with GenAI
semantic conventions (gen_ai.operation.name, gen_ai.tool.name,
gen_ai.system) plus caller identity (enduser.id, enduser.team) and outcome
(duration_ms, error.type) (Telemetry/GenAiTelemetry.cs:20-86). It is
default-off and zero-overhead — with no listener (unless Otel:Enabled),
StartToolSpan returns null and the hot path is byte-identical.
Identity resolved once at the edge propagates everywhere for free, and standard GenAI-semconv spans are portable to any OTLP backend (Grafana/Tempo/Honeycomb) with per-developer/team attribution built in.
How git authorship flows into attribution
An indexed chunk path → DeveloperAttribution.SplitRepo recovers
(motmaster-<repo>, relPath) → looked up in the commit-tally map from local
git log → primary human author. LearningNeedsService calls
_attrib.DeveloperFor(filePath) to attribute quality gaps (per file's author), findings (per
finding's file author), and reverts (dominant author of edited files). Once tokens are issued, session signals
prefer the real runtime identity over the git-author fallback, and query/audit/metrics key on the
token-resolved developer/team. Git authorship is the bootstrap; tokens are the upgrade.
.git from sources/current/*, LearningNeeds:GitRepositories
must point at full clones for real author data, else "unknown". Per-developer durability/incident metrics
remain deliberately deferred (not faked) until session→identity fusion lands.Open Brain — the MOTMaster Architectural Ledger · generated from source 2026-07-07