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.

Why it matters

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.

Honest current state, as of 2026-09-13: nobody has been issued a token yet. 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.

KEY CAVEAT: the ingestion repos (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.
Why it matters

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:

SignalScoreScope
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×highSevdeveloper
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).

Why it matters

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).

A recent fix: the endpoint returns via minimal-API Web defaults (camelCase), but a hand-rolled serializer once emitted PascalCase, so admin.html (reading 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.
Why it matters

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".

Why it matters

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.

Why it matters

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.

Why it matters

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.

Why it matters

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).

Why it matters

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.

Why it matters

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.

Per-developer git attribution is only as good as the checkouts it reads: because the deploy strips .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