xworkmate-app/docs/architecture/xworkmate-internal-state-architecture.md
2026-03-23 20:33:19 +08:00

24 KiB

XWorkmate App Internal State Architecture Last Updated: 2026-03-22

Purpose

This document defines the current internal state model of XWorkmate with a focus on the relationship between:

  • Settings center configuration state
  • Current assistant session state
  • Task thread state
  • Skill state
  • Execution target / work mode
  • Model selection
  • Conversation content

This file is intended to be the plain-text baseline for future AI-assisted changes. If a new implementation conflicts with this document, the change should be treated as suspicious until the ownership and data flow are clarified.

Scope note

This document is written from the Desktop implementation first, because the Desktop controller currently owns the richest runtime and persistence path. Where Web has a parallel implementation with the same state semantics, that mapping is called out explicitly instead of being treated as an afterthought.

Platform runtime matrix

  • Desktop runtime:
    • Platforms: macOS, Windows, Linux
    • Linux desktop shell support: GTK-based (GNOME/KDE/XFCE etc.); KDE Plasma (Qt) integration is a future direction, not yet implemented in runtime code
    • Fixed work modes: AI Gateway, Local OpenClaw Gateway, Remote OpenClaw Gateway
  • Mobile runtime:
    • Platforms: iOS, Android
    • Fixed work modes: Remote OpenClaw Gateway only
  • Web runtime:
    • Platform: standard browser runtime
    • Fixed work modes: AI Gateway, Remote OpenClaw Gateway

Persistence guardrails (v0.6.1)

  • Desktop persistence path is stable at ~/Library/Application Support/plus.svc.xworkmate/xworkmate.
  • SettingsStore and SecretStore initialize durable directories/files on first install.
  • Path/DB resolution failure defaults to fail-fast instead of silent in-memory persistence.
  • In explicit test fallback mode, temporary in-memory state will attempt best-effort sync back to durable storage when possible.

These work-mode arrays come from feature-manifest capabilities. They are not derived from gateway profile data.

========================================================================

  1. Core Rule ========================================================================

There are two primary state layers and one derived UI layer.

Layer A: Settings center configuration state Layer B: Current assistant session state Layer C: Derived UI state

The most important rule is:

Settings center state is not the same thing as current session state.

Settings defines defaults and persisted app-level configuration. Session state defines what the currently selected task thread is actually using. UI should render from the resolved session state, not from settings alone.


Architecture Diagram

