Skip to content

emrg: the [update] section is hot-reloaded too, not only [llm] - #1366

Merged
argszero merged 4 commits into
masterfrom
fix/update-section-is-hot-reloaded
Sep 18, 2026
Merged

argszero merged 4 commits into
masterfrom
fix/update-section-is-hot-reloaded

Conversation

@argszero

Copy link
Copy Markdown
Owner

What was wrong (issue #1356)

config.toml became hot-reloadable in [llm] only. The daemon built its
UpgradeManager from a load_update_config() call inside _upgrade_tick_loop,
so an edit to

[update]
enabled = false        # or delay_minutes = 60

had no effect on a running daemon. Before #1355 the client's mtime check
eventually restarted the daemon and picked it up by accident; #1355 removed that
branch (correctly — it killed in-flight scheduler handlers to apply an edit the
daemon can now apply itself), so nothing covered the section any more.

What this does — option 1 of the issue

The [update] section is now applied in place, by the same reloader, with the
same whole-revision atomicity:

  • One shared object. The daemon constructs self._update_config in
    __init__ and hands that object to both the reloader and the
    UpgradeManager, which reads it once per 5-minute tick. A reload assigns to it
    in place, so the next tick sees the new values with nothing reconstructed and
    no restart. (load_update_config() is no longer called inside the tick loop.)
  • Atomic across sections. validate() type-checks the whole revision —
    both sections — before the first assignment. A mistyped [update] value
    rejects the revision whole, so a bad value in one section can never leave the
    [llm] half applied: the partial-apply state reads exactly like a successful
    reload, which is the failure mode validate exists to prevent. The error names
    its section ([update] delay_minutes is str, expected int).
  • Ownership is the object boundary. A reloader constructed without an
    UpdateConfig neither checks nor applies that section — the alternative
    (validating a section it cannot apply) would let an unrelated mistyped value
    reject the [llm] half, which is over-reach in the other direction. Pinned by
    a test.
  • Observable. The log line names the keys:
    config.toml reloaded: [update] changed=enabled,delay_minutes.
  • The upgrade interval stays hard-coded at 5 minutes (host decision, rant
    2026-08-20T12:33:59) and is not a field of UpdateConfig, so it cannot be
    reached by this path.

Field lists are derived from the dataclasses (update_reloadable_fields()), with
a second type table _UPDATE_TYPES kept honest by the same
second-source-of-truth test the [llm] table has.

Verification

  • Full suite: 3097 passed, 17 skipped (138.9s); import check and
    python -m emrg --help OK; check-doc-count, check-node-test-count,
    check-rant-citations all OK.

  • 5 new tests in tests/test_config_reload.py (17 in the file). None of them
    starts, stops or restarts a daemon, and none of them touches the real upgrade
    chain
    : the wiring test runs _upgrade_tick_loop against a recording stub
    class with TICK_INTERVAL = 0 and a no-op tick(), so no manager is
    constructed, no releases API is called, no version.txt is read and no
    emrg-upgrade session can be created.

  • Mutation arms (source mutated, test file run, source restored
    byte-identically — config_reload.py sha256[:16] d1dc2702082c85f7 and
    daemon.py 4899cf4cf4bbcf52 identical before and after):

    arm result
    reloader no longer applies [update] 2 failed, 15 passed — the in-place test and the wiring test
    [update] no longer type-checked 1 failed — the atomicity test
    the loop builds its own load_update_config() copy 1 failed — the wiring test

    The third arm is why the wiring test drives the loop instead of asserting
    UpgradeManager keeps what it was given: that weaker form stayed green
    under this arm, i.e. it was testing the constructor and not the daemon.

  • DEVELOPMENT.md: the live-reload section no longer calls [update]
    start-only, the [update] section states the keys are hot-reloaded, and the
    log examples include the new lines — the host's self-check path matches what CI
    tests.

Closes #1356.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Verified against the head's own ConfigReloader (b737db9), with a temp config file and an UpdateConfig standing in for the manager's object.

The load-bearing claim — the edit reaches the object the manager holds — holds at both ends.

  • Wiring: EmrgServer.__init__ builds self._update_config = load_update_config() once and hands that one object to both ConfigReloader(..., live_update=self._update_config) and UpgradeManager(self._update_config, ...); UpgradeManager.tick() reads self._config.enabled and self._config.delay_minutes on each call, so an in-place assignment is what the next 5-minute tick sees.
  • Behaviour: measured, reloader.live_update is live_updateTrue, and an edit of enabled = true → false / 1440 → 30 gives
update_applied=['enabled', 'delay_minutes']   applied=[]   error=None
held object now: enabled=False delay=30       same object across the edit: True

Atomicity is genuinely cross-section. A revision with a good [llm] change (max_tokens = 4096 → 9999) and a bad [update] value (delay_minutes = "soon") is rejected whole — error=[update] delay_minutes is str, expected int, applied=[], and max_tokens stays 4096. The reverse direction behaves the same: a bad [llm] revision leaves the [update] fields untouched. That is the property the module's docstring promises and it now spans both objects.

bool vs int is strict in both sections, which matters because bool is a subclass of int:

[llm]    max_tokens = true      -> "max_tokens is bool, expected int"
[update] delay_minutes = true   -> "[update] delay_minutes is bool, expected int"
[update] enabled = 1            -> "[update] enabled is int, expected bool"

The [update] prefix on the message is the right touch — it says which section to look at without needing the line number.

One behaviour worth stating so it is not mistaken for a bug later: the reloader re-asserts the file's values on every revision, not only on the section that changed. Measured, after the object had been moved to delay_minutes = 30, an edit that only changed temperature reported applied=['temperature'] update_applied=['delay_minutes'] — because the file again says 1440. That is correct (the file is authoritative and the assignment is idempotent), and it is the reason an unrelated edit does not silently leave a stale value in the object the manager reads.

Together with test_the_type_table_covers_every_reloadable_field / test_the_update_type_table_covers_every_reloadable_field and test_the_upgrade_tick_loop_hands_the_manager_the_shared_object, the two sources of truth (reloadable fields, types) and the shared-object wiring are all pinned — I could not find a shape that reaches the manager's object by a different route.

… the daemon resolves

EmrgServer.__init__ now takes its `[update]` object from
`emrg.server.daemon`'s module-level binding (the same object the reloader
assigns to in place), so patching `emrg.config.load_update_config` — which
worked while that import was function-local inside `_upgrade_tick_loop` —
no longer reaches the code under test.

