jk
Blog

Two merges that broke my private fork

I run an open-source AI agent from a private fork, with a rule that keeps its updates on my branch. Twice, merging in the project's new work broke it.

29 October 20264 min readTooling

Since June I’ve run Hermes Agent, an open-source AI agent from the AI lab Nous Research, on a server at home. You talk to it through chat apps like Telegram, and it does its work on that server. I wanted some changes the project doesn’t have, like support for one more AI model provider. So I run it from a private fork, my own copy of the project, with my changes on a branch of their own.

One of those changes is a small rule for updates. The agent updates itself with one command, and out of the box that command switches the server to a branch called main. In my repo that branch is an old copy of the project from 1 June, without any of my changes. My rule makes the update stay on my branch and pull its newest code.

To keep the fork current, I merge in everything the project adds, and it adds a lot. In June alone I merged 3,202 of its commits, more than a hundred a day. Twice in five weeks, a merge that git reported as done broke my update rule. The first time the rule was gone. The second time it was still there, and it crashed.

Where the code lives

Upstream is the original project: Hermes Agent’s public repo on GitHub, where Nous Research and its contributors add work every day. My fork is a separate private repo, because GitHub doesn’t let a fork of a public repo be private. My server runs the agent from my branch and gets new versions through the agent’s update command.

Upstreamthe public project on GitHub
Merge into my branchconflicts fixed by hand
My private repo
My serverupdates itself with one command
Upstream's work reaches my server one merge at a time. The merge is where my changes and upstream's meet.

Keeping the fork current means merging upstream’s work into my branch. Git combines the changes from both sides. Where both sides changed the same lines, it stops and shows me a conflict, and I decide what to keep. Everywhere else it combines them on its own, and a file with no conflict merges cleanly. When no conflicts are left, git reports the merge as done.

These merges are big. In October one of them changed 13,049 files, far more than I can read. So I resolve the conflicts git shows me and lean on the tests for the rest.

The first break: a conflict took the rule with it

The first break came in September, from a cleanup upstream. Upstream had split up the big file behind the agent’s commands, the one that held my rule, and moved the update code into new files.

When I merged that on 18 September, git stopped on conflicts in six files, that one among them. I resolved it by keeping upstream’s new version of the file and dropping my side of it, and my rule went with it. With no conflicts left, git reported the merge as done.

The tests for my rule lived in other files, which merged cleanly, so from then on they tested a function that no longer existed. Three of them failed, and 41 more stopped with errors before they could run. I didn’t see those failures until 6 October, 18 days later. In all that time, the update command would have switched my server to the old copy.

On 6 October, right after the next merge, I put the rule back next to the update code in its new home.

The second break: a clean merge that crashed

The rule lasted 17 days in its new place, until the next merge. On 23 October I merged upstream’s latest stable release, and this time the file with my rule merged cleanly. My rule used a helper from one of Python’s standard modules. Upstream had deleted the line that imports that module, since nothing of its own in the file used it anymore. The two edits sat on different lines, so git combined them without asking. From then on the update command crashed, and so did its check for updates, because my rule called something the file no longer imported.

Putting the import back would have stopped the crash, but the update would still have gone to the old copy. Upstream had also added release channels, a setting for which versions an install follows. Upstream’s change put installs that come straight from the official repo on published releases. It left forks following the newest commit on the branch called main, which in my repo is the old copy.

So the rule had to move again, into the channel code, since that’s where upstream now decides which branch an update follows. I kept upstream’s version of the file my rule had been in and added one small function to the channel code. On a fork, an update now follows the branch the server is on. Installs from the official repo behave as before.

17 JuneThe ruleAn update stays on the branch the server is on.
18 SeptemberLost in a conflictUpstream's new version of the file was kept, and the rule went with it.
6 OctoberPut backNext to the update code in its new home.
23 OctoberMerged cleanly, then crashedUpstream had removed an import the rule used. The rule moved into the channel code.
The same small rule broke twice in five weeks, and both times git reported the merge as done.

What I learned

The merges that git reported as done did the damage. Git stopped on every conflict and showed it to me. It didn’t stop when my resolution dropped the rule, or when a file that merged cleanly left the rule calling something that was gone.

Put the rule where upstream makes the decision now. By October upstream had moved the choice of branch twice, first into new files and then into release channels. Each time the rule had to follow, next to the code that makes the choice.

Where it stands

My server still updates along my own branch. Two things guard the rule now. One is a test that sets up a real fork and runs an update and a check for updates. It fails if the rule is missing or broken, or if either one goes anywhere but the branch the fork is on.

The other is a habit: after every merge I go through my own changes by hand and make sure each one still exists and still runs. The test only helps if someone looks at its result after a merge, and in September nobody saw the failing tests for 18 days.

Read next