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. SettingsStoreandSecretStoreinitialize 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.
========================================================================
- 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_threadRecordsinstance; it has the sameAssistantThreadRecordschema 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
messageslist 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:
- AssistantThreadRecord.executionTarget for that session
- 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:
assistantExecutionTargetis 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:
gatewayProfilesis 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:
- AssistantThreadRecord.assistantModelId
- 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:
- Is this a settings default or a thread override?
- If a setting is applied, should the current thread record be synchronized?
- If a thread changes, which derived UI surfaces must refresh?
- Is the task list only displaying state, or accidentally owning it?
- 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.