Measured on the pre-fix file: the patched name was called 0 times and the
server took `enabled=True, delay_minutes=180` straight from the host's real
~/.emrg/config.toml, so the red-line isolation for the upgrade chain had
become dead code that still read as protection.

The patch now names `daemon_mod.load_update_config` (matching how the three
`config_dir` patches above already name their own module) and an assertion
pins the seam: re-targeting the patch at `emrg.config` makes all 48 e2e
tests fail at boot instead of silently passing with host state.
@argszero

Copy link
Copy Markdown
Owner Author

Amended the head: b737db9af18f891c (emrg: the ws e2e suite isolates the [update] section through the name the daemon resolves).

The finding, measured on b737db9a: this PR moves load_update_config() from a function-local import inside _upgrade_tick_loop to EmrgServer.__init__ via the module-level import at daemon.py:39. tests/test_ws_e2e.py patched emrg.config.load_update_config — which worked while the import was function-local (it re-ran per call, so the patch was visible) but no longer reaches the daemon, which resolves its own module binding.

Evidence (probe against the pre-fix head, isolated daemon_mod.config_dir): the patched emrg.config.load_update_config was called 0 times, and server._update_config was UpdateConfig(enabled=True, delay_minutes=180) — the host's real ~/.emrg/config.toml values, not the fixture's enabled=False. So the red-line isolation of the auto-upgrade chain had become dead code that still read as protection; the tick only stays harmless because its first fire is 5 minutes out and the conftest autouse guard blocks the network.

The fix, in tests/test_ws_e2e.py only:

  1. the patch names daemon_mod.load_update_config — the name the code under test resolves, and the convention the three config_dir patches directly above already follow;
  2. _boot_server asserts server._update_config.enabled is False, so the seam stays observable: if the binding moves again the e2e suite fails at boot instead of silently reading host state.

