7个问题根因分析+修复方案: P0: 聊天路由竞态/记忆查询缺陷/hand_trigger硬编码 P1: Agent画像断链/反思持久化多重缺陷 P2: 演化差异视图/管家Tab上下文混淆 路径B: 系统桥接修复,扩展已有命令而非新增
14 KiB
Detail Panel 7-Issue Fix Design
Date: 2026-04-11 Status: Draft Approach: Path B — System Bridge (fix bugs + bridge disconnected systems) Constraint: No new SaaS endpoints, no new stores, no new middleware (stabilization compliance)
Background
User discovered 7 issues during real usage of ZCLAW desktop app's detail panel (RightPanel). Root cause analysis revealed a pattern: 5 of 7 issues stem from "backend has capability but frontend doesn't consume it" — disconnected systems that need bridging.
Issue Summary
| # | Problem | Severity | Root Cause | Category |
|---|---|---|---|---|
| 1 | Can't chat after restart | P0 | Heartbeat degrades to 'tauri' in localStorage, restoreSession blindly trusts it | Route race |
| 2 | "About me / How I see you" never updates | P1 | Panel reads agentStore.Clone, memory extractor writes identity files — no bridge | Data disconnect |
| 3 | Memory 311 count but empty list/graph | P0 | memory_search empty query + min_similarity=None → FTS5 returns 0 | Query logic bug |
| 4 | Only 1 reflection, effects unknowable | P1 | History only persists latest (not array); pattern thresholds too high; state restore race | Multi-factor |
| 5 | hand_trigger always rejected | P0 | autonomy-manager.ts:268 maps hand_trigger to null — hardcoded bug | Hardcoded bug |
| 6 | Evolution has data but effects unknowable | P2 | HistoryItem only shows truncated reason, no expandable diff | UX gap |
| 7 | Butler "6 messages" incorrect + empty below | P2 | Shared chat stats header confused for butler data; no pain points detected | UX confusion |
Section 1: P0 Fixes
Fix 1 — Chat Routing After Restart
Root Cause: saasStore.restoreSession() blindly trusts localStorage connectionMode written by heartbeat degrade. After restart, SaaS auth may still be valid but the persisted 'tauri' mode prevents SaaS relay usage.
Solution:
- In
restoreSession()(saasStore.ts ~line 750): Before accepting persisted mode, validate SaaS token:- Attempt refresh token → access token exchange
- If successful, restore to 'saas' mode regardless of persisted value
- Only accept 'tauri' mode if token refresh fails
- In
loadConnectionMode()(saas-session.ts ~line 170): Add staleness check:- Persist timestamp alongside mode in localStorage
- On load, if persisted mode is 'tauri' and timestamp > 5 minutes old, re-validate
- Heartbeat degrade writes timestamp for staleness tracking
Files:
desktop/src/store/saasStore.ts— restoreSession() ~line 750 (verify exact location before implementation)desktop/src/lib/saas-session.ts— loadConnectionMode() ~line 170desktop/src/store/connectionStore.ts— connect() routing ~line 428
Risk: Low. Only changes startup validation logic, doesn't affect active session behavior.
Fix 3 — Memory List/Graph Empty Despite 311 Entries
Root Cause: memory_search Tauri command calls VikingStorage::find() with empty query string and min_similarity: None. FTS5 requires non-empty query to match anything. memory_stats works because it sets min_similarity: Some(0.0) which triggers table scan path.
Solution:
- In
memory_commands.rsmemory_search(line 164): When query is empty or whitespace-only, setmin_similarity: Some(0.0)as default fallback - This makes the empty-query behavior consistent with
memory_stats— table scan returns all entries - No changes to VikingStorage internals — only the calling parameter
Files:
desktop/src-tauri/src/memory_commands.rs— memory_search at line 148-184
Risk: Minimal. Single-line parameter change. Performance concern mitigated by existing limit: 50 default.
Fix 5 — hand_trigger Always Rejected
Root Cause: autonomy-manager.ts:268 maps hand_trigger: null in the actionMap. When configKey is null, isActionAllowed() immediately returns false. No handAutoTrigger field exists in AutonomyConfig.allowedActions.
Solution:
- Add
handAutoTrigger: booleantoAutonomyConfig.allowedActionstype (autonomy-manager.ts) - Change actionMap:
hand_trigger: null→hand_trigger: 'handAutoTrigger' - Update default configs:
autonomouslevel:handAutoTrigger: truesupervisedlevel:handAutoTrigger: falseguidedlevel:handAutoTrigger: false
- Add toggle control in
AutonomyConfig.tsxUI
Files:
desktop/src/lib/autonomy-manager.ts— line 268 actionMap + allowedActions type + default configsdesktop/src/components/AutonomyConfig.tsx— add handAutoTrigger toggle
Risk: Low. Pure frontend change. Backend already supports autonomous mode bypassing approval gate.
Section 2: P1 Fixes
Fix 2 — "About Me / How I See You" Auto-Update
Root Cause: Panel reads from agentStore.Clone static fields. Memory extractor writes to identity files (soul/instructions/user_profile). Backend UserProfileStore (Rust) has structured user modeling (industry/role/expertise/communication_style/recent_topics) but no Tauri command exposes it to frontend.
Solution (System Bridge — Alternative Approach: Extend Existing Command):
To comply with stabilization constraint "no new Tauri commands" (CLAUDE.md §1.3: "已有 189 个,70 个无前端调用"), we use Option (b): extend an existing command's return payload.
-
Extend
agent_getcommand: The existingagent_get(agent_id)command returnsAgentConfig. Extend its return to include an optionaluser_profilefield fromUserProfileStore::get().- This avoids creating a new command while exposing existing backend data.
- File:
desktop/src-tauri/src/kernel_commands/agents.rs— extend return type
-
Frontend bridge:
- Use component-local state (
useState) in RightPanel for UserProfile data (not Store — per CLAUDE.md §4.2 layered responsibilities) - On mount and after conversation complete, re-fetch agent data which now includes profile
- "How I See You" section renders from two sources:
- Static:
Clonefields (userName, workspaceDir, privacyOptIn) - Dynamic:
UserProfilefields (industry, role, expertise, communication_style, recent_topics)
- Static:
- Fallback: if UserProfile empty, show static Clone data only
- Use component-local state (
-
Refresh trigger: In
streamStoreflow completion callback (~line 446), after memory extraction, trigger RightPanel's profile refresh via a lightweight event or re-fetch
Files:
desktop/src-tauri/src/kernel_commands/agents.rs— extend agent_get return typedesktop/src-tauri/src/lib.rs— no change needed (command already registered)desktop/src/components/RightPanel.tsx— dual-source rendering (lines 523-557)desktop/src/store/chat/streamStore.ts— add profile refresh trigger ~line 446
Risk: Medium. Extends existing command rather than creating new one. UserProfileStore already exists. No new SaaS endpoints. No new stores.
Fix 4 — Reflection Only 1 Entry, Effects Unknowable
Root Cause: Multi-factor — history only persists reflection:latest (single entry) not reflection:history (array); pattern detection thresholds too high (5+ same type); state restore race condition in post_conversation_hook.
Solution:
-
Fix history persistence (reflection.rs):
- Ensure
reflect()appends toreflection:history:{agent_id}array (not overwrite) - Maintain cap at 20 entries (FIFO)
- Always write both
reflection:latestANDreflection:history
- Ensure
-
Lower pattern detection thresholds (reflection.rs
analyze_patterns()):- Pattern "many tasks": 5+ → 3+ (rationale: early pattern detection provides timely insights)
- Pattern "many preferences": 5+ → 3+ (rationale: user preferences accumulate quickly in normal use)
- Pattern "many experiences": 5+ → 3+ (rationale: experiences are high-value patterns worth detecting early)
- Pattern "high-visit memories": 3+ → 2+ (rationale: even 2 high-visit memories indicate recurring interest)
- Pattern "low-importance memories": 20+ → 15+ (rationale: conservative reduction from 20 — avoids noisy cleanup suggestions at 10)
-
Fix state restore race (intelligence_hooks.rs):
- Change
post_conversation_hookstate restore from "pop (read + delete)" to "read only" - Cleanup mechanism: old state is cleaned up on next conversation hook invocation (FIFO with max 1 entry per agent), preventing indefinite accumulation
- Let
getHistoryalways find complete data in VikingStorage
- Change
-
Enhance reflection effect visibility (ReflectionLog.tsx):
- Each history entry shows: pattern count + summary, improvement suggestion count, identity proposal count
- Timeline indicator: "Triggered after N conversations"
- Expandable detail view for patterns and suggestions
Files:
desktop/src-tauri/src/intelligence/reflection.rs— history persistence + threshold adjustmentdesktop/src-tauri/src/intelligence_hooks.rs— state restore race fixdesktop/src/components/ReflectionLog.tsx— enhanced visibility UI
Risk: Low-Medium. Reflection persistence fix is the most impactful change. Threshold changes are conservative (3+ is still meaningful).
Section 3: P2 Fixes
Fix 6 — Evolution Effects Unknowable
Root Cause: HistoryItem component only renders truncated reason text and timestamp. Snapshot data contains full files object but UI never renders diff content. ProposalCard already has a DiffView component that could be reused.
Solution:
-
Expandable HistoryItem (IdentityChangeProposal.tsx):
- Add click-to-expand behavior on history items
- When expanded, show which files changed (as tags)
- Show full reason text (no truncation)
- Reuse existing
DiffViewcomponent for before/after comparison
-
Evolution summary metadata:
- Each history item header shows file change count badge
- Color coding: soul changes = purple, instructions = blue, user_profile = green
Files:
desktop/src/components/IdentityChangeProposal.tsx— HistoryItem component (lines 269-304)
Risk: Low. Pure UI enhancement. Reuses existing DiffView component.
Fix 7 — Butler Tab Confusion + Empty Data
Root Cause: "6 messages" is shared chat statistics header displayed across ALL tabs (not butler-specific). Butler tab below is empty because: (a) no pain points detected in short conversations, (b) no proposals generated, (c) Viking memory path empty.
Solution:
-
Contextual header (RightPanel.tsx):
- When butler tab is active, replace chat statistics with butler-specific summary
- Show: Butler name + pain points count + proposals count + memory count
- Other tabs keep existing chat statistics
-
Enhanced empty state (ButlerPanel/index.tsx):
- When all sections empty, show friendly onboarding message
- "管家正在了解您,多轮对话后会自动发现您的需求"
- Add manual "analyze now" button that calls
analyze_for_pain_signalswith current conversation history - Button disabled when conversation < 2 messages (prevents meaningless analysis)
- Show loading state during analysis
- Display "未发现痛点" message when analysis completes with no results
-
Lower pain point detection threshold (pain_aggregator.rs):
- Relax keyword matching patterns
- Allow pain point extraction from shorter conversations (minimum 2 messages instead of 3+)
Files:
desktop/src/components/RightPanel.tsx— conditional header rendering (lines 374-386)desktop/src/components/ButlerPanel/index.tsx— empty state enhancementdesktop/src-tauri/src/intelligence/pain_aggregator.rs— threshold adjustment
Risk: Low. UX improvements. Pain point threshold change is conservative.
Implementation Priority
| Batch | Issues | Severity | Est. Complexity |
|---|---|---|---|
| Batch 1 | 1, 3, 5 | P0 | Low (small targeted fixes) |
| Batch 2 | 2, 4 | P1 | Medium (system bridge + persistence fix) |
| Batch 3 | 6, 7 | P2 | Low (UX enhancements) |
Testing Plan
Automated Tests (after each batch)
tsc --noEmit— TypeScript type checkcargo check— Rust compilationvitest run— Frontend unit tests
Rust Unit Tests (Batch 2)
memory_searchempty query behavior: verifymin_similaritydefaults toSome(0.0)when query is empty- Reflection history persistence: verify
reflect()appends to history array, not overwrites
Manual Test Matrix
Fix 1 — Chat Routing (must test both modes):
- SaaS mode: restart app with valid SaaS session → should restore to 'saas' mode
- SaaS mode: restart app with expired SaaS session → should degrade to 'tauri' mode
- Tauri mode: restart app → should stay in 'tauri' mode
- Fresh install → should prompt login or default to local mode
Fix 2 — Agent Profile:
- UserProfile empty → fallback to static Clone data
- UserProfile populated → dynamic fields shown alongside static
- After multi-turn conversation → profile fields refresh
Fix 3 — Memory Display:
- Memory panel loads with entries visible (not just count)
- Memory graph renders with nodes and edges
- Pagination works for > 50 entries
Fix 5 — Hand Trigger:
- Existing autonomy configs migrate correctly (no handAutoTrigger → default false)
- Autonomous level → hand_trigger auto-allowed
- Supervised/guided → hand_trigger requires approval
Fix 4 — Reflection:
- After 3+ conversations → reflection triggers automatically
- History shows multiple entries (not just 1)
- Each entry shows pattern/improvement/proposal counts
Fix 6 — Evolution:
- Click history item → expandable diff view appears
- Diff view shows before/after for changed files
Fix 7 — Butler:
- Butler tab shows contextual header (not chat stats)
- "Analyze now" button disabled when < 2 messages
- Empty state shows onboarding message
Documentation Updates (per CLAUDE.md §8.3)
- Update
docs/TRUTH.mdif command count changes - Update
wiki/log.mdwith change record - Update
wiki/module-status.mdif module status affected
Constraint Compliance
| Constraint | Status |
|---|---|
| No new SaaS API endpoints | OK |
| No new SKILL.md files | OK |
| No new middleware | OK |
| No new stores | OK — using component-local state |
| No new admin pages | OK |
| No new Tauri commands | OK — Fix 2 extends existing agent_get command instead |
Note: Fix 2 originally proposed a new Tauri command but was revised to extend existing agent_get to comply with stabilization constraint.