feat: the agent derives its version instead of being asked to remember it (4386)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 2s
CI and images / frontend-build (push) Successful in 19s
extension / lint (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 33s
CI and images / integration (push) Successful in 2m25s
CI and images / sign-extension (push) Successful in 4s
CI and images / build-web (push) Successful in 2m1s
CI and images / smoke-web (push) Successful in 1m2s
CI and images / build-agent (push) Successful in 10m7s
CI and images / promote (push) Skipped

Operator: the agent's build string could not identify the agent. VERSION was a
literal in app.py an author was meant to bump, and nobody did — the September
image printed the same "2026-07-17.1" as the July one, so the one surface
meant to answer "did my pull work?" answered the same either way.

Nothing new was needed. scripts/artifacts.sh has derived a version per
artifact since milestone 313, and build-agent has been computing the agent's
on every run and printing it to the log. The image just never carried it.

Three values, never folded together (rule 149):

  FC_VERSION   YYYY.MM.DD.HHMM from the COMMIT its shipped files last changed
               in — identical on dev and main for the same source, which is
               what makes "am I running production's code?" answerable.
  FC_CHANNEL   a sibling field, never a suffix inside the name.
  FC_REVISION  the 12-char sha; the same string as the fc.revision LABEL, so
               the image and the registry cannot disagree about which commit
               this is.

The page SHOWS the version and COMPARES the revision. Those were one value
before, which is how a version acquires a second job and then cannot be
changed without breaking the reload banner. An unstamped local build reads
`unknown` and compares `local` — absent rather than empty, one spelling of
"cannot say".

scripts/artifacts.sh joins the AGENT path set in the same commit, and it had
to: a version has no backstop. A revision that is computed differently stops
matching the published label and forces a rebuild, so it self-corrects; a
version is compared against nothing, so a change to cmd_version alone would
leave the agent publishing the old format with nothing to contradict it. That
is #3202's finding, and the agent was rightly exempt only while it had no
version of its own. tests/test_artifact_paths.py pins it.

Also corrects two build.yml comments claiming agent/ had not changed since
2026-07-17. Both were already false — it changed 2026-09-23 — and one of them
is the stated rationale for the force_build escape hatch. Rewritten without
dates: how long an artifact has been quiet is a `git log` question, and its
answer in a comment is wrong the next time anyone commits (lesson #4383).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-24 07:07:31 -04:00
co-authored by Claude Opus 5
parent 970d17f98a
commit e1cd629196
7 changed files with 325 additions and 37 deletions
+27 -8
View File
@@ -12,16 +12,21 @@ from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse
from . import logbuf
from .build_info import FC_CHANNEL, FC_REVISION, FC_VERSION, build_id, display_version
from .config import Config
from .gpu import read_gpu
from .worker import Worker
log = logging.getLogger("fc_agent.app")
# Bump on every agent change. The page embeds this and /status reports it; the UI
# warns to reload when they differ — so a stale browser-cached page can't be
# mistaken for "the new image didn't deploy". (Belt-and-braces with no-store.)
VERSION = "2026-07-17.1 · idle model-unload: after ~5 min idle the GPU models release their VRAM and reload on the next job (env IDLE_UNLOAD_SECONDS, 0=off) · sleep mode sheds to one downloader"
# DERIVED at image build time, not hand-maintained — see build_info. This was a
# literal an author was asked to bump on every agent change, and the September
# image printed the same "2026-07-17.1" as the July one, so the surface meant to
# answer "did my pull work?" answered the same either way.
#
# Two values with two jobs, kept apart (rule 149): the page SHOWS the version
# and COMPARES the build id. /status reports both, plus the raw fields, so a
# reader never has to take a formatted string apart to get at one of them.
logbuf.install()
cfg = Config.from_env()
@@ -52,7 +57,14 @@ def _maybe_autostart() -> None:
@app.get("/", response_class=HTMLResponse)
def index() -> str:
return _PAGE.replace("__BUILD__", VERSION)
# Two substitutions, not one: `__VERSION__` is what a person reads in the
# meta line, `__BUILD_ID__` is what the script compares against /status to
# notice the page is a cached copy from a previous build.
return (
_PAGE
.replace("__VERSION__", display_version())
.replace("__BUILD_ID__", build_id())
)
@app.post("/start")
@@ -117,7 +129,14 @@ def status():
s["fc_url"] = cfg.fc_url
s["configured"] = bool(cfg.token)
s["queue"] = worker.latest_queue()
s["build"] = VERSION
# `build` is the comparison token the page checks — see build_info.
# `version`/`channel`/`revision` ride BESIDE it rather than inside it, so a
# reader wanting the version never has to parse it back out of something
# else. Absent rather than empty when the image carries no stamp.
s["build"] = build_id()
s["version"] = FC_VERSION or None
s["channel"] = FC_CHANNEL or None
s["revision"] = FC_REVISION or None
return JSONResponse(s)
@@ -203,7 +222,7 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
<div class=brand><span class=logo>◆</span> FabledCurator <span class=sub>GPU agent</span></div>
<div class=conn><span class="dot" id=dot></span><span id=connlbl>—</span></div>
</header>
<p class=meta>Server <code id=fc>—</code> · token <code id=cfg>—</code> · build <code id=build>__BUILD__</code></p>
<p class=meta>Server <code id=fc>—</code> · token <code id=cfg>—</code> · build <code id=build>__VERSION__</code></p>
<div id=verbanner class=banner style="display:none;background:#3a1212;border-color:#5a1717;color:#ffb3b3">
a newer agent version is running — reload this page (Ctrl+Shift+R) to update the controls
@@ -262,7 +281,7 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
</section>
</div>
<script>
const PAGE_BUILD="__BUILD__"
const PAGE_BUILD="__BUILD_ID__"
let CAP=8
// Optimistic transitional state on click, then apply the POST's own status
// response (it returns worker.status()) for instant feedback — don't wait on the