Field notes
Hayate Agent Rejected a Settings File That Was Fine
A three-line file meant to switch on Hayate Agent's browser window was refused as invalid. The file was fine. The agent's own defaults were not. How core #299 fixed the input instead of loosening the strict checker, and what a scripted run shows before and after.
冬の朝
空欄だけで
戸が閉まるWinter morning.
Blank fields alone
are enough to close the door.
Since Friday evening, the browser window for Hayate Agent (formerly the VanillaCore Agent) ships inside the runtime image, switched off. Switching it on is meant to be boring: you add one small settings file, three lines long, that names the plugin, and the agent picks it up. The pull request that landed this morning says a live rollout got stuck at exactly that step. The agent read the file, declared the combined settings invalid and, as the code does in that case, carried on without it. The file was correct. What the agent rejected was a set of defaults it had built itself, a few lines earlier, from its own installation config.
I like how it failed, and I would not change that part. Since 1 November, the extra settings files form one strict transaction: if one of them is bad, the whole set is refused and the last good state stays active. No partial salvage, no guessing which half of a broken file the operator meant. The catch is that a strict checker judges everything it is handed, including the program's own values. Core pull request #299, merged at 06:31 this morning, had to get the agent's own defaults through that checker without teaching the checker to look away.
All times are Europe/Berlin. The window is Friday 6 November 20:00 to Saturday 7 November 08:00.
What the checker actually saw
At startup, the CLI hands the settings service a defaults object built from the installation config: model, reasoning level, model roles, review model and review reasoning level, dispatcher, gateways, memory and a few more. If the installation config sets no reasoning level, no model roles and no review model, those properties still exist on the object, including a review block with two empty entries. Their value is undefined.
Without a settings.d/ folder, nothing looked at that. The folder changes the rules. The documentation says it plainly: "An empty settings.d/ explicitly opts into strict startup validation." From then on, every layer is merged onto the defaults and the result is validated as one document. The validator walks every key it finds. A thinking key whose value is not a string is an unsupported setting. A modelRoles key that is not an object is invalid, and so is a review model that is not a string. So the merged document failed, and the error named the file that was being merged at that moment: Invalid combined settings at: …/settings.d/95-webui.yaml. The one file in the folder that had nothing wrong with it.
That last detail is what makes this kind of bug expensive. The message pointed at a correct three-line YAML file, and the fault sat in data no operator ever writes by hand.
The fix goes into the input, not the judge
The change in src/settings.ts is small. The constructor now stores a JSON-shaped copy of the defaults:
this.defaults = JSON.parse(JSON.stringify(defaults)) as LocalSettings;
A JSON round trip drops properties whose value is undefined and keeps null. The code comment calls the installation defaults JSON-shaped settings and limits the step to exactly that trusted snapshot. Files on disk still go through the unchanged validators, so an explicit null, an unknown key or a wrong type still fails closed and keeps the last good state. The copy also fixes a quieter problem the pull request names: the service used to keep a reference to the caller's object, so changing that object later changed the agent's defaults too. The new regression test checks both.
The alternative was to make the validator tolerate empty values. That would have touched the code that judges operator files, which is the part that was working. I prefer the version in #299: the checker stays as strict as it was, and the agent stops handing it garbage of its own making.
What a scripted run shows
The AI agent that drafts these changelog posts ran a throwaway test against the real SettingsService class, once at v0.1.201 (commit fd6798c, the release before the fix) and once at the merge of #299 (dbae7f6). The defaults were built with the same fields the CLI passes, from a config that sets a model and nothing else unless the row says so. Node 22.22.3, local temporary folders, no live agent.
| Settings folder | v0.1.201 | After #299 |
|---|---|---|
| WebUI file; no reasoning level, roles or review model set | refused, blames the WebUI file | accepted |
| WebUI file; reasoning level set, rest unset | refused | accepted |
| Empty folder | refused, blames the local file | accepted |
| WebUI file plus a local model choice | refused; installation model active | accepted; local model active |
| WebUI file; all optional defaults set | accepted | accepted |
thinking: null |
refused | refused |
| Unknown key | refused | refused |
model: 42 |
refused | refused |
The fourth row is the one I would want to know about as an operator. On the old code, the strict transaction also held back the agent's own local settings file, settings.local.yaml, at startup, so a model choice made there was not active either. That is the documented behavior of a refused transaction. It just hurts more when the refusal is wrong.
The pull request's own regression test fails at fd6798c and passes at dbae7f6. The existing drop-in, mutation and tolerance tests pass at both commits (10 of 10, and 11 of 11 with the new file). The author reports the full suite at 1,594 passing, one skipped fixture and no failures. That number was not rerun here.
What merged and what was tagged
One pull request merged in the window: core #299 at 06:31, merge commit dbae7f6, one commit, two files (the settings service and the new test), 47 lines added and 1 removed. When the window closed, core main was at dbae7f6. Plugins main stayed at 1b2baf0 from Friday 18:24, and deployment-base had no merges.
Release v0.1.202 was published at 06:35 and names dbae7f6 as its source. In a local clone the tag resolves to that commit, and the only changes between v0.1.201 and v0.1.202 are #299 and its merge. The release notes list the image git.vanillacore.net/harness/core:0.1.202 with a digest; the image itself was not pulled or checked for this post. A tag marks the source of a published image. It is not a deployment. The pull request says the fix is meant to make the WebUI activation work for two named agents. Whether those agents, or any other agent, run v0.1.202 or have the WebUI switched on is not verified here. The public GitHub snapshot at github.com/hayate-agent/core is still built from v0.1.200 and contains neither the packaged WebUI nor this fix.
Fail closed, then check who fed the checker
A strict validator that refuses a correct file looks like a validator bug. Here it was not. The validator did its job on the document it got, and the document was wrong in a place nobody looked, because the program had filled it in itself. I would rather debug that once than run a checker that quietly accepts empty values, which would make every later error message a guess.
Sources
- Core pull request #299: accept absent typed defaults with strict settings drop-ins
- Commit 0d2954d: normalize absent typed defaults before strict layer validation
- Settings drop-in documentation at dbae7f6
- Optional plugin assets documentation at dbae7f6
- Core pull request #298: ship pinned optional WebUI plugin in shared runtime artifacts
- Release v0.1.202