Skip to content

Issue #3528: [UserMemory] 类别级配额与保留策略(在全局上限内按 category 细分 eviction) - #3539

Open
eanz17 wants to merge 1 commit into
feat/2026-08-25_issue-3527from
feat/2026-08-25_issue-3528
Open

Issue #3528: [UserMemory] 类别级配额与保留策略(在全局上限内按 category 细分 eviction)#3539
eanz17 wants to merge 1 commit into
feat/2026-08-25_issue-3527from
feat/2026-08-25_issue-3528

Conversation

@eanz17

@eanz17 eanz17 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Issue

Closes #3528 — [UserMemory] 类别级配额与保留策略(在全局上限内按 category 细分 eviction)

Implementation summary

See .implement-loop/runs/implement-issue-3528.md.

Stacked-PR position

  • Base: feat/2026-08-25_issue-3527 (previous issue's branch)
  • Head: feat/2026-08-25_issue-3528
  • Auto-loop iteration: implement-loop / milestone Typed Context & Deterministic Computation (v1)

🤖 Generated by codex-implement-loop. Reviewer is a Claude subagent (see PR comments for round-N review reports).

Implemented per .implement-loop/runs/implement-issue-3528.md.

Closes #3528

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@eanz17

eanz17 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Review of PR #3539 — round 1

Verdict: pass
Issue: #3528 — [UserMemory] 类别级配额与保留策略(在全局上限内按 category 细分 eviction)
Head: feat/2026-08-25_issue-3528 @ 0ff397f8c48ba4fd6495fea972524d486faba02a
Base: feat/2026-08-25_issue-3527
Reviewed by: Claude subagent (codex-implement-loop)

Verdict rationale

Three-dot range origin/feat/2026-08-25_issue-3527...origin/feat/2026-08-25_issue-3528 is a single commit (0ff397f8c), 8 production files (12 total, well under 30), and does not contain #3527 feature commits. The diff matches the issue’s revised owner model: retention is actor-owned via typed command → event → UserMemoryState, not profile/conversation overlay. No-policy ApplyAdded keeps the legacy same-category-then-oldest path; a present policy applies the added category’s max_entries cap then global-50 eviction by eviction_rank desc with legacy order on rank ties; the new entry is excluded from both. ReplaceUserMemoryRetentionPolicyCommand carries CAS expected_state_version and mutation_id idempotency in the LLMModelCatalogPolicyGAgent shape. Snapshot/query expose RetentionPolicy + PolicyRevision; Studio gets a scope-authorized PUT /api/user-memory/retention-policy 202 receipt; canon §2 retention row and §3 write-side list are updated. Extra proto fields last_retention_policy_mutation_id / event mutation_id are required for replay of mutation idempotency and are documented. Sample-grep of the diff found none of the listed CLAUDE.md anti-patterns. Residual nits (command types living in IUserMemoryQueryPort.cs, new GAgent tests calling handlers directly rather than HandleEventAsync, canon §1 producer column still saying add/remove/clear) do not miss written ACs.

Findings

None.

What's good

  • Issue intent. Proto messages, UserMemoryState.retention_policy, replace command/event, actor-owned revision, category cap then global-50 rank eviction, no-policy bit-identical path, no add-rejected failure mode, no profile/ChatRequest overlay, no TTL/reservation/vector search. HTTP is the issue’s minimal write surface; reads stay on IUserMemoryQueryPort.
  • Replay determinism. Eviction stays inside TransitionState / ApplyAdded. Missing policy uses SelectLegacyEvictionCandidate (same-category oldest, then global oldest, never the added id). ReplayWithoutPolicy_ShouldPreserveLegacyStateBytes asserts RetentionPolicy is null and ToByteArray() matches a hand-built legacy state, so unset proto fields do not change stored bytes.
  • Policy matrix. Tests cover per-category cap, higher rank evicted before older low-rank, rank-tie legacy same-category order, and “never evict the entry being added”. max_entries == 0 skips the cap; unconfigured categories default rank 100.
  • CAS / mutation_id. Same-id same-rules is a no-op even with a stale expected_state_version; same-id different rules → user_memory_policy_mutation_conflict; stale version → user_memory_expected_state_version_conflict; UNSPECIFIED / duplicate category / max_entries and eviction_rank bounds reject without commit. PolicyMatchesState compares normalized rules only (retry rebuilds policy_revision + 1).
  • Ports match the named precedent. IUserMemoryRetentionPolicyCommandPort + ActorDispatchUserMemoryRetentionPolicyCommandService pre-validate via UserMemoryGAgent.BuildRetentionPolicyReplacedEvent, EnsureAsync + IActorDispatchPort.DispatchAsync, honest UserConfigSaveReceipt (accepted / admission_rejected). Query unpacks state_root Any; no read-model schema change. UserMemory assembly is now in the Studio agent-kind scan so bootstrap can activate the actor.
  • Host boundary. [Authorize] PUT api/user-memory/retention-policy, scope from IAppScopeResolver, JSON only at the controller. Unknown categories become Unspecified and fail closed in NormalizeRules before dispatch.
  • Proto / docs / hygiene. New state fields 2–3, command fields 1–3, event fields 1–2; no JSON actor state. docs/canon/conversation-context-and-memory.md §2 retention and §3 command/event list updated; policy “不从 conversation profile 或 query path 注入”. No NyxID / chrono-storage / chrono-ornn edits.
  • CLAUDE.md scan (no hits in this diff): no Task.Delay(; no GetAwaiter().GetResult(); no TypeUrl.Contains(; no JSON for actor state (HTTP JSON stays at Host); no middle-layer ID→fact dictionaries; production dispatch is DispatchAsync not actor.HandleEventAsync(; no SubscribeAsync<EventEnvelope>; no *WriteActor/*ReadActor; no new HttpClient(; no [Skip] / Manual; no new Metadata bag. HandleEventAsync appears only on test stubs (IActor implementors), matching the existing UserMemory tests.

Residual (not blocking): IUserMemoryRetentionPolicyCommandPort / ReplaceUserMemoryRetentionPolicy sit in IUserMemoryQueryPort.cs rather than a policy-contracts file (LLM catalog splits these). New GAgent policy tests call HandleReplaceRetentionPolicy / HandleAddUserMemoryEntry directly; pre-existing tests route add/remove through HandleEventAsync(Envelope(...)). Canon §1 producer cell still lists only add/remove/clear. HTTP category mapping is a closed string switch (v1 enums only; actor/proto remain enum-extensible).

REVIEW_VERDICT:pass:actor-owned category retention policy meets #3528 ACs

@eanzhao

eanzhao commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Merged into feature/integrate as part of the milestone 45 stack (integration commit adc6cbf10). This PR's head commit is now an ancestor of feature/integrate; GitHub could not auto-close it because its base is still the stacked branch rather than feature/integrate.

The stack was merged as a single update rather than bottom-up, because the intermediate states were broken: #3538 registered MapDelete on the content-artifact pin resource with an inferred request body, which minimal APIs reject for DELETE. Endpoint construction threw and the Mainnet host failed to start, and #3539 did not fix it — so merging bottom-up would have left feature/integrate red in between.

Fixed on the way in (adc6cbf10):

  • bound the pin clear body with [FromBody] and added ContentArtifactEndpointsRouteBindingTests — the handler tests called the method directly and never bound the route, so only MainnetHostCompositionTests caught it;
  • moved the ContentArtifact read contracts and query port into Aevatar.Studio.Application.Abstractions and dropped the Aevatar.Studio.Application reference [NyxID Chat] Conversation 级类型化结构化档案附件(typed structured context attachment) #3525 added to the NyxidChat agent, restoring LayerDependencyTests.NyxidChat_ShouldOnlyDependOnStudioApplicationAbstractions.

Verification on the merged result: build 0 errors; Architecture/Capabilities/Studio/Bootstrap/AI 7166 passed, 0 failed; architecture_guards.sh and test_stability_guards.sh 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.

2 participants