fix(desktop): cascade persona respond_to edits to linked inheriting instances (#2501) - #4115
Conversation
…nstances (block#2501) A persona definition's "Who can talk to this agent" edit updated only the definition record; every already-minted agent instance kept its mint-time respond_to (owner-only by default), because the harness boots the gate from the instance record (build_respond_to_env) and no write path ever propagated behavior changes from the definition. Manual managed-agents.json edits worked, confirming the store was authoritative and the write path was the gap. update_persona_with now cascades behavioral-group edits to linked instance records, with the same discrimination rule the display_name cascade uses: - An instance whose respond_to still equals the PRE-edit definition value was inheriting → it adopts the new definition mode/allowlist/parallelism. - An instance carrying a different value holds an explicit instance-level pin → preserved untouched (parity with the pool-named-instance rule). - Every linked instance's definition mirror fields refresh either way, so future mint/inspect paths see the current definition bytes. Absent definition parallelism does not stomp an instance's pool width; unknown mode strings fail loudly rather than rewriting inheriting instances to a default the author didn't choose (same contract as mint). Adds 6 regression tests: inheriting cascade, pinned-override preservation, mixed fleet, definition-clear resets to default, unknown mode fail-loudly, and absent parallelism preservation. cargo test --lib: 2094 pass. Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
|
Nice — this is the cascade 1. No empty-allowlist guard on the inherit branch — it regenerates the The inherit branch copies the definition allowlist verbatim: record.respond_to = match persona.respond_to.as_deref() {
Some(wire) => RespondTo::parse_wire(wire)?,
None => RespondTo::default(),
};
record.respond_to_allowlist = persona.respond_to_allowlist.clone();
2. The inheritance discriminator is mode-only, so it clobbers a same-mode allowlist pin. if record.respond_to == old_mode {
3. Scope / coordination with #2505. This is backend-only, so defect 1 (person-picker never persisting Happy to build and run an end-to-end regen check (owner-only → allowlist-with-members, mixed inheriting/pinned fleet, restart + re-mint) once the empty-allowlist skip is in. |
…e-mode pins Both asks from review on block#4115: 1. Mirror the mint guard: a definition sitting in allowlist with an empty allowlist is reachable today (person-picker writes the mode without the principals — block#2501 defect 1), and resolve_mint_behavioral_defaults / apply_persona_behavior both reject that state. The cascade now skips adoption for such a definition (mirrors still refresh) instead of manufacturing a record neither mint path would produce. Skip, not fail. 2. The allowlist joins the inheritance discriminant: mode alone reads a same-mode allowlist pin (exactly what the per-instance EditRespondToDialog workaround writes) as still-inheriting and clobbers it. Comparison and adoption both use the EFFECTIVE allowlist (empty for non-allowlist modes, apply_persona_behavior's asymmetry) — writing the raw definition list would break the next edit's detection the same way. Also hoists the wire parse above the loop so a bogus definition mode cannot half-apply mirror refreshes. 9 behavior_cascade tests pass (6 existing + 3 new). Signed-off-by: iroiro147 <sarthak.singh@mastersunion.org>
|
@georgerous both asks are in, pushed as
Also hoisted the wire parse above the loop, so a bogus mode can no longer half-apply mirror refreshes. Three new tests pin all of it: On your scope note: agreed #2501 defect 1 (person-picker never persisting the allowlist) stays open regardless — I've already downgraded #4270 from |
|
Independent verification + two observations. I hit exactly #2501 in production this week (Desktop v0.5.14, macOS Apple Silicon, self-hosted relay from Verification against current
Two things I noticed reading the diff against my production store, for whatever they're worth: 1. Adopted records don't republish their retained kind:30177 projection. 2. Already-desynced installs never heal through this cascade. Every instance minted before this fix, on a definition that was already edited during the buggy era, fails the inheriting test forever. Concrete bytes from my store: definition (Investigation done with Claude Code against the live store + harness logs; test runs above are from a clean worktree.) |
…s order-insensitively Both nits from @georgerous's review on block#4115: 1. The Allowlist+[] skip wrapped the whole adoption, so a parallelism-only edit stopped reaching inheriting instances even though behavior_changed fires on it. Freezing an instance's pool width is a separate, unasked-for change; the skip now covers only the respond_to gate, which is the part that is actually unsafe. 2. The inheritance test compared allowlists positionally, so an instance holding the same principals in a different order read as a deliberate pin and was stranded on the old gate. same_allowlist() sorts before comparing -- sorted rather than set-wise so a genuine duplicate still counts as a difference. 12 behavior_cascade tests pass (9 existing + 3 new). Signed-off-by: iroiro147 <sarthak.singh@mastersunion.org>
Both observations from @xtranger51's independent verification on block#4115: 1. The behavioral triple is part of the published kind:30177 projection (agent_event_content), so a cascade left the relay's retained record stale until the next boot reconcile. propagate_persona_behavior now reports which records actually adopted, and update_persona_with retains exactly those -- the same contract as the rename cascade (block#2423). Mirror-only refreshes are excluded: the projection is unchanged, so retaining would be a guaranteed no-op. 2. An instance minted before this cascade existed, against a definition already edited during the buggy era, matches neither the old mode nor the old allowlist, so it read as a deliberate pin and stayed desynced forever. The mirror fields are written only by this cascade, so an unset definition_respond_to marks a pre-fix record; combined with a gate still at the mint default that is inheritance, not a pin. A pre-fix record with a non-default gate is still preserved. The marker is captured before the mirror refresh overwrites it -- doing it after silently disabled the heal, which the test caught. 15 behavior_cascade tests pass; full desktop lib suite 2103 passed, 0 failed; clippy --all-targets -D warnings clean. Signed-off-by: iroiro147 <sarthak.singh@mastersunion.org>
|
@xtranger51 Thank you — testing this instead of opening a duplicate was the generous call, and both observations are right. Both are implemented in 1. Retained kind:30177 projection. Correct, and the precedent you point at is the right one. 2. Already-desynced installs. This is the more important of the two — without it the fix does nothing for the population that already has the bug, which the #2501 thread suggests is most of it. Implemented as you proposed: unset I also added the negative case, because the heal is the riskiest thing in this PR: a pre-fix record whose gate is not the mint default stays pinned. Your reasoning for why that is safe is what convinced me — before this fix the dialog could not write an instance-level gate at all (defect 1 of #2501), so a default gate on a pre-fix record cannot be a deliberate choice. Worth stating plainly as the residual risk: if some path I have not found could write One implementation trap worth recording, since it would be easy to repeat: the marker has to be read before the mirror refresh in the same loop iteration, because Verification: 15 Also, following @georgerous's note about two PRs on one write path: this PR is now Your offer to test a follow-up against your repro is worth more than anything I can assert from here, and the heal is exactly the part that deserves it. If it misbehaves against a real desynced store I would rather hear it now than after a merge. |
|
@xtranger51 Flagging this here because it's about your observation 2, and I originally wrote it on #4270 where you may not have seen it. Real-store bytes beat anything either of us can assert from reading, so I'd value your take. Your diagnosis is right and worth fixing — a pre-fix desynced instance does fail the inheriting test forever. My concern is the marker The safety argument for Two consequences, opposite directions: False positive, the one that worries me. #4270 (the other half of #2501) lands a dialog that reliably writes the instance-level gate, and its transition matrix includes False negative, affecting your coverage estimate. Records created through Your observation 1 and the retention scoping look unaffected by any of this, for what it's worth — excluding mirror-only refreshes from the retain is right. Two things your store could settle that our reading can't:
The direction I suggested on #4270 was to make the marker mean what it's asserted to mean — stamp the mirror triple at mint in |
The desync heal's safety premise was 'unset mirrors means the record predates the cascade'. It did not: create_managed_agent minted every record with definition_respond_to: None, so unset mirrors meant only 'no cascade has touched this yet' -- the state of every fresh mint. Once the instance-gate dialog (block#4270) can pin a fresh instance to owner-only, that pin matched the heal's pre-fix shape and the next persona behavior edit silently un-pinned it (georgerous, review on block#4270). Mint now stamps the mirror triple from the linked definition, so an unset mirror unambiguously means pre-fix and the heal stops firing for records that never needed it. A mint under a persona with no explicit mode still leaves the mirror None, and the heal is harmless there: owner-only + [] IS the inherited value under such a persona, so a pin to it is indistinguishable from inheritance even in principle. 16 behavior_cascade tests; full desktop lib suite 2104 passed; clippy -D warnings clean. Signed-off-by: iroiro147 <sarthak.singh@mastersunion.org>
@georgerous's point on block#4270: the heal is safe only while no path links a record to a definition carrying an explicit non-default respond_to without stamping the mirror triple. Mint stamps; migration/backfill.rs manufactures the definition from the record's own config so the dangerous shape cannot arise there. A future linker that skips the stamp would reopen the un-pinning bug with nothing to catch it, so the requirement is stated where a new linker's author will read it. Signed-off-by: iroiro147 <sarthak.singh@mastersunion.org>
|
@xtranger51 — your offer to test the heal against your real desynced store still open? @georgerous made the point on #4270 that a genuine pre-fix install is the one thing neither of us can synthesise, and I agree: my fixture is your reported bytes, which means it proves my code matches my reading of your report, not that either matches your disk. One thing changed since you reported, and it changes what to look for. In That makes your store the interesting case, because it cuts both ways:
Either result is useful — if (1) fails, the fix does not reach the population it was written for. No pressure and no timeline; the PR is not blocked on it. If you would rather just paste the raw |
Summary
respond_to/respond_to_allowlist/parallelismedits on a persona definition never reached linked instances: the harness bootsrespond_tofrom the instance record (build_respond_to_env), so every running agent silently stayed at its mint-time mode — owner-only by default — whatever the persona said (defect 2 of #2501).update_persona_withnow cascades behavior edits to linked instance records, mirroringpropagate_persona_name_rename.How an instance is judged inheriting vs pinned (shaped by review, see thread):
apply_persona_behavior's storage asymmetry.allowlist+[](reachable via the person-picker today; the crash-shape the mint path rejects) skips only therespond_togate adoption; parallelism still cascades, and mirror fields still refresh.create_managed_agent— without that, every fresh mint matched the pre-fix shape and a dialog pin could be silently un-pinned after fix(desktop): send complete respond_to wire patch on every allowlist transition #4270 lands (@georgerous's catch).agent_event_content), same contract as the rename cascade ([Bug] Renaming or re-adding personal agents can desynchronise identity and break @mentions #2423). Mirror-only refreshes do not retain.parse_wireis hoisted above the loop so a bad mode cannot half-apply.Known scope limits: records created via snapshot import or team import set mirrors at creation, so the heal does not fire for them however desynced; the healed population is pre-fix mints only.
Sibling PR: #4270 fixes the dialog's wire patch (defect 1). No file overlap — this PR is Rust
src-tauri/commands/personas/, that one TypeScriptfeatures/agents/ui/. Neither carries a closing keyword; #2501 should be closed by hand when defect 1 lands.Tests
update/behavior_cascade_tests.rs— inheritance, pins (explicit, same-mode-allowlist, reordered-allowlist, duplicate-principal, stamped-mirror), the empty-allowlist skip, parallelism-through-unsafe-state, the pre-fix heal and its negative, adopted-set retention scoping, and fail-loudly on a bad mode.cargo clippy --workspace --all-targets -- -D warningsclean;pnpm typecheckclean.Refs #2501