graph TB
    subgraph P["④ Persistence Layer"]
        SettingsStore["SettingsStore"]
        SecretStore["SecretStore"]
        SecureConfigStore["SecureConfigStore"]
    end

    subgraph C_D["②a AppControllerDesktop (Desktop)"]
        settings["settings<br/>(persisted snapshot)"]
        settingsDraft["settingsDraft<br/>(in-memory draft)"]
        _draftSecretValues["_draftSecretValues"]
        _pendingApply["_pendingSettingsApply<br/>_pendingGatewayApply<br/>_pendingAiGatewayApply"]
        _assistantThreadRecords["_assistantThreadRecords[sessionKey]<br/><AssistantThreadRecord>"]
        _assistantThreadMessages["_assistantThreadMessages[sessionKey]"]
        _gatewayHistoryCache["_gatewayHistoryCache[sessionKey]"]
        _localSessionMessages["_localSessionMessages[sessionKey]"]
        _aiGatewayStreaming["_aiGatewayStreamingTextBySession[sessionKey]"]
    end

    subgraph C_W["②b AppControllerWeb (Web, parallel)"]
        settings_W["_settings"]
        settingsDraft_W["_settingsDraft"]
        _threadRecords["_threadRecords[sessionKey]<br/><AssistantThreadRecord>"]
    end

    subgraph R["③ Resolver / Accessor Layer"]
        executionTargetResolver["assistantExecutionTargetForSession(sessionKey)"]
        modelResolver["assistantModelForSession(sessionKey)"]
        availableSkillsR["assistantImportedSkillsForSession(sessionKey)"]
        selectedSkillsR["assistantSelectedSkillKeysForSession(sessionKey)"]
        connectionStateR["assistantConnectionStateForSession(sessionKey)"]
    end

    subgraph U["④ UI Layer (Derived, not authoritative)"]
        subgraph AP["AssistantPage"]
            _taskSeeds["_taskSeeds<br/>(rendering cache)"]
            connectionChip["connection chip"]
            execSelector["execution target selector"]
            skillPanel["skill panel"]
            modelLabel["model label"]
            taskList["task list"]
        end
        subgraph SP["SettingsPage"]
            draftEditor["settingsDraft editor"]
            saveApply["Save / Apply buttons"]
        end
    end

    %% Persistence writes
    SecretStore -->|"secure secrets"| _draftSecretValues
    SecureConfigStore -->|"config refs"| settings
    SettingsStore -->|"persisted snapshot"| settings

    %% Settings draft flow
    settingsDraft -.->|"_settingsDraftInitialized<br/>? draft : settings"| settings
    _draftSecretValues -->|flush on Apply| SecretStore
    _pendingApply -->|Apply triggers| settings

    %% Thread record is the per-session state core
    _assistantThreadRecords -->|executionTarget<br/>assistantModelId<br/>selectedSkillKeys<br/>importedSkills<br/>messageViewMode| R
    _assistantThreadMessages -->|gateway-backed messages| R
    _gatewayHistoryCache -->|gateway history| R
    _localSessionMessages -->|local messages| R
    _aiGatewayStreaming -->|streaming text| R

    %% Resolver output feeds UI
    executionTargetResolver -->|currentAssistantExecutionTarget| execSelector
    executionTargetResolver -->|connection state| connectionChip
    modelResolver -->|resolved model| modelLabel
    availableSkillsR -->|available skills| skillPanel
    selectedSkillsR -->|selected keys| skillPanel
    connectionStateR -->|connection state| connectionChip

    %% Task list from thread records
    _assistantThreadRecords -.->|"title / preview<br/>/ status"| _taskSeeds
    _taskSeeds --> taskList
    settings -.->|grouping defaults| taskList

    %% Settings page drives draft and apply
    draftEditor --> settingsDraft
    saveApply -->|Save = persist| settingsDraft
    saveApply -->|Apply = runtime sync| _assistantThreadRecords
    saveApply -->|Apply = runtime sync| settings

    %% Web parallel — same schema, isolated instance
    C_W -.->|"same AssistantThreadRecord<br/>schema as Desktop"| C_D

    style P fill:#e8f4f8,stroke:#4a90a4,stroke-width:1px
    style C_D fill:#f0f0f0,stroke:#666,stroke-width:2px
    style C_W fill:#f9f9f9,stroke:#999,stroke-width:1px,stroke-dasharray:4
    style R fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style U fill:#e8f5e9,stroke:#43a047,stroke-width:1px

How to read this diagram:

  • Solid arrows (-->) = authoritative data flow / ownership
  • Dashed arrows (-.->) = derived / recomputed read
  • Resolver layer implements the resolution order: thread record field → settings fallback
  • Web (AppControllerWeb) maintains its own isolated _threadRecords instance; it has the same AssistantThreadRecord schema but is a separate runtime copy
  • Persistence layer never flows upward — settings are loaded at bootstrap, drafts are written back on Save, runtime never directly mutates persisted state

======================================================================== 2. State Ownership

2.1 Settings center configuration state

Primary owners:

  • lib/app/app_controller_desktop.dart
  • lib/app/app_controller_web.dart

Primary fields:

  • settings
  • settingsDraft
  • _settingsDraftInitialized
  • _draftSecretValues
  • _pendingSettingsApply
  • _pendingGatewayApply
  • _pendingAiGatewayApply
  • settings.gatewayProfiles
  • settings.assistantExecutionTarget
  • settings.assistantCustomTaskTitles
  • settings.assistantArchivedTaskKeys
  • settings.assistantLastSessionKey

Sources:

  • settings Persisted global snapshot from SettingsController.snapshot
  • settingsDraft In-memory draft used by Settings page before save/apply
  • _settingsDraftInitialized Gate that decides whether settingsDraft should return the in-memory draft or fall back to the persisted settings snapshot
  • _draftSecretValues Temporary secret draft values before they are persisted into secure storage

Responsibilities:

  • Store global default configuration
  • Persist app-level settings
  • Persist secure secrets
  • Persist OpenClaw connection source profiles
  • Persist the default work mode for newly created threads
  • Persist assistant task metadata that is not owned by AssistantThreadRecord
    • custom task titles
    • archived task keys
    • last restored session key
  • Make the saved configuration take effect only when Apply is executed

Important APIs:

  • saveSettingsDraft(...)
  • persistSettingsDraft()
  • applySettingsDraft()
  • saveSettings(...)

Important rule: Settings center should define defaults, integrations, and persisted config. It should not be treated as the only truth for the current task thread. It also must not collapse assistantExecutionTarget and gatewayProfiles into the same field.

2.2 Current assistant session state

Primary owners:

  • Desktop: lib/app/app_controller_desktop.dart
  • Web: lib/app/app_controller_web.dart

Primary in-memory store:

  • Desktop:
    • _assistantThreadRecords[sessionKey]
    • _assistantThreadMessages[sessionKey]
    • _gatewayHistoryCache[sessionKey]
    • _aiGatewayStreamingTextBySession[sessionKey]
    • _localSessionMessages[sessionKey]
  • Web:
    • _threadRecords[sessionKey]
    • _streamingTextBySession[sessionKey]
    • current record fallback through _currentRecord

Primary schema: lib/runtime/runtime_models.dart AssistantThreadRecord

AssistantThreadRecord fields that matter most:

  • executionTarget
  • messageViewMode
  • importedSkills
  • selectedSkillKeys
  • assistantModelId
  • gatewayEntryState
  • messages

Responsibilities:

  • Hold per-thread overrides
  • Isolate thread behavior from other threads
  • Preserve per-thread mode, skills, model, and content
  • Allow thread state to differ from global default settings
  • Carry the authoritative messages list for the session (the primary conversation content; see also Section 3.4 for all content sources)

Important rule: If a value exists in AssistantThreadRecord for a session, that thread-level value wins over the global settings default.

Web note: The Web controller uses the same AssistantThreadRecord schema and the same basic ownership rule, but the runtime-backed data sources are simpler than Desktop. In Web, relay/direct-AI conversation state is resolved through _threadRecords, _currentRecord, and browser/session repositories rather than Desktop runtime controllers.

2.3 Derived UI state

Primary owners:

  • lib/features/assistant/assistant_page.dart
  • lib/features/settings/settings_page.dart

Examples:

  • task list groups
  • top-right connection chip
  • bottom execution target selector
  • empty-state card
  • skill panel
  • model label
  • task row labels

Responsibilities:

  • Display resolved state
  • Never become the authoritative source of truth

Important rule: UI must render from resolved state, not invent its own parallel mode/model/skill state.

======================================================================== 3. Resolution Priority

3.1 Execution target / work mode

Meaning:

  • Single Agent
  • Local OpenClaw Gateway
  • Remote OpenClaw Gateway

Platform availability:

  • Desktop: singleAgent, local, remote
  • Mobile: remote
  • Web: singleAgent, remote

Primary resolver: assistantExecutionTargetForSession(sessionKey)

Resolution order:

  1. AssistantThreadRecord.executionTarget for that session
  2. settings.assistantExecutionTarget

Interpretation:

  • settings.assistantExecutionTarget is the default
  • thread.executionTarget is the actual current-session override

Consequence: Changing settings alone does not automatically mean the current thread display has changed unless the current thread record is also synchronized.

Important separation:

  • assistantExecutionTarget is the work-mode default / thread override axis
  • it is not a pointer into gatewayProfiles
  • Single Agent has no OpenClaw profile
  • there is no implicit local-to-remote or AI-to-remote profile fallback

3.1.1 OpenClaw gateway profile list

Primary owner:

  • SettingsSnapshot.gatewayProfiles

Meaning:

  • OpenClaw connection sources saved in Settings center

Fixed structure:

  • index 0: fixed Local OpenClaw profile
  • index 1: fixed Remote OpenClaw profile
  • index 2: custom slot
  • index 3: custom slot
  • index 4: custom slot

Rules:

  • gatewayProfiles is a list, not a single gateway field
  • first two slots are reserved and normalized on load
  • Desktop may use both fixed OpenClaw profiles
  • Mobile only consumes the fixed remote profile
  • Web only consumes the fixed remote profile
  • custom slots are saved configuration only; they do not expand the platform work-mode array by themselves

Ownership rule:

  • work mode selects whether the current thread is AI, Local OpenClaw, or Remote OpenClaw
  • profile selection provides connection parameters for OpenClaw-backed modes
  • changing a profile does not by itself change the current thread mode

3.2 Model

Primary resolver: assistantModelForSession(sessionKey)

Resolution order:

  1. AssistantThreadRecord.assistantModelId
  2. resolved model for current execution target

Fallback rules:

  • If target is singleAgent, use resolvedAiGatewayModel
  • If target is local or remote, use resolvedDefaultModel

Interpretation: Model selection is thread-bound when explicitly set, otherwise inherited from target-specific defaults.

3.3 Skills

Primary owner: AssistantThreadRecord

Fields:

  • importedSkills
  • selectedSkillKeys

Resolution rule:

  • The available and selected skills shown in UI belong to the current session thread
  • Settings center must not be treated as the source of selected thread skills

3.4 Conversation content

Primary sources:

  • _chatController.messages
  • _gatewayHistoryCache[sessionKey]
  • _assistantThreadMessages[sessionKey]
  • _localSessionMessages[sessionKey]
  • _aiGatewayStreamingTextBySession[sessionKey]

Resolution rule:

  • Gateway-backed thread content and AI-Gateway-only thread content do not come from the same runtime path
  • The UI composes the final visible conversation from multiple stores depending on the current thread target

3.5 Task thread list

Primary source:

  • assistantSessions
  • _taskSeeds in AssistantPage as a rendering cache
  • settings.assistantCustomTaskTitles
  • settings.assistantArchivedTaskKeys

Important rule: Task list is a derived representation of thread/session state. Task list must not become the owner of mode, model, or skill state.

Important companion rule: Task titles, archive membership, and last-session recovery are not owned only by AssistantThreadRecord. Part of that metadata is persisted in SettingsSnapshot and must be considered when reconstructing the task list.

Implementation note: _taskSeeds is still a cache of derived values such as title, preview, status, owner, surface, and executionTarget. It is not an authoritative source, but it can become stale if source mutations do not eventually trigger task recomputation.

======================================================================== 4. Data Flow

4.1 Settings center flow

Edit in Settings page -> settingsDraft changes -> Save -> persisted settings + secure secrets update -> Apply -> current configuration takes effect immediately

Meaning of buttons:

  • Save Persist configuration only Do not trigger runtime connection or model sync
  • Apply Make the current saved configuration take effect immediately This may connect a gateway, switch execution behavior, or sync AI Gateway catalog

4.2 Session flow

Select thread -> switchSession(sessionKey) -> resolve thread executionTarget -> resolve thread model -> resolve thread skills -> apply thread execution target -> reload conversation content for the chosen thread

Create new thread -> inherit current thread executionTarget -> inherit current thread messageViewMode -> initialize AssistantThreadRecord -> switch to the new thread

Change execution target from Assistant page -> update current thread record.executionTarget -> optionally persist new global default selection -> reconnect / disconnect runtime as needed -> refresh skills and derived UI

======================================================================== 5. Dependency Graph

5.1 Settings center depends on

  • SettingsController snapshot and secure refs
  • SecureConfigStore / SettingsStore / SecretStore
  • Runtime side-effect handlers in AppController

5.2 Current assistant session depends on

  • _assistantThreadRecords
  • runtime snapshot / gateway runtime
  • current selected session key
  • persisted thread records restored during bootstrap

5.3 Task list depends on

  • assistantSessions
  • current session key
  • thread executionTarget
  • per-thread preview / title / status

5.4 Skill panel depends on

  • current session key
  • assistantImportedSkillsForSession(sessionKey)
  • assistantSelectedSkillKeysForSession(sessionKey)

5.5 Top-right status chip depends on

  • current session key
  • assistantConnectionStateForSession(currentSessionKey)
  • runtime connection snapshot
  • session executionTarget

5.6 Bottom execution target selector depends on

  • currentAssistantExecutionTarget
  • thread executionTarget for current session

5.7 Model label depends on

  • assistantModelForSession(currentSessionKey)
  • resolvedAiGatewayModel
  • resolvedDefaultModel

======================================================================== 6. Correct Sync Rules

6.1 What Save should update

Save should update:

  • persisted settings snapshot
  • persisted secure secrets
  • persisted gatewayProfiles
  • pending apply markers

Save should not update:

  • live runtime connection by itself
  • current thread execution target by itself
  • task list grouping by itself unless the task list is explicitly reading the global defaults

6.2 What Apply must update when execution behavior changes

If Apply changes assistant execution behavior, it must synchronize:

  • settings.assistantExecutionTarget
  • current thread AssistantThreadRecord.executionTarget
  • the exact OpenClaw profile used by that execution target, if the target is local or remote
  • runtime connection / disconnection path
  • session-specific skill visibility if mode changes
  • derived UI:
    • top-right chip
    • bottom selector
    • empty-state card
    • task list grouping

6.3 What thread switching must update

switchSession(sessionKey) must synchronize:

  • current thread executionTarget
  • current thread message view mode
  • current thread model
  • current thread available / selected skills
  • current thread conversation content source
  • current thread connection label

6.4 What must never happen implicitly

  • local OpenClaw selectedAgentId must not silently fall back to remote
  • Single Agent mode must not silently borrow a gateway profile
  • gatewayProfiles changes must not silently overwrite the current thread mode
  • platform capability filtering must not invent unsupported work modes

6.5 Integration cards share one action contract

Gateway-family cards (OpenClaw Gateway / Vault / AI Gateway) and future extensions must keep the same contract:

  • Test = validate current draft only (including temporary secret overrides), no persistence side effects
  • Save = persist draft + secure secrets only
  • Apply = make saved draft effective in runtime/session state

To avoid semantic duplication, the Gateway settings tab uses local card actions and does not render another global Save/Apply action row.

6.6 What task list must never do

Task list must never:

  • own executionTarget
  • own model selection
  • own selected skills
  • become the source of truth for session state

Task list should only display resolved session state.

======================================================================== 7. Known Failure Modes

7.1 Settings center and current session diverge

Symptom:

  • User saves/applies a new mode in Settings
  • Top-right chip still shows old mode
  • Bottom selector still shows old mode

Typical cause:

  • settings.assistantExecutionTarget updated
  • current session AssistantThreadRecord.executionTarget not updated

7.2 Task list grouping is wrong

Symptom:

  • Task appears under the wrong mode group
  • Group count does not match the visible current thread target

Typical cause:

  • task seed / task entry is rendering stale thread executionTarget
  • _taskSeeds still holds stale derived values because the mutation path did not trigger task recomputation

7.3 Skill panel leaks across threads

Symptom:

  • A skill selected in one task appears selected in another unrelated task

Typical cause:

  • selectedSkillKeys not isolated to AssistantThreadRecord
  • UI reading global or shared state instead of current session record

7.4 Model label is stale

Symptom:

  • Current thread changed, but header/composer still shows previous model

Typical cause:

  • UI not recomputed from assistantModelForSession(currentSessionKey)

7.5 Conversation content source mismatch

Symptom:

  • Thread switches, but visible content still reflects previous mode/path

Typical cause:

  • current session switched
  • content source not switched between gateway-backed history and AI-Gateway-only cache

======================================================================== 8. Canonical Ownership Table

Field: settingsDraft Owner: Settings center Scope: global draft

Field: settings Owner: persisted settings snapshot Scope: global persisted config

Field: secret drafts Owner: Settings center Scope: global draft, secure persistence path

Field: executionTarget Owner: AssistantThreadRecord first, settings fallback second Scope: thread

Field: assistantModelId Owner: AssistantThreadRecord first, target-specific fallback second Scope: thread

Field: selectedSkillKeys Owner: AssistantThreadRecord Scope: thread

Field: importedSkills Owner: AssistantThreadRecord Scope: thread

Field: messageViewMode Owner: AssistantThreadRecord Scope: thread

Field: conversation messages Owner: runtime/message stores depending on target Scope: thread

Field: task list group Owner: derived UI only Scope: visual grouping

======================================================================== 9. Modification Rules For Future AI Changes

Before changing Assistant, Settings, or Gateway behavior, check:

  1. Is this a settings default or a thread override?
  2. If a setting is applied, should the current thread record be synchronized?
  3. If a thread changes, which derived UI surfaces must refresh?
  4. Is the task list only displaying state, or accidentally owning it?
  5. Does the change preserve per-thread isolation for:
    • mode
    • model
    • skills
    • content

If a proposed change cannot answer those five questions clearly, the implementation is not ready.

======================================================================== 10. Relevant Files

Global settings and apply flow:

  • lib/app/app_controller_desktop.dart
  • lib/app/app_controller_web.dart
  • lib/features/settings/settings_page.dart

Session/thread state:

  • lib/runtime/runtime_models.dart
  • lib/app/app_controller_desktop.dart
  • lib/app/app_controller_web.dart

Assistant UI:

  • lib/features/assistant/assistant_page.dart

Persistence:

  • lib/runtime/secure_config_store.dart
  • lib/runtime/settings_store.dart
  • lib/runtime/secret_store.dart

Supporting architecture docs:

  • docs/architecture/assistant-thread-information-architecture.md
  • docs/architecture/xworkmate-integrations.md

End of document.