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>
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