Skip to content

DGM: score metadata completeness fixes for fleet promotion #168

Description

@justrach

Summary

The DGM persona-evolution pipeline is architecturally sound and running end-to-end, but several metadata gaps prevent most scores from forming promotable fitness cells. This issue documents each gap, why it matters, and the specific fix needed — all within this repository.


Gap 1: scoreVariants drops the niche (workflow tournament scores are uncelled)

Location

src/subagent.zig line 261 — the prov string sent with scoreEvent:

const prov = std.fmt.bufPrint(&provbuf, "{s}\t{s}\t{s}\t{s}\t{s}", .{ "", "", esh, pclass, "" }) catch "";

The last field is "" — but niches[i] is available at the call site (line 263 passes it to fleetEvent on the very next line).

Impact

Workflow tournaments that judge multiple prompt variants produce scores that land in an anonymous cell. The backend cannot determine which built-in persona (reviewer, researcher, implementer, skeptic) they should replace. These scores are permanently invisible to promotion.

Fix

Change line 261 to pass niches[i]:

const prov = std.fmt.bufPrint(&provbuf, "{s}\t{s}\t{s}\t{s}\t{s}", .{ "", "", esh, pclass, niches[i] }) catch "";

Gap 2: Niche and provider_class are not part of the signed score envelope

Location

src/scoring.zig line 95 — the canonical message signed by signScore:

return std.fmt.bufPrint(buf, "v1\n{s}\n{s}\n{d:.6}\n{s}\n{s}\n{s}\n{s}", .{
    prompt_sha, parent_sha, score, run_id, judge_id, artifact_sha, eval_set_hash,
}) catch null;

Niche and provider_class are transported but not HMAC-signed. The code at src/mainloop.zig line 295-296 acknowledges this explicitly:

// + provider_class, niche (unsigned transport)

Impact

A score signed for one niche/tier could be replayed as a different niche/tier by mutating the unsigned transport fields before ingestion.

Fix

Bump to v2 with niche and provider_class in the signed message:

return std.fmt.bufPrint(buf, "v2\n{s}\n{s}\n{d:.6}\n{s}\n{s}\n{s}\n{s}\n{s}\n{s}", .{
    prompt_sha, parent_sha, score, run_id, judge_id, artifact_sha, eval_set_hash,
    niche, provider_class,
}) catch null;

The backend worker needs a coordinated update to verify v2 signatures against the ingested niche/provider_class, while still accepting v1 during a transition window.


Gap 3: Genome text is not validated against its claimed fingerprint

Location

src/telemetry.zig line 206 — fleetEvent("propose", ...) sends both prompt_text and prompt_sha, but nothing verifies that SHA256(prompt_text) actually produces the claimed fingerprint.

Impact

A proposal could carry mismatched text and fingerprint. If later promoted, clients would pull text that doesn't match the fingerprint that earned the scores.

Fix

Add a debug assertion at the proposal site (e.g., in runSub line 101-108) that promptFingerprint(so) == child_fp before sending. The backend should also independently recompute and reject mismatches.


Gap 4: Score scale is ambiguous (0–1 vs 0–100)

Location

The scoring pipeline accepts any numeric value. parseEvalScore in repl_glue.zig parses both scales. The backend clamps means into [0, 1], so percentage scores (43, 76, 98) all become 1.0 after clamping — indistinguishable from a perfect fractional score.

Impact

Cells containing mixed-scale scores produce invalid aggregate statistics and wrong champion selection.

Fix

  1. Canonicalize: all DGM scores must be [0, 1].
  2. In runEval and scoreVariants, normalize percentage scores (divide by 100 if > 1).
  3. In the backend, reject scores outside [0, 1] rather than silently clamping.

Gap 5: scoreVariants doesn't propose the genome before scoring

Location

src/subagent.zig scoreVariants (line 252-263) calls scoreEvent and fleetEvent("submit", ...) but never calls fleetEvent("propose", ...) with the prompt text.

Compare with runEval in src/agent_compact.zig (line 110):

if (niche.len > 0) t.fleetEvent("propose", niche, genome, "", pclass, "", 0, sys);

Impact

If a variant enters scoreVariants without a prior runSub proposal, the score lands but the genome text is absent. A cell with scores but no genome text cannot produce a deployable champion.

Fix

Add a propose before submit in scoreVariants, matching the runEval pattern:

if (niches[i].len > 0) t.fleetEvent("propose", niches[i], genome, "", pclass, "", 0, overrides[i].?);

Proposals are deduplicated by fingerprint in the backend, so this is safe even if runSub already proposed it.


Gap 6: Run telemetry and score telemetry share no stable join key

Location

runEvent (in runSub, line 145) and scoreEvent (in runEval/scoreVariants//score) use different run_id sources — trajectory turn IDs vs scoring.g_run_id.

Impact

Cannot correlate operational outcomes (duration, tools, success) with evaluation fitness for the same agent execution.

Fix

Standardize on a single run_id that both runEvent and scoreEvent use for the same logical execution.


Priority

  1. Gap 1 — one line, biggest impact on cell formation
  2. Gap 4 — prevents corrupted rankings as volume grows
  3. Gap 3 — data integrity, cheap to add
  4. Gap 2 — security, needs coordinated backend change
  5. Gap 5 — completeness edge case
  6. Gap 6 — analytics, nice to have

Related files (this repo only)

  • src/scoring.zig — fingerprints, provider class, signing
  • src/subagent.zigrunSub (propose + run), scoreVariants (workflow judging)
  • src/agent_compact.zigrunEval (eval-driven scoring)
  • src/mainloop.zig/score JSON ingestion
  • src/telemetry.zigscoreEvent, fleetEvent, OTLP encoding
  • src/fleet.zigpullElites, agentTypePrompt, promoteAgents

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions