magus v0.4.3 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
3 min read

MGS4003: non-deterministic output

A project's declared outputs produced different content across two consecutive runs of the same dispatch. The same inputs should yield the same outputs; magus treats determinism as a build-system invariant.

[MGS4003] non-deterministic output (see .../MGS4003.md)
  project=api target=build differing_paths=[api/dist/bundle.js api/dist/manifest.json]

Why

This is an error, not a warning: it fails the run. Determinism is not a preference here, because three things magus does are unsound without it: drift gating compares regenerated output against what is committed, a cache hit replays stored bytes instead of rerunning, and a conflict in generated output is resolved by regenerating. A generator that emits different bytes for the same inputs breaks all three. The fix is always in the generator.

It is only emitted by --race=replay, which:

  1. Runs the affected set normally (concurrent, cache-aware).
  2. Content-hashes every declared output of every project.
  3. Re-executes the same set sequentially, bypassing the cache.
  4. Content-hashes the outputs again.
  5. Compares the two hash sets per project.

Any project whose outputs differ between the two runs is non-deterministic. Common causes:

  • Embedded timestamps. A build that injects time.Now() or __DATE__ into the artifact (linker build IDs, pkg/build.go ldflags, source maps with generation timestamps).
  • Map iteration order. Go and Python iterate maps in random order; a generator that emits code from a map without sorting keys produces different bytes each run.
  • Parallel work-stealing leaking into output. Worker IDs, goroutine IDs, or the order of fan-in results landing in a manifest.
  • PID / hostname / cwd in the output. Some toolchains embed these by default (e.g. pip embeds the build tempdir path into compiled .pyc).
  • Random salt / nonce. Crypto helpers used at build time without a fixed seed.

This check is expensive: it doubles the wall-clock time of the affected build, and the second pass cannot benefit from the cache. That is why it is opt-in and belongs on a scheduled audit rather than on every push. Asking for it and getting a warning would be the worst of both, so when you ask, you get a verdict.

Resolution

1. Identify the source

The differing_paths list narrows the search. Open one of the differing files in both runs and diff them. The divergence is often visible at first glance (a timestamp, a permuted list, a tempdir path).

For binary outputs, use diffoscope (the reproducible-builds.org tool) which unpacks archives, decompresses sections, and diffs the contents recursively:

magus run build
cp -r api/dist /tmp/run1
magus run build --race=replay   # second run rebuilds
diffoscope /tmp/run1/bundle.js api/dist/bundle.js

2. Remove the source of non-determinism

Common fixes:

  • For embedded timestamps, ldflag the build time to a fixed value (-X main.BuildTime=$(git log -1 --format=%cI) ties it to the commit timestamp, which is deterministic).
  • For map iteration, sort keys before emitting (slices.Sort(maps.Keys(m))).
  • For tempdir paths, use a fixed relative path or strip the absolute prefix.
  • For build IDs and PIDs, pass -buildid= to go build and -Wl,--build-id=none to the linker.

3. If non-determinism is unavoidable

Some artifacts (e.g. compiler outputs with debug info on Windows) are non-deterministic by design. For these, exclude the path from the project's declared outputs so the cache doesn't track them.

What this is not

  • Not a runtime crash detector. It only checks that outputs are bit-for-bit identical, not that they behave the same.
  • Not run by default. You must pass --race=replay explicitly.
  • Not a substitute for reproducible-builds.org tooling. That community has stronger guarantees (cross-machine, cross-time, cross-locale). MGS4003 is a same-machine same-session check.

See also

MGS4003racedeterminismreplayreproducible buildsoutputshashing
Last updated (353014be)
Earlier changes on this page (4)

Full history ↗ · Blame source ↗

Glossary

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. Project in full.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. Target in full.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). Op in full.

Manifest

The file that declares what a package is: its name, dependencies, entry points and tool config (go.mod, package.json, pyproject.toml, Cargo.toml). How the workspace builds, tests and lints the package belongs in the magusfile, not in the manifest's scripts. Manifest in full.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. Cache in full.

Affected

The set of projects touched by a change; magus affected <target> runs a target only over them. Affected in full.

CI

An ordinary target you compose yourself with ctx.needs; magus does not hardcode its stages. It is the anchor magus affected ci keys off, and ci strips the rw charm. CI in full.

Tool

An action a model can call through its host: a name, a description, a typed input and a result. magus serves its tools over MCP, each a read of the workspace model or a run of the CLI it already has. Tool in full.

Session

An agent host's conversation, by the id the host delivers to its hooks. magus never mints one: a record with no session is unattributed, and the OS user it carries says whose account ran it. Session in full.

Window

The terminal a command runs in. It keys fire-once notices for a caller no host gave a session, and is never recorded as a session. Window in full.

Run

One target executing under one magus invocation, such as magus run test web or magus affected ci. A run keeps its captured output behind an output reference. Every magus run is a run whether or not any job asked for it; see Job for how the two relate. Run in full.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.