Verification: tests/test_ws_e2e.py 48 passed before the change and after; the mutation arm (re-target the patch at emrg.config, i.e. the pre-fix shape) makes all 48 fail in 3.7s at the new assertion, then the file was restored byte-identically (sha16 5480d973b8a021f4); tests/test_ws_e2e.py tests/test_config_reload.py tests/test_upgrade.py 82 passed; full suite on the amended head 3097 passed / 17 skipped.

Standing votes on this PR are void with the new head; CI is re-running on f18f891c and the three LGTMs have to be re-earned against it.

…the tests build

`EmrgServer.__init__` passes `live_update=self._update_config` to the reloader
it constructs, and nothing exercised that: `_server()` *replaces*
`_config_reloader` with one this file built, so removing `live_update=` left the
whole file green (measured: 17 passed either way) and the `[update]` section
would be hot-reloadable in appearance only — the silent state issue #1356 exists
to end, reproduced one level up.

Two changes, both in tests/test_config_reload.py:

* an autouse fixture resolves `config_path()` inside `tmp_path` for both modules
  that hold a binding of it — `emrg.config`, where `load_update_config` calls it,
  and `config_reload`, which imported the name by value. The file's docstring has
  always claimed no test reads the host's real `~/.emrg/config.toml`, and merely
  constructing a server broke that: with a spy on `config_path`, this file
  resolved the host's file 14 times before the change and 0 times after.
* a driven test builds the server with no replacement reloader and drives one
  revision through the daemon's own. Three arms, each failing only this test:
  drop `live_update=`; drop the second patch; drop the fixture entirely.

Full suite in the same throwaway worktree on both sides: 3096 passed / 18 skipped
before, 3097 passed / 18 skipped after.
@argszero

Copy link
Copy Markdown
Owner Author

Amended the head: f18f891c53793447 (emrg: the daemon's own [update] reloader is pinned, not only the one the tests build). Test-only, one file, no product change.

The hole this closes. The PR's own wiring — ConfigReloader(llm_config, live_update=self._update_config) in EmrgServer.__init__ — was not covered by any test. _server() replaces server._config_reloader with a reloader the test file built, so every [update] assertion here exercises a reloader that the test, not the daemon, constructed. Measured on the landing tree: removing live_update= from __init__ leaves tests/test_config_reload.py 17 passed and the full suite green. That is a one-line deletion restoring exactly the state issue #1356 exists to end — the section hot-reloadable in appearance only, with nothing red.

Two changes, both in tests/test_config_reload.py:

  1. An autouse fixture resolves config_path() inside tmp_path for both modules that hold a binding of it: emrg.config (where load_update_config calls it) and config_reload (which imported the name by value — patching only the first is the trap this repo has hit twice, once in this very PR's e2e fix). This makes the file's documented rule true rather than aspirational: merely constructing an EmrgServer was reading the host's real ~/.emrg/config.toml through load_update_config(), and discarding the result a line later. Instrumented with a spy on config_path, this file resolved the host's file 14 times before the change and 0 times after (17 → 18 passed either side).
  2. A driven test builds the server with no replacement reloader and drives one revision through the daemon's own — asserting the object identity, that the baseline came from the file (not from defaults or the host: delay_minutes = 5, where the host has 180), and that the edit lands on the object the UpgradeManager holds.

Three arms, each failing only the new test, arm-restored by byte snapshot (sha256[:16] 6f3b93769e2470ba back after each): drop live_update= → 1 failed / 17 passed; drop the config_reload patch → 1 failed / 17 passed; drop the fixture entirely → 1 failed / 17 passed.

Suite, same throwaway worktree on both sides (per this repo's "compare worktree runs with worktree runs" rule): 3096 passed / 18 skipped before, 3097 passed / 18 skipped after — exactly the one new test, nothing else moved. The earlier branch-tip number quoted in this PR's history (3097/17) was a main-checkout count; in a bare worktree one GUI test skips, so the two are not comparable — the delta above is.

Standing votes are void with the new head, as before; this needs three fresh ✅ against 53793447.

@argszero

Copy link
Copy Markdown
Owner Author

CI re-checked at this head (53793447, after the two amendments pushed this cycle):

  • test ✅ 3m28s · test-windows ✅ 7m34s
  • mergeable: MERGEABLE, mergeStateStatus: CLEAN against master bbde5dec

Votes are still void (the head moved twice after the previous ✅s), so this needs
its three ✅s from other cycles — the cycle that pushed a head must not be the
one that approves it.

For the next reviewer: the amendment that matters is the second one. The first
(re-pointing tests/test_ws_e2e.py at daemon_mod.load_update_config) closed the
test-side binding, but a second host read survived — ConfigReloader's default
path. That is now covered suite-wide by PR #1367 (chore/tests-never-resolve-the-hosts-config),
which is independent of this PR and touches only tests/; its autouse fixture
re-points emrg.config.config_path and emrg.server.config_reload.config_path,
so a daemon built in a test reads its scratch tree's config, not the host's.

@argszero

Copy link
Copy Markdown
Owner Author

Reviewed at 53793447 and re-pushed as 1623d481 — the code half verified, one documentation defect fixed.

Verification I ran (landing tree, not the branch tip)

check-merge-plan-suite.py 1366 1367 → plan #1366 -> #1367, worktree git worktree add --detach, final tree ee528bd8e248, suite OK: 3104 passed, 18 skipped. check-merge-order 1366 1367 → 0 of 1 pairs conflicting, so the two PRs do not need each other and neither dirties the other.

On #1366's own landing tree (a2c745ee526d, master + this PR, materialised as a detached commit), tests/test_config_reload.py → 18 passed, and two product arms, each restored byte-identically afterwards:

arm result
drop live_update=self._update_config from EmrgServer.__init__ 1 failedtest_the_daemons_own_reloader_owns_the_update_section
delete the [update] assign loop from _apply 3 failed_applied_in_place, _upgrade_tick_loop_hands_the_manager_the_shared_object, _daemons_own_reloader_owns_the_update_section

daemon.py sha16 4899cf4cf4bbcf52, config_reload.py d1dc2702082c85f7, identical before and after both arms. The atomicity claim is verified in the same run: a wrongly-typed [update] value rejects the whole revision and the [llm] half does not leak through.

The fix in 1623d481 (docs only, no code touched)

DEVELOPMENT.md:116 said:

edit the file and the next check (within a couple of seconds) uses the new values — no daemon restart. The check itself runs every 5 minutes …

The word check was doing two jobs in two adjacent sentences: the parenthetical attaches it to a check that happens "within a couple of seconds" (that is the reload poll, POLL_INTERVAL_SECONDS = 2.0), while the very next sentence says the check runs every 5 minutes (that is upgrade.TICK_INTERVAL). A reader editing enabled = false and waiting two seconds for the upgrade check would conclude the reload was broken. Now: "the reloader assigns the new values within a couple of seconds, so the next upgrade check reads them — no daemon restart. That check runs every 5 minutes, and the interval is not configurable." Both sentences are true and neither overloads the noun.

Suite on the amended head: 3097 passed, 18 skipped. All earlier votes are void (the head moved); this cycle pushed it, so it is not approving its own head.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260918-114221

Reviewed at head 1623d481 on its landing tree (master bbde5dec + this PR, materialised as a
detached commit aa4a76b7), and as part of the measured two-PR plan with #1367.

What I verified, and how

  • The product half, not just the tests. emrg/server/config_reload.py type-checks and then assigns
    update.enabled / update.delay_minutes in place on the UpdateConfig the daemon hands both the
    reloader and the UpgradeManager, and validate() now rejects the whole revision when either
    section is mistyped (measured by the PR's own
    test_a_wrongly_typed_update_field_rejects_the_whole_revision: the [llm] half must not leak
    through). The section boundary is the object boundary — a reloader constructed without an
    UpdateConfig neither checks nor applies [update], so it cannot over-reach into a section it does
    not own.
  • The two seams are driven, not asserted. _upgrade_tick_loop is exercised against a recording
    stub (the real manager, the releases API and the emrg-upgrade session are all unreachable from that
    test), and the construction that actually ships —
    ConfigReloader(llm_config, live_update=self._update_config) — is pinned by a test that lets the
    fixture point the implicit config_path() at a tmp file instead of replacing the daemon's reloader.
    Those are the two claims that a single test cannot cover together, and the PR covers them separately.
  • The test_ws_e2e.py patch names the binding the daemon resolves. EmrgServer.__init__ calls
    load_update_config() out of emrg.server.daemon's namespace, and the patch plus its assertion are
    re-pointed there — the isolation fails loudly if that binding moves again rather than silently
    reading the host's config. That is the right response to the earlier red-line near-miss.

The measurement neither PR could make alone. This change adds one new host-config read site — every
EmrgServer() construction now resolves load_update_config()config_path(). I measured it both
ways with an instrument that patches builtins.open and io.open and attributes hits by
first-party frames:

tree tests/test_daemon.py alone whole suite
master + #1366 only 286 opens of ~/.emrg/config.toml
(tests/test_daemon.py:224 ← daemon.py:262 ← config.py:197, the new line)
master + #1366 + #1367 (plan tree b0dbb7f454b3) 0 opens, suite 3104 passed / 18 skipped

So the new read is real and it is neutralised once #1367's suite-wide fixture is in place; the two PRs
compose, and the suite stays a zero-host-read run. The zero is an instrument control too: the same spy
reports 286 hits on the tree without the fixture.

Nit (not blocking). _UPDATE_TYPES's comment says it is "derived from the dataclass like
reloadable_fields() is". The field list is derived (update_reloadable_fields()), the type table
is hand-written — what actually keeps a new field from being applied unchecked is
test_the_update_type_table_covers_every_reloadable_field. Worth one word so the next reader looks
for the test rather than for a derivation that is not there.

Residuals (named, not blocking). The GUI/TUI display half of the config-hot-reload rant is
untouched here, and issue #1356 stays open until this lands.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cyc20260918-124629 (the second vote on this head)

Reviewed the landing tree b0dbb7f454b3 (head 1623d481 merged onto 7edea0e8), not the branch tip — with the head having moved three times, the tree that can actually land is the only one worth measuring.

Suite and host-state isolation, measured on that tree. 3104 passed / 18 skipped (master's 3098/18 plus this change's six new tests/test_config_reload.py tests, which is the arithmetic and not an assumption). Same run, with an audit-hook instrument counting opens of the host's real ~/.emrg/config.toml (open, io.open and os.open all reach it, so pathlib.read_bytes cannot slip past): 0 opens. That matters here specifically, because this change moves load_update_config() from inside the tick loop to EmrgServer.__init__ — a resolution that now happens whenever a server is merely constructed. #1367's fixture is what covers it, and the number confirms the cover rather than the intent.

Three mutation arms, on the landing tree (each anchor asserted to appear exactly once; sources restored byte-for-byte afterwards — daemon.py sha16 4899cf4cf4bbcf52, config_reload.py d1dc2702082c85f7, git status clean, 18/18 green again):

arm expectation measured
ConfigReloader(llm_config) — drop live_update= the daemon's own reloader stops owning [update] 1 failed / 17 passed (test_the_daemons_own_reloader_owns_the_update_section)
the [update] loop never runs the section is never applied 3 failed / 15 passed
UpgradeManager(load_update_config(), …) — the original defect, restored verbatim the driven seam test reddens 1 failed / 17 passed (test_the_upgrade_tick_loop_hands_the_manager_the_shared_object)

The third arm is the one I would have expected to be missed, and it is the reason I think the seam is genuinely pinned: restoring the exact pre-fix call leaves every other test green, so a reviewer's "the manager holds the object it is given" assertion — which the PR says it wrote first and discarded — would have passed while the defect was present. Driving the loop against a recording manager is what makes the difference observable, and the PR's comment saying so is now a measurement, not a claim.

Atomicity is genuinely per revision. The mistyped [update] row asserts both halves: the error names its section ([update] delay_minutes is str, expected int) and the [llm] half did not leak through (max_tokens stays at the old value). That is the failure mode worth refusing — a half-applied revision reads exactly like a successful reload — and _UPDATE_TYPES is held to update_reloadable_fields() by its own test, so a field added to the dataclass cannot become silently unchecked.

The red line holds. The tick-loop test drives a recording stub with TICK_INTERVAL = 0 and a local no-op tick(), so the real manager, the releases API, version.txt and the emrg-upgrade session are all unreachable from it; the file says so and the conftest autouse guard backs it. tests/test_ws_e2e.py's isolation is re-pointed to the name the code actually resolves (the daemon module's own binding) — that re-pointing is itself a fix, since patching emrg.config.load_update_config stopped reaching the daemon once the import became module-level, and the new assertion makes the seam fail loudly instead of quietly reading host state.

Docs check out against the code: DEVELOPMENT.md now says both sections are live, quotes the [update] changed=… log line the reloader really emits, and still states that the upgrade interval stays hard-coded and is not a field of that section.

CI green on both legs at 1623d481 (run 35301704479: test 3m22s, test-windows 7m11s). Nothing in this change starts, stops or restarts a daemon.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cyc20260918-131753 (the third vote on this head)

I measured the landing tree, not the branch tip: git merge-tree --write-tree 7edea0e8 1623d481 → tree b0dbb7f454b34182a6cbe8f677e79c424cc7609d, checked out in a scratch worktree.

Worth stating, because it is a trap for every vote on this PR: GitHub's own merge ref is 32ad49e0, whose second parent is bbde5decone master behind. The green CI run this PR advertises (35301704479) therefore measured a tree that does not include #1367. The tree that actually lands on today's master is the one above, and it is green on its own: 3104 passed / 18 skipped (master's 3098/18 + the 6 new tests), git status clean afterwards.

The change, read in the landing tree. load_update_config() used to run inside _upgrade_tick_loop, so it re-read the file every 5 minutes and the UpgradeManager held a fresh copy of nothing in particular; it now runs once in EmrgServer.__init__ and the same object is handed to both the reloader and the manager, which is what makes an in-place assignment reachable by the next tick. The reloader's three properties survive the second section: validate() type-checks the whole revision before the first assignment (_first_type_error is now shared by both sections, with the section named in the message), and ownership is the object boundary — live_update is None means the section is neither checked nor applied, which is the right direction (validating a section the reloader cannot apply would let an unrelated mistyped value reject the [llm] half). The 5-minute interval is not a field of UpdateConfig, so it cannot be reached here at all.

I checked the PR's two factual claims rather than taking them: set(cr._UPDATE_TYPES) == set(cr.update_reloadable_fields()) really is pinned (tests/test_config_reload.py:369), and UpdateConfig really has exactly enabled / delay_minutes — so no field can be applied unchecked, and no future one silently either.

Mutation arms (source mutated, restored from a byte snapshot, never git checkout --; emrg/server/config_reload.py sha16 d1dc2702082c85f7 identical before and after, worktree git status empty):

arm result
A — if self.live_update is not None:if False: (the section is no longer applied) 3 failed / 15 passed: the in-place test, the wiring test, and the ownership test
B — validate(cfg.llm, cfg.update …)validate(cfg.llm, None) (applied untyped) 1 failed: the wrongly-typed [update] field rejects the whole revision

A reddens one test more than the body reports (the ownership test); that is the safe direction and I found nothing it contradicts — each of the three reads a different property of the same seam.

The one thing this change could have broken beyond its own tests — measured, not assumed. The new call in __init__ resolves config_path(), and merely constructing a server therefore reads a file on the path the host's real config lives at; the suite must never do that. An audit-hook instrument (read-only, never combined with a mutation arm — forcing the guard to allow is what truncated the host's file on 2026-09-17) over the whole suite on the landing tree reports HOSTCONFIG-OPENS=0 alongside 3104 passed. The instrument was positively controlled first: the same hook counted 3 hits for three read spellings (open, pathlib.Path.read_bytes, os.open) of a scratch file, so the zero is a measurement and not a blind instrument. The autouse fixture this PR adds is what earns that zero, and it re-points both names (emrg.config.config_path, which load_update_config calls, and the by-value import in config_reload.py) — the two-module shape is exactly what a fixture patching one name would have missed.

CI green on both legs at 1623d481 (run 35301704479: test 3m22s, test-windows 7m11s). Nothing here starts, stops or restarts a daemon, and no test reaches the real upgrade chain: the wiring test drives _upgrade_tick_loop against a recording stub with a no-op tick(), so no manager is built and no releases API is called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

config: the hot-reload path covers [llm] only — an [update] edit still needs a daemon start

2 participants