feat(daemon): surface daemon upgrade skew and staleness to CLI clients - #611
Open
madeinoz67 wants to merge 9 commits into
Open
feat(daemon): surface daemon upgrade skew and staleness to CLI clients#611madeinoz67 wants to merge 9 commits into
madeinoz67 wants to merge 9 commits into
Conversation
This was referenced Aug 19, 2026
zzet
approved these changes
Aug 19, 2026
zzet
left a comment
Owner
There was a problem hiding this comment.
@madeinoz67 good work, thank you!
I found a non-blocking proposals, please let me know if you'd like to address them.
Review #4969353348 (r3810847325): 'brew upgrade gortex' only works for macOS brew installs — curl-installed and scoop users get advice that fails. 'gortex upgrade' detects the install method and runs the matching update, and it stops/starts the daemon around the swap itself.
Review #4969353348 (r3810812621): the comparators outgrew the proxy — internal/version parsed semver but could not order it, so Compare fills a package gap instead of living as cmd/gortex-private helpers. The §11 precedence test moves with it (TestComparePrecedence); the skew remedy stays in cmd/gortex as CLI messaging.
…robe Review #4969353348 (r3810888879): doctor is the command a confused user runs when versions feel wrong, so the handshake row now downgrades to a warning and prints the same remedy line the MCP proxy emits — computed by the same daemonSkewWarning, so the three surfaces cannot disagree. DoctorEnvironment carries cli_version and version_skew_warning for --json consumers; dev builds stay silent via the existing sentinel.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #609. When the homebrew cask upgrades
gortex, a long-running daemon keeps serving from the old binary and every CLI invocation silently talks to the version it was just upgraded past.docs/versioning.mdalready promises that "the daemon exposes its running version in the handshake ACK asDaemonVersionand on the control surface'sstatusresponse — clients can feature-gate or warn on mismatch" — this PR implements the client half of that documented intent: the MCP proxy now warns on mismatch, anddaemon statusshows both the CLI-vs-daemon version skew and the daemon's own on-disk-binary drift.Three surfaces, one compare each:
cmd/gortex/proxy.go):daemonSkewWarning(daemonVer, localVer)compares the handshake-reported daemon version againstcanonicalVersion()and prints one stderr line through the existing[gortex mcp]prefix — warn-and-continue, never gate. The remedy is direction-aware: daemon older ⇒run 'gortex daemon restart' to upgrade the daemon(a restart respawns from the newer on-disk binary); binary older ⇒run 'gortex upgrade'— it upgrades the way gortex was installed (brew, scoop, go install, or the install script) and restarts the daemon around the swap itself; unparseable/same-precedence-different-build ⇒ the generic restart-or-upgrade remedy. Dev builds stay silent on either side (v0.0.0-devas the local OR the daemon identity) so plaingo buildruns — CLI or daemon — never nag or advise "upgrading" a dev process.internal/daemon/server.go): the daemon captures its own executable's path/size/mtime at startup and stats it again on everyControlStatus, populating additiveStatusResponsefields (BinaryChecked/BinaryStale/BinaryReplacedAtUnix). A stat failure or an uncaptured identity reports unknown (both flags false) — unknown is never rendered as fresh. Theon-disk binary changedwarning is logged exactly once per detection (once-flag), so pollingstatus --watchcannot spam the daemon log. Detection covers both hand-started and launchd daemons: it derives from the kernel's view of the running image (os.Executable), not from any launchd integration.cmd/gortex/daemon.go): two conditional rows in the header table —cli (differs from daemon)when the same skew compare fires, andbinary stale — on-disk image newer than running image; run 'gortex daemon restart'when the daemon reports drift. Matching versions and fresh binaries keep the table exactly as terse as before.cmd/gortex/doctor.go, review follow-up): the handshake row downgrades from ✓ to!and prints the same remedy line — computed by the samedaemonSkewWarning, so doctor,daemon status, and the MCP proxy give one verdict.DoctorEnvironmentcarriescli_version/version_skew_warningfor--jsonconsumers.Changes
cmd/gortex/proxy.go—daemonSkewWarning+ connect-time stderr warning in the proxy path; direction-aware remedies (gortex upgrade, never package-manager-specific); dev-build silence.internal/version/compare.go(review follow-up) —Compareorders two Versions by SemVer 2.0.0 precedence (build metadata ignored); fills a package gap — internal/version parsed semver but could not order it. The §11 precedence chain is pinned incompare_test.go.internal/daemon/server.go,internal/daemon/proto.go— startup binary-identity capture, per-status drift probe, log-once warning, additiveStatusResponsefields (no protocol-version bump; older clients ignore the new fields).cmd/gortex/daemon.go— conditionalcliandbinaryrows inrenderDaemonHeader;cmd/gortex/daemon_status_render_test.gopins both rows and their negative cases.cmd/gortex/doctor.go— skew-aware handshake row +DoctorEnvironmentfields;cmd/gortex/doctor_env_test.gopins the ✓/!rows.Testing
go test -race ./...) — 140 ok / 0 data races; six non-green packages, every one dispositioned against cleanmainatb55b9a0d(throwaway-worktree reruns) or by isolation re-run, none a regression from this PR:TestFormatModelCostSeparatesUnpricedFromZero(cmd/gortex) andTestCostAvoidedAll_IncludesDefaults(internal/savings) both fail identically on main (stale pricing-table rows);TestUninstallHookPostCommit_RemovesFileWhenStubOnly(internal/githooks) fails identically on main;internal/agents/opencode+internal/gitcmdare load flakes that pass on this branch in isolation and on main;internal/graph/store_sqlitetimed out at 606s only under the pegged full-suite run;TestRouter_ReloadConfig_ConcurrentNoRace(internal/daemon) hangs deterministically on this host, identically on main (workers stuck inServerClient.ProxyToolCtxHTTPgetConn; skipped via-skip; needs its own issue).TestDaemonSkewWarning— 7 subtests: equal, daemon-older, daemon-newer, same-version-different-build, unparseable daemon version, empty daemon version, dev build.TestComparePrecedence(internal/version) — the semver.org §11 chain (alpha < alpha.1 < alpha.beta < beta < beta.2 < beta.11 < rc.1 < release) pinned pairwise againstCompare, plus build-metadata-ignored and numeric-vs-alphanumeric rows.TestRenderDaemonHeader_SkewRow— 3 subtests: skewed appends theclirow, matching omits it, dev build omits it.TestRenderDaemonHeader_BinaryRow— 3 subtests: stale appends thebinaryrow, fresh omits it, unchecked omits it even ifBinaryStaleis set.TestPrintDoctorEnvironment_VersionSkew— matching versions keep the ✓ row; skewed versions render!plus the remedy line.TestStatusBinaryDriftFreshThenStale/_SizeChange/TestStatusBinaryStatErrorReportsUnknown/TestStatusBinaryUncapturedIdentityReportsUnknown— full lifecycle through the realControlStatushandler: fresh → mtime-drift stale → log-once pinned; size-only drift; stat-error ⇒ unknown; uncaptured identity ⇒ unknown.go build ./...+go vet ./...clean;gofmtclean on all branch-touched files;go test -race ./cmd/gortex/ ./internal/version/ ./internal/daemon/green except the two pre-existing dispositioned failures above.statper status poll).E2E dogfood (XDG-isolated scratch daemon; binary swapped beside it, simulating a cask upgrade)
Teardown by PID kill (never
daemon stop); the live homebrew daemon was healthy and untouched afterwards (v0.63.4+5f5fce2, 5 sessions,ready).Review follow-ups (#4969353348)
All three proposals addressed (352131a, 205b2e2, 668dfa4):
gortex upgrade— install-method-agnostic, and it bounces the daemon around the swap itselfinternal/version.CompareChecklist
Meta["methods"]for interfaces (if applicable) — N/AEdgeMemberOfedges to their containing type (if applicable) — N/AKnown limitations
v0.0.0-devCLI identity never triggers the warning or theclirow — the spec's "degrade silently" posture, extended to the dev-sentinel case so contributor builds don't nag.statusis requested; a daemon nobody polls still drifts silently. The proxy warning independently covers every MCP client connect.