Skip to content

Commit bea36ae

Browse files
authored
docs(catalog): add IP-037 a retired setting is not an absent setting (#5043)
1 parent f89de1f commit bea36ae

1 file changed

Lines changed: 141 additions & 1 deletion

File tree

‎docs/concepts/interaction-pattern-catalog.md‎

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Map P0/P1 catalog rows to canary archetypes before picking commands:
8888
| --- | --- | --- | --- | --- | --- |
8989
| Work Routing | IP-001, IP-002, IP-003, IP-007, IP-008, IP-021, IP-029 | Hot-path route canary; Planning governance canary when cadence or repair is involved | `quota should-run`, `interaction_contract`, `work_lane_contract`, scheduler hint, handoff todo state | one eligible delivery fixture, one blocked/fallback fixture, one quiet or monitor fixture | agent turn routing is unsafe: it may spend, wait, notify, or choose fallback incorrectly |
9090
| Human Decision | IP-004, IP-014, IP-017, IP-027, IP-030, IP-033 | Scoped decision canary; Product/readiness canary when first-screen human copy changes | user todos, decision scope, operator-gate/reward preview, deferred resume candidates | one concrete user ask, one scoped non-blocking gate, one preview-or-append dry run | humans may be asked the wrong question, or an agent may continue without the needed decision |
91-
| State And Boundary | IP-005, IP-006, IP-011, IP-016, IP-019, IP-020, IP-022, IP-023, IP-025, IP-026, IP-028, IP-031, IP-032, IP-035, IP-036 | Projection and boundary canary; Hot-path route canary when the projection feeds quota/status | active state, todo metadata, task graph, authority source, claim lease, completed-work archive, install ownership, connector runtime policy, operation receipt, public/private scan | fixture state plus structured projection check; boundary scan for touched public files | compact state and executable truth diverge, so dashboards and agents may trust stale or unsafe authority |
91+
| State And Boundary | IP-005, IP-006, IP-011, IP-016, IP-019, IP-020, IP-022, IP-023, IP-025, IP-026, IP-028, IP-031, IP-032, IP-035, IP-036, IP-037 | Projection and boundary canary; Hot-path route canary when the projection feeds quota/status | active state, todo metadata, task graph, authority source, claim lease, completed-work archive, install ownership, connector runtime policy, operation receipt, retired setting projection, public/private scan | fixture state plus structured projection check; boundary scan for touched public files | compact state and executable truth diverge, so dashboards and agents may trust stale or unsafe authority |
9292
| Evidence Lifecycle | IP-012, IP-015 | Evidence lifecycle canary; Product/readiness canary when evidence is rendered | external handle observation, benchmark lifecycle reducer, compact result projection | compact public-safe evidence fixture with raw-material exclusion assertions | progress evidence may be missing, double-counted, or represented with unsafe raw material |
9393
| Planning Governance | IP-010, IP-013, IP-018, IP-024, IP-034 | Planning governance canary; Hot-path route canary when cadence changes affect execution | stalled run history, autonomous replan obligation, repair delta, cadence hint, plan-to-todo writeback | two-turn stalled fixture plus repair/writeback delta assertion | the agent may keep planning in prose while the machine-visible frontier stays unchanged |
9494

@@ -345,6 +345,7 @@ Projection, authority, write scope, and lease integrity.
345345
| P1 | IP-032 | Completed Work Archive With Durable Decision Retention | Archive selector plus controller | no interruption; preview-then-execute readback | treat archived done work as history, keep durable decisions authoritative, and never move another role's lane |
346346
| P1 | IP-035 | Install Ownership Is Not An Update Permission | Install lifecycle owner plus user | no silent mutation; report the owning installer and its command | classify the install before mutating it; when LoopX does not own it, hand back the owner-owned command instead of switching install channels |
347347
| P1 | IP-036 | A Lost Response Is Not An Absent Commit | Effect dispatcher plus caller | no interruption unless recovery needs a user decision; report the receipt read back | name the write with a stable operation id, recover by readback instead of blind retry, and never leave a committed record pointing at material nobody published |
348+
| P1 | IP-037 | A Retired Setting Is Not An Absent Setting | Configuration reader plus migration owner | no interruption; keep the retired entry visible and read-only where it was once configurable | reject the retired activation before any write, carry its reason in the projection, and treat clearing it as neither enable nor bootstrap of the replacement |
348349

349350
### Evidence Lifecycle
350351

@@ -2620,6 +2621,145 @@ flowchart TD
26202621
recovery path reuses it instead of inventing a fourth answer.
26212622
- `examples/interaction-pattern-catalog-smoke.py` protects this entry.
26222623

2624+
#### IP-037 A Retired Setting Is Not An Absent Setting
2625+
2626+
**Trigger**
2627+
2628+
- a Goal, registry entry, or settings document still names a configuration
2629+
whose implementation has been retired, so a reader must decide whether that
2630+
setting is off, missing, or still writable;
2631+
- the caller is about to re-enable it, clear it, or migrate state that mentions
2632+
it, and the cheapest wrong move is to treat "no longer supported" as "never
2633+
existed";
2634+
- the signals that say this already happened are typed, not inferred from
2635+
prose: a summary carrying `configured: true` with
2636+
`status: "retired"` and `enabled: false`, a migration row with
2637+
`attempted: false` and `outcome: "retired"`, a
2638+
`request_rejected / local_authority_shadow_retired` reply, or a
2639+
`retired corpus requires lifecycle.retirement_reason` rejection.
2640+
2641+
**Expected behavior**
2642+
2643+
Retiring a capability removes what it may write, not what it says. Four rules
2644+
keep "we stopped supporting this" from becoming "this was never configured".
2645+
2646+
1. **Keep the retired setting in the projection.** The summary of a Goal that
2647+
still carries the old key stays present and self-describing rather than
2648+
dropping the field: `local_authority_shadow_summary` returns
2649+
`{"enabled": False, ..., "status": "retired" if valid else "invalid",
2650+
"configured": True, ...}`
2651+
(`loopx/control_plane/coordination/runtime_shadow.py:53-63`), so a malformed
2652+
setting reads as `invalid` and a retained one reads as `retired` — neither
2653+
collapses into "unconfigured". Historical records under the old path remain
2654+
readable and are labelled instead of being relabelled as promotion evidence:
2655+
`local_authority_shadow_adapter.py:784` computes `legacy_observation` and
2656+
`:805` stamps each candidate store as `legacy_observation` or
2657+
`runtime_shadow`.
2658+
2. **Reject re-enabling before any write, and reject it by code.** The gate sits
2659+
ahead of the mutation, not inside it:
2660+
`validate_coordination_shadow_changes` is documented as "Reject retired
2661+
activation before any registry mutation"
2662+
(`loopx/control_plane/coordination/runtime_shadow.py:103-115`) and carries the
2663+
reason in its message (`:67-78`, text at `:72`). The old runtime RPC keeps
2664+
its address and answers `local_authority_shadow_retired`
2665+
(`loopx/control_plane/coordination/local_authority_shadow.ts:57`) rather than
2666+
disappearing into a transport error, because a rejection that looks like a
2667+
transient failure invites a retry loop.
2668+
3. **Clearing is neither enable nor bootstrap.** One setting is retired; the
2669+
replacement capture path is configured on its own terms.
2670+
`apply_coordination_shadow_changes` is documented as "Clear retired settings
2671+
and configure the transaction-bound shadow independently"
2672+
(`loopx/control_plane/coordination/runtime_shadow.py:117-131`), and
2673+
`tests/control_plane/test_local_authority_shadow_config.py:58` pins that
2674+
clearing preserves the runtime configuration and peer registration instead of
2675+
silently starting the new capture.
2676+
4. **Migration reports the retirement rather than seeding it.** `migrate-state`
2677+
keeps the response field callers already parse and answers it with a
2678+
non-action: `retired_authority_shadow_notices` emits
2679+
`{"attempted": False, "outcome": "retired", "reason_code":
2680+
"local_authority_shadow_retired"}`
2681+
(`loopx/state_migration.py:236-241`), is still the call site's source of that
2682+
column (`:395`), and renders `attempted=` in the operator table (`:460`). The
2683+
same rule holds where retirement is a lifecycle state instead of a deletion:
2684+
a corpus may not claim `state: "retired"` without a reason
2685+
(`loopx/capabilities/reward_memory/registry.py:156-157`).
2686+
2687+
IP-030 owns applying a machine configuration under a revision guard, which
2688+
presumes the setting still has an apply path; this is the case where the apply
2689+
path is gone and the setting remains. IP-032 keeps durable decisions
2690+
authoritative across an archive of completed work, and IP-033 reads a recorded
2691+
rejection as a decision that is present — both are about what stays *writable
2692+
or countable* after a state change, while this pattern is about what stays
2693+
*legible* after a capability change. IP-036 is its transport-side twin: a lost
2694+
response must not be read as an absent commit, and a retired path must not be
2695+
read as an absent setting. IP-006 owns a projected write scope that disagrees
2696+
with its checkpoint; here the projection is honest about being inert, and the
2697+
danger is a reader inferring a write from silence.
2698+
2699+
**Visual Model**
2700+
2701+
```mermaid
2702+
flowchart TD
2703+
A["reader finds a setting whose path is retired"] --> B{"is the setting well-formed?"}
2704+
B -->|"malformed"| C["status=invalid, configured=true; offer clear, never enable"]
2705+
B -->|"retained"| D["status=retired, enabled=false, configured=true"]
2706+
D --> E{"what does the caller want?"}
2707+
E -->|"enable the old path"| F["reject by code before any registry write"]
2708+
E -->|"clear it"| G["clear only this key; replacement stays as configured"]
2709+
E -->|"migrate state"| H["report attempted=false, outcome=retired; do not seed"]
2710+
E -->|"read history"| I["label legacy records as legacy; never as promotion proof"]
2711+
F --> J["operator doc names the replacement path"]
2712+
G --> J
2713+
H --> J
2714+
```
2715+
2716+
| Situation | What must be visible | What must not happen |
2717+
| --- | --- | --- |
2718+
| Retained old key | `configured=true`, `enabled=false`, `status=retired` | field dropped, or read as unconfigured |
2719+
| Re-enable attempt | typed rejection naming the reason code | registry or store write, or a retryable transport error |
2720+
| Clear request | this key removed, replacement untouched | implicit bootstrap of the new capture |
2721+
| Migration row | `attempted=false`, `outcome=retired` | seeding a second observation store |
2722+
| Historical read | records labelled `legacy_observation` | counted as promotion evidence |
2723+
2724+
**Bad smell**
2725+
2726+
- the setting vanishes from status or the settings surface, so an operator
2727+
reading an old Goal cannot tell whether they ever opted in, and the history
2728+
under it loses its explanation;
2729+
- a retired enable flag that "does nothing" instead of rejecting, so a script or
2730+
a stale client believes it turned capture on;
2731+
- clearing a retired key as a side door that boots the replacement capture, or
2732+
a migration that re-seeds the retired observer because the response field is
2733+
still expected;
2734+
- retrying `local_authority_shadow_retired` as a transient storage failure;
2735+
- promoting a historical `legacy_observation` record into evidence for the
2736+
transaction-bound lineage because both rows live in one directory;
2737+
- a retirement that ships as a deletion with no reason recorded, so the next
2738+
reader reinstates it as a bug fix.
2739+
2740+
**Validation**
2741+
2742+
- `tests/control_plane/test_local_authority_shadow_cli_e2e.py` runs the real CLI
2743+
upgrade journey: `:10` proves a retained retired setting can neither enable
2744+
capture nor satisfy a bootstrap, `:20` asserts the rejection names
2745+
`local_authority_shadow_retired`, and `:25` asserts status still reads
2746+
`retired`.
2747+
- `tests/control_plane/test_local_authority_shadow_config.py` pins both halves:
2748+
`:49` a retired enable rejects without rewriting the registry (dry-run and
2749+
execute), `:58` clearing preserves runtime configuration and peer
2750+
registration.
2751+
- `tests/control_plane/test_state_migration_authority_shadow.py` keeps
2752+
`migrate-state` reporting retirement instead of seeding it, and
2753+
`tests/control_plane_ts/local_authority_shadow.test.ts` keeps the old RPC
2754+
answer typed.
2755+
- `tests/control_plane/test_coordination_runtime_shadow_adapter.py:637` proves a
2756+
retired CLI observer cannot overwrite transaction evidence, which is what
2757+
makes the historical read safe rather than merely available.
2758+
- `docs/reference/authority-observation-retirement.md` owns the operator
2759+
transition and rollback wording; `#5011` records this as the answer to the
2760+
shared-authority RFC's open question on keeping one capture boundary.
2761+
- `examples/interaction-pattern-catalog-smoke.py` protects this entry.
2762+
26232763
### Evidence Lifecycle
26242764

26252765
#### IP-012 External Evidence Observation

0 commit comments

Comments
 (0)