Skip to content

Re-discover a profile's models when its harness home is edited - #1472

Merged
foundev merged 31 commits into
masterfrom
agent/profile-home-watch
Nov 7, 2026
Merged

foundev merged 31 commits into
masterfrom
agent/profile-home-watch

Conversation

@foundev

@foundev foundev commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #1337.

What

Editing config.toml, models.json, or settings.json in a profile's harness home never reached the catalog, so mj models, the Settings and Move pickers, and automatic reviewer selection kept the pre-edit answer for the daemon's whole life. 55b8ea778 fixed this by hashing home files into the discovery key; b39c0e69c removed that hash to keep read paths free of home-file I/O and one global generation — and nothing since carried a home edit to the catalog.

How

The catalog owner now watches each distinct profile home from its background task:

  • A 30-second stat poll of the three catalog inputs per home, hashing contents only when a stat changes; a file modified within the last minute is re-hashed even on an unchanged stat, catching same-length rewrites inside one mtime tick on coarse-granularity filesystems.
  • Every probe reads the home's inputs before staging and, on success only, records that digest against its entry. The watch compares each served entry's recorded digest against the current files and re-probes mismatches through the keep-serving-the-old-answer refresh path, so a failed re-probe never blanks the pickers and a probe that read pre-edit files records the pre-edit digest and stays pending.
  • Re-probes bypass the process-global cache, which would otherwise answer a deliberate re-probe with the stale row it exists to replace. A cache-served publish settles nothing.
  • Failed re-probes arm a retry window against the digest they were correcting: an unchanged home retries at a five-minute cadence like the expiry refresh, while a fresh edit's new digest bypasses the window and re-probes on the next pass.

One owner decides; catalog reads still do no home-file I/O; profiles with other homes are untouched; a home shared by several profiles re-probes each of them; there is no TTL. Edits made while the daemon is stopped are picked up at start because entries and the process cache live only as long as the daemon.

Tests

harness_home_files_do_not_invalidate_configured_capabilities is replaced by tests for the issue's acceptance rules, plus one per review finding (15 rounds): edited home re-publishes within one pass and only that home re-probes; shared homes re-prime each profile; a new catalog generation discovers the current home; in-flight and lagging probes cannot settle an edit; failed re-probes retry bounded and adopt the failing digest; fresh edits bypass failure windows; pending and failed entries do not force re-probes of healthy siblings; stat-hidden (cp -p) rewrites converge once a probe reads the home; same-size rewrites inside one mtime tick are caught by the recency re-hash.

Validation

Workspace cargo test green (controller suite ~2,360 tests exit 0), cargo clippy --all-targets -- -D warnings clean, cargo fmt --all -- --check clean; watch tests stable across repeated parallel runs. Reviewed clean by the project review flow.

Plan: .agents/plans/profile-home-refresh-watch.md.

Issue 1337: editing config.toml, models.json or settings.json in a profile
home never reached the catalog, so mj models, the Settings and Move pickers
and auto reviewer selection kept the pre-edit answer for the daemon's whole
life. 55b8ea7 fixed this by hashing home files into the discovery key;
b39c0e6 removed that hash to stop read-path home I/O and one global
generation, and nothing since carried a home edit to the catalog.

The catalog owner now watches each distinct profile home from its background
task: a 30-second stat poll of the three catalog inputs, hashing contents
only when a stat changes. A home whose inputs hash differently re-probes
exactly the profiles that use that home, through the keep-serving-the-old-
answer refresh path. Re-probes bypass the process-global cache, which would
otherwise answer a deliberate re-probe with the stale row it exists to
replace. The first pass only baselines (also the restart case: entries and
the process cache live only as long as the daemon, so a stopped-then-started
daemon discovers the current home), and a detected change settles only once
re-probes launched for it and nothing selected for it is still re-probing,
so an in-flight probe that staged the pre-edit home cannot re-introduce the
old state. One owner decides; reads still do no home-file I/O; profiles with
other homes are untouched; no TTL.

harness_home_files_do_not_invalidate_configured_capabilities is replaced by
tests for these rules: an edited home re-publishes within one pass and
re-probes only that home, a shared home re-primes each of its profiles, a
new catalog generation discovers the current home, and metadata-only
rewrites do not register as edits.

Validation: cargo test -p brokk-mj-controller profile_catalog (34 passed)
and profile_config (12 passed); fmt clean.
Review of the home watch found the launch-gated baseline could settle an
edit on a failed re-probe, leaving it silent until the one-hour expiry, and
could not see probes still pending that staged the pre-edit home: the
first-sight window after daemon start, and a sibling profile of a shared
home whose own probe had not finished. Both re-introduced the stale answer
the watch existed to catch.

Every attempt now reads the home's catalog inputs before staging and, on
success only, records that digest per identity. The watch compares each
identity's settled digest against the current files, so a probe that read
pre-edit files records the pre-edit digest, a failed probe records nothing,
and a pending probe records nothing yet: all three stay pending for the
next 30-second pass instead of settling wrongly. Failed re-probes retry
after five minutes, matching the expiry refresh cadence, rather than every
pass. An input modified within the last minute is re-hashed every pass even
when its stat matches, catching a same-length rewrite hidden inside one
mtime tick on a coarse-granularity filesystem.

Adds the failure-retry and fixed-mtime-rewrite regressions; the in-flight
regression now pins the recording rule directly.

Validation: cargo test -p brokk-mj-controller profile_catalog (36 passed)
and profile_config (12 passed), repeated runs stable; cargo clippy
--all-targets -- -D warnings clean.
Review round 2: the watch armed home_edit_retry_wait when a re-probe
launched and discharged it only when a later pass observed convergence. A
successful re-probe followed by another home edit within the window served
the second edit up to five minutes late — the very staleness this watch
exists to catch. A success now clears the window alongside recording the
settled digest, so a genuinely new edit re-probes on the next pass; a
failure still records nothing and keeps the bounded retry.

Pinned by a_follow_up_edit_reprimes_without_waiting_for_the_retry_window.
Validation: cargo test -p brokk-mj-controller profile_catalog (37 passed)
and profile_config (12 passed).
Review round 3: home_settled was one digest per identity, last-success-wins,
so when a detection pass re-probed an identity's entries and they diverged —
the default entry's probe succeeding while a model entry's failed — the
success settled the whole identity and the failed entry's pre-edit copy
stayed served until the one-hour expiry refresh. The settled digest is now
keyed by the entry, and the watch requires every served entry of an identity
to record the current digest; claim_models stamps the digest of the probe it
copies the current-model entry from, so a copied entry settles with its
source.

Pinned by a_failed_entry_reprimes_even_when_its_sibling_reprobe_succeeded.
Validation: cargo test -p brokk-mj-controller profile_catalog (38 passed)
and profile_config (12 passed), repeated runs stable; clippy clean.
Review round 4: a failed entry's backoff retry and reread_failed read the
process-global cache (refresh = false), so a row stored before a home edit
could be published with the home's post-edit digest recorded as settled.
The watch then saw every entry settled at the current digest and stayed
quiet for up to the choice lifetime — the edit-not-picked-up defect again.

Discovery now reports whether the harness actually ran. The catalog records
a settled digest only for a probe that read the home; a cache-served answer
settles nothing, so the entry keeps its older digest, stays stale, and the
watch's next pass launches the cache-bypassing re-probe that reads the
edited home. Muse probes always read the installation and always settle.

Pinned by resolve_cached_discovery_reports_whether_the_harness_ran.
Validation: cargo test -p brokk-mj-controller profile_catalog (38 passed)
and profile_config (13 passed); clippy clean.
Review round 5: the every-entry settle rule counted Entry::Failed, which
never records a settled digest, so one permanently failed entry kept its
identity stale forever and re-probed the healthy entries every pass, about
two harness launches per 30 seconds. A failed entry serves no home-derived
answer; its own supervisor retries it with backoff, and its success records
a digest that rejoins the check. Failed entries therefore neither settle
nor block convergence. Re-probe failures that keep an entry Ready still
hold the identity pending, as before.

Pinned by a_failed_entry_does_not_keep_repriming_its_healthy_siblings.
Validation: cargo test -p brokk-mj-controller profile_catalog (39 passed)
and profile_config (13 passed), repeated runs stable; clippy clean.
Review round 6: any success of the identity discharged the throttle, so a
persistently failing entry mixed with healthy siblings re-probed the whole
identity on every 30-second pass — the throttle never bound it. A failed
drive_refresh now re-arms the window, and only a success discharges it, so a
mixed identity retries at the five-minute cadence like any other persistent
failure while a genuinely converged one re-probes immediately on a new edit.

The sibling regression now pins both halves: the window holds right after a
failure, and the failed entry still re-probes after it.

Validation: cargo test -p brokk-mj-controller profile_catalog (39 passed)
and profile_config (13 passed), three repeated runs stable; clippy clean.
Review round 7: arming the retry window on failure and discharging it on
any success raced between a failing entry and a succeeding sibling — when
the sibling completed last, the window was cleared before the next pass and
the identity re-probed every entry every 30 seconds. A success now
discharges the window only when every non-Failed entry of the identity
settles at an identical digest; a still-failing sibling keeps its older
digest, so the five-minute bound holds whatever order the probes complete
in. The home watch additionally clears the window for identities it
observes converged, and failures keep re-arming it.

Validation: cargo test -p brokk-mj-controller profile_catalog (39 passed)
and profile_config (13 passed), three repeated runs stable; clippy clean.
Review round 8: a timestamp-preserving rewrite (cp -p) hides from the stat
poll, which keeps reusing its old content hash, while any probe reads the
home in full and records the fresh digest. The watch then saw settled and
polled digests disagree on every pass and re-probed the identity every
30-60 seconds without a bound — the re-probe storm class #1028 removed —
because the file's stat never changed and the poll could never catch up.

The watch now re-hashes a home in full when the poll saw no change yet a
settled digest disagrees: the poll is provably behind, since a probe's full
read and the poll's hash can only differ if the content changed after the
hash was taken. One extra read ends the churn, and the documented cp -p
residual — undetected until something reads the home — now converges
quietly instead of looping.

Pinned by a_stat_hidden_rewrite_converges_once_a_probe_reads_it.
Validation: cargo test -p brokk-mj-controller profile_catalog (40 passed)
and profile_config (13 passed), three repeated runs stable; clippy clean.
Review round 9: two P3 defects in the settle rule and the retry window.

A still-Pending entry — like a Failed one — serves no complete answer, yet
the settle check required it to carry the current digest, which no pending
probe can record. One slow harness launch left the identity stale
permanently, re-probing every healthy entry every retry window. Pending
entries are now excluded from the settle check; their supervisor's success
records a digest that rejoins it.

The retry window was digest-blind: after a failed re-probe of one edit, a
fresh edit waited out the window, up to five minutes, before re-probing.
Launches and failures now arm the window against the digest they retry,
and a pass whose current digest differs from the armed one is a new edit
and re-probes on the next pass; the window still bounds retries of the
same failing digest.

Pinned by a_pending_entry_does_not_keep_repriming_its_healthy_siblings and
a_fresh_edit_bypasses_the_retry_window_of_an_earlier_failure.

Validation: cargo test -p brokk-mj-controller profile_catalog (42 passed)
and profile_config (13 passed), three repeated runs stable; clippy clean.
Review round 10: a failed refresh with no prior window — the routine
expiry refresh is one — armed the window with an "any digest" marker, so a
fresh home edit inside the window was suppressed for up to five minutes
instead of re-probing on the next pass. Such a failure now arms the window
with the failing entry's settled digest, the state the retry was
correcting: an unchanged home retries at the bound, and a pass whose
current digest differs is a fresh edit and bypasses, the same rule a
watch-launched window already followed.

Pinned by a_fresh_edit_bypasses_the_window_of_an_unrelated_failure.
Validation: cargo test -p brokk-mj-controller profile_catalog (43 passed)
and profile_config (13 passed), three repeated runs stable; clippy clean.
Review round 11: a Ready entry published from a cache-served discovery has
no settled digest, so a failure could still arm the window with an "any
digest" marker, holding a fresh edit — and each re-armed failure extended
the hold. The failed attempt now reports the digest it read before
running, the home state the failure is about, and the window arms against
that — falling back to the entry's settled digest, then to the marker only
when no state is known at all. A pass whose current digest differs remains
a fresh edit and bypasses the window in every case the fixtures can reach.

Validation: cargo test -p brokk-mj-controller profile_catalog (43 passed)
and profile_config (13 passed), three repeated runs stable; clippy clean.
Review round 12: a refresh failure whose attempt could not hash the home —
a hard read error, or a profile no longer enabled — and an entry a
cache-served publish never settled could still arm the window with the
"any digest" marker, holding a fresh edit until the window lapsed. With no
known state, any current digest could be a fresh edit's, so the window now
stays unarmed rather than suppress what it cannot characterize. An existing
concrete window still extends on later failures, so retries of a known
state stay bounded.

Validation: cargo test -p brokk-mj-controller profile_catalog (43 passed)
and profile_config (13 passed); clippy clean.
Review round 13: the watch re-armed the window for every identity whose
entries launched, against the pass's digest, before the probes' outcomes
were known. When a home edit landed while one entry's probe was already in
flight, the pass launched only the sibling and armed the window anyway; the
sibling succeeded and the lagging entry — which never failed — then waited
out the window before re-probing, up to five minutes stale.

Arming at launch was redundant: the refreshing map already keeps launched
probes from doubling, and a failure re-arms with the digest it was
correcting. Removing it leaves failures bounded, fresh edits bypassing,
and a lagging sibling re-probing on the next pass.

Pinned by a_lagging_entry_reprimes_on_the_next_pass_after_its_sibling_launched.
Validation: cargo test -p brokk-mj-controller profile_catalog (44 passed)
and profile_config (13 passed); clippy clean.
Review round 14: with launch-arming gone, extending the window kept the
old armed digest, so after a fresh edit bypassed the window and its
re-probe failed, the edited state matched no window and re-probed every
30-second pass for as long as the failure persisted. A failure now also
adopts the digest its probe read, so the window retries the state that
just failed: an unchanged home waits the bound, and only a newer edit
bypasses.

The unrelated-failure regression now pins the whole cycle: bypass, bounded
retry of the failed fresh edit, and convergence once the failure lifts.

Validation: cargo test -p brokk-mj-controller profile_catalog (44 passed)
and profile_config (13 passed); clippy clean.
@foundev
foundev merged commit a8cc92b into master Nov 7, 2026
13 checks passed
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.

Editing a profile's harness home does not refresh its discovered models (regression of #1028)

1 participant