Skip to content

Rename MoltBot to OpenClaw across entire codebase - #2

Merged
shanselman merged 9 commits into
masterfrom
copilot/update-references-to-openclaw
Jan 30, 2026
Merged

Rename MoltBot to OpenClaw across entire codebase#2
shanselman merged 9 commits into
masterfrom
copilot/update-references-to-openclaw

Conversation

Copilot AI commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

Project has been renamed to OpenClaw. Updates all references throughout codebase while preserving "Molty" as the mascot name.

Changes

Namespace & Project Structure

  • Moltbot.*OpenClaw.* (all projects and namespaces)
  • MoltbotTrayOpenClawTray (WinForms/WinUI projects)
  • Class renames: MoltbotGatewayClientOpenClawGatewayClient, IMoltbotLoggerIOpenClawLogger

Deep Links & Storage

  • URI scheme: moltbot://openclaw://
  • Settings: %APPDATA%\MoltbotTray%APPDATA%\OpenClawTray
  • Logs: moltbot-tray.logopenclaw-tray.log

Build & Deployment

  • GitHub Actions artifacts: MoltbotTray-*.zipOpenClawTray-*.zip
  • Installer output: MoltbotTray-Setup-*.exeOpenClawTray-Setup-*.exe
  • Assembly names and manifests updated

Documentation

  • Added historical note to README: "Formerly known as Moltbot, formerly known as Clawdbot"
  • Updated all references in README, CODE_REVIEW, TEST_COVERAGE, and project docs

Breaking Changes

Users will need to reconfigure:

  • Deep links use new openclaw:// scheme
  • Settings stored in new OpenClawTray directory
  • Executable names changed (e.g., OpenClaw.Tray.WinUI.exe)
Original prompt

The original project this supports has been renamed OpenClaw. So all references to MoltBolt should now become OpenClaw. The mascot is still Molty. Update everything, project names, references, readme etc. include at the bottom of the readme - formerly Moltbot, formerly Clawdbot. Need to be thorough and not break the build


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Update project references from MoltBolt to OpenClaw Rename MoltBot to OpenClaw across entire codebase Jan 30, 2026
Copilot AI requested a review from shanselman January 30, 2026 16:41
Copilot AI and others added 8 commits January 30, 2026 10:16
- Renamed all project directories and files
- Updated all namespaces and class names
- Updated deep link scheme from moltbot:// to openclaw://
- Updated AppData directories to OpenClawTray
- Updated README with OpenClaw references and added history note
- Updated all configuration and build files
- Kept mascot name "Molty" unchanged

Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
- Updated CommandPalette namespace from Moltbot to OpenClaw
- Updated OpenClaw class name (was Moltbot)
- Updated all user-facing UI strings to use OpenClaw
- Updated XAML window titles and x:Class references
- All 88 tests passing

Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
- Updated all namespaces from MoltbotTray to OpenClawTray
- Updated all window titles, dialog text, and UI strings
- Updated assembly metadata and comments
- Updated pipe names and window class names
- Fixed using statements and type references
- All 88 tests passing

Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
- Updated GitHub Actions workflow with new project paths
- Updated Tray README.md and DEVELOPMENT.md
- Updated TEST_COVERAGE.md
- All references to Moltbot now changed to OpenClaw
- Tests still passing (88/88 ✅)

Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
- Updated app.manifest assembly identity
- Fixed icon file references (openclaw.ico)
- Fixed DeepLinkHandler URI scheme constant
- Updated StatusDetailForm header text
- Fixed Logger comment path
- All 88 tests passing

Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
- Updated CommandPalette app.manifest assembly name
- Fixed WelcomeDialog icon reference
- Updated user agent string in gateway client
- All 88 tests passing ✅
- Renaming complete!

Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
- Updated WelcomeDialog icon path to openclaw.ico
- Fixed log file names in documentation (openclaw-tray.log)
- Zero Moltbot references remaining! ✅
- All 88 tests passing ✅

Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
@shanselman
shanselman force-pushed the copilot/update-references-to-openclaw branch from c00f7b2 to af5752f Compare January 30, 2026 18:17
- CI artifacts: moltbot-commandpalette -> openclaw-commandpalette
- CODE_REVIEW.md: Update class names to OpenClaw versions
- README files: moltbot.com -> openclaw.ai
- Keep repo URLs as-is (can't change without renaming repo)
@shanselman
shanselman merged commit 6353e05 into master Jan 30, 2026
6 checks passed
@shanselman
shanselman deleted the copilot/update-references-to-openclaw branch February 8, 2026 02:22
AlexAlves87 referenced this pull request in AlexAlves87/openclaw-windows-node May 4, 2026
…nally

Previously HasEnvManipulationBeforeShellWrapper only blocked env with
modifiers when the effective command was a shell wrapper. Direct exec
commands like `env PATH=/evil wget` passed through: the resolver used
the original PATH to find wget while execution would use the modified
one, allowing a different executable to run under an approved identity.

Change: any env invocation with flags or VAR=val assignments now returns
[] from ResolveForAllowlist, regardless of what follows. This also
correctly handles `env --bogus bash -c ...` (unknown flag before wrapper)
which previously could degrade to resolving env itself as the executable
(Hanselman review finding #2).

HasEnvManipulationBeforeShellWrapper removed; no longer needed.

Tests updated/added: flag before direct exec (now empty), PATH= before
direct exec, unknown flag --bogus before shell wrapper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request May 6, 2026
23 tasks
RBrid added a commit to RBrid/openclaw-windows-node that referenced this pull request May 6, 2026
openclaw#6-openclaw#8, Low openclaw#9)

High openclaw#2: Reuse a singleton TextToSpeechService for chat replies
* App.SpeakResponseAsync now goes through NodeService.TextToSpeech
  (a new public accessor on the existing _textToSpeechService field)
  instead of constructing a fresh service per call. Cached Piper
  client is reused across replies; the service-internal _playbackGate
  + _activePlayer now actually serialize back-to-back replies, and
  Interrupt=true takes effect.

High openclaw#3: Per-provider VoiceId routing
* New TtsWindowsVoiceId setting (round-tripped via SettingsManager
  + SettingsData; SettingsRoundTripTests assert it).
* SpeakResponseAsync no longer passes _settings.TtsElevenLabsVoiceId
  as a generic VoiceId; the per-provider Speak* paths each look up
  their own setting (TtsPiperVoiceId / TtsWindowsVoiceId /
  TtsElevenLabsVoiceId).
* SpeakWithWindowsAsync falls back to TtsWindowsVoiceId when
  args.VoiceId is blank.
* VoiceSettingsPage.OnWindowsVoiceChanged writes TtsWindowsVoiceId
  (was overwriting TtsElevenLabsVoiceId, a real cross-provider bug).

High openclaw#4: stt.listen returns a complete utterance, not the first segment
* New AudioPipeline.UtteranceTranscribed event fires once per silence-
  bounded utterance with all Whisper segments aggregated and an
  immutable Segments snapshot.
* VoiceService bubbles it as UtteranceCompleted.
* ListenOnceAsync subscribes to UtteranceCompleted (drops the
  per-fragment accumulator) so multi-segment utterances no longer
  return truncated text.

High openclaw#5: Voice Overlay submits one chat message per utterance
* OnTranscriptionReceived keeps the per-fragment streaming bubble
  update; chat submission moved to a new OnUtteranceCompleted
  handler so the gateway sees one message per spoken utterance.

Medium openclaw#6: Per-asset cancellation tokens in VoiceSettingsPage
* Split _downloadCts into _whisperDownloadCts and _piperDownloadCts
  so starting a Piper download no longer cancels an in-flight
  Whisper download (and vice versa).

Medium openclaw#7: Preflight tar.exe before Piper download
* PiperVoiceManager.EnsureExtractorAvailable runs a fast
  `tar --version` check before any network I/O. Downlevel Windows
  users now get a clear actionable error instead of a wasted ~50-150
  MB download that would later fail at extraction.

Medium openclaw#8: Refresh stale MCP tool descriptions
* stt.transcribe / stt.listen / stt.status now describe the single
  Whisper engine surface (no preferredEngine / effectiveEngine /
  engineFallbackReason); stt.listen description explicitly notes
  the result is the full silence-bounded utterance.
* tts.speak description includes `piper` in the provider list and
  notes the fresh-install default.
* Updated McpToolBridgeTests assertion for the new shape.

Low openclaw#9: Per-asset single-flight in download managers
* Both WhisperModelManager and PiperVoiceManager wrap their
  Download*Async in a static ConcurrentDictionary<string,Task> keyed
  on the canonical asset ID. Concurrent calls for the same asset
  await the same in-flight Task instead of racing on the same .tmp
  file. Failed downloads remove themselves from the table so a fresh
  retry isn't blocked.

Tests: Shared 1271 / Tray 462. Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
RBrid added a commit to RBrid/openclaw-windows-node that referenced this pull request May 6, 2026
…n pane default

Two findings from the second rubber-duck pass.

Medium: ShowHub(activate:false) was a no-op when the Hub was previously
minimized. AppWindow.Show(activateWindow:false) does not restore minimized
windows. Detect OverlappedPresenter.State == Minimized first and
Restore(activateWindow:false) so the window actually surfaces behind the
popup, then call Show.

Low: regression test for HubNavPaneOpen migration. Settings files written
before this field existed must deserialize to true (NavView expanded).
Added an explicit FromJson(\"{}\") assertion plus pinned the field's
default in MissingFields_UseDefaults and BackwardCompatibility_OldSettings*
so a future refactor can't silently flip new installs to a collapsed pane.

Tests: Tray 463 (one new). Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
indierawk2k2 referenced this pull request in indierawk2k2/openclaw-windows-node May 7, 2026
…(Bug #2 from manual test)

On the Local easy-setup path, the loopback gateway parks the Phase 14 node-role connect as PairingStatus.Pending for ~100ms before SettingsWindowsTrayNodeProvisioner's pending-approver auto-approves it. App.OnPairingStatusChanged showed the 'copy pairing command' toast for that transient blip even though the user never needed to copy anything.

Engine: add LocalGatewaySetupEngine.IsAutoPairingWindowsNode bracketed exactly around the Phase 14 _windowsTrayNode.PairAsync call (LocalGatewaySetup.cs:2401), via try/finally with Interlocked.Exchange. Phase 12 PairOperator and the rest of RunLocalOnlyAsync are NOT bracketed — scope is exactly the node-role autopair window per RubberDucky's closure condition.

App: cache the engine in App._localSetupEngine when CreateLocalGatewaySetupEngine is invoked (App.xaml.cs:62). OnPairingStatusChanged Pending branch now consults LocalGatewaySetupEngine.ShouldSuppressPairingPendingNotification(_localSetupEngine, status) — a pure static decision helper that returns true only for (autopair-on, Pending). Paired/Rejected confirmations and the manual ConnectionPage path (which calls App.ShowPairingPendingNotification directly, bypassing OnPairingStatusChanged) are unaffected.

Tests (LocalGatewaySetupAutoPairFlagTests, +12 cases): IsAutoPairingWindowsNode toggles only during Phase 14 (asserts false during Phase 12 callback, true during Phase 14 callback, false after run); flag resets even if Phase 14 throws; ShouldSuppressPairingPendingNotification theory covers all 6 (autopair × status) combos plus null-engine; all assert that Paired/Rejected and out-of-scope Pending pass through. Also exposed shared helpers in LocalGatewaySetupTests as internal nested classes for reuse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
indierawk2k2 referenced this pull request in indierawk2k2/openclaw-windows-node May 7, 2026
…rtifact

Fix #1 — validation script env-var alignment (Scott's PR openclaw#274 backlog item):
- scripts\validate-wsl-gateway.ps1 was setting OPENCLAW_TRAY_APPDATA_DIR
  and OPENCLAW_TRAY_LOCALAPPDATA_DIR for isolation, but SettingsManager
  reads OPENCLAW_TRAY_DATA_DIR. Result: validation runs touched real
  %APPDATA%\OpenClawTray\settings.json, causing operator-auth contamination
  in earlier test rounds.
- Set OPENCLAW_TRAY_DATA_DIR alongside the existing isolation env vars.
- Document the canonical env-var contract in docs\wsl-owner-validation.md.
- Add SettingsManagerIsolationTest asserting OPENCLAW_TRAY_DATA_DIR
  redirects writes away from real %APPDATA%.

Fix #2 — remove .squad\decisions\inbox\aaron-uninstall-plan.md from PR diff:
- The file is an agent planning artifact unrelated to the WSL gateway port
  and was flagged in RubberDucky's adversarial review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
indierawk2k2 referenced this pull request in indierawk2k2/openclaw-windows-node May 7, 2026
…detection (PR openclaw#274 must-fix openclaw#6)

- Add OnboardingExistingConfigGuard service: detects existing token, bootstrap
  token, non-default gateway URL, device identity tokens, and completed/running
  setup-state.json (sync); async variant probes WSL distro list.

- Gate #1 (SetupWarningPage): warn-and-confirm inline section replaces 'Set up
  locally' button when existing config detected. Shows immediately on page load
  per Mike's directive. Lists specifically what would be lost (token, device
  pairing, gateway URL). 'Replace my setup' sets ReplaceExistingConfigurationConfirmed.

- Gate #2 (OnboardingWindow): default SetupPath=Advanced when existing config
  detected so returning users land on SetupWarning with Next button enabled
  (→ Connection page), not the local setup path.

- Gate #3 (LocalSetupProgressPage): defense-in-depth guard before engine
  construction blocks any env-override / deep-link path that bypasses
  SetupWarningPage. Error code: existing_config_gate.

- Gate openclaw#4 (LocalGatewaySetupEngineFactory): fail-closed check in CreateLocalOnly;
  throws InvalidOperationException when settings.Token exists and
  replaceExistingConfigurationConfirmed=false. Default is always strict.

- Conditional menu label (Mike refinement): tray flyout shows 'Reconfigure...'
  when existing config detected, 'Setup Guide...' otherwise.

- Add 5 localization keys in all 5 locales:
  Onboarding_SetupWarning_ReplaceHeading/Body/Confirm/Cancel, Menu_Reconfigure.

- Add 13 new tests: OnboardingExistingConfigGuardTests (8), SetupWarning
  GuardPolicyTests (2), LocalSetupProgressGuardTests (2), OnboardingStateTests
  +1, LocalGatewaySetupTests +2. Tray tests: 627/627 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
shanselman added a commit that referenced this pull request May 7, 2026
* Add Windows STT transcribe capability

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* stt: privacy hardening, localization, and test coverage

Review-driven cleanup on top of the initial stt.transcribe capability.
No behavior change for successful invocations.

Privacy:
* SttCapability no longer echoes the caller-supplied language tag in the
  "Invalid language tag" error, and no longer interpolates the underlying
  exception's Message into "Transcribe failed". Both could end up in the
  recent-activity stream and BuildSupportBundle output, which can be
  shared off-device. Full detail still goes to the local logger.
* App.OnNodeInvokeCompleted now sanitizes failed-invoke details for
  privacy-sensitive commands (stt.transcribe, camera.snap/clip,
  screen.snapshot/record). Recent activity and support bundles record
  only "privacy-sensitive | <ms> | error" instead of the raw error
  string. Non-privacy-sensitive commands keep the error text since it is
  useful for diagnostics and does not carry mic/camera args.
* Models.cs PermissionDiagnostics microphone detail now mentions
  stt.transcribe instead of "future voice features", so users hitting
  0x800455A0 see microphone in their permissions checklist as relevant.

Refactors for testability (no behavior change):
* New Services/NodeInvokeActivityFormatter.cs owns GetPrivacyClass and
  BuildDetails. App.OnNodeInvokeCompleted delegates to it.
* New Services/NodeCapabilityGating.cs owns the optional-capability
  predicates. NodeService.RegisterCapabilities calls into it instead of
  inlining "_settings?.NodeXxxEnabled" checks. Privacy-sensitive
  defaults stay off; everything else stays default-on.
* Both helpers are linked into OpenClaw.Tray.Tests.

Localization:
* SettingsWindow.xaml gains x:Uid for every TTS and STT control. The
  literal Text/Header/PlaceholderText values are kept as dev-time
  fallbacks, matching the SettingsTokenTextBox and SettingsMcpDescription
  pattern already in the file.
* en-us, fr-fr, nl-nl, zh-cn, and zh-tw .resw files gain matching
  entries for the 14 new TTS/STT keys. Brand names (ElevenLabs),
  command names (tts.speak, stt.transcribe, gateway.nodes.allowCommands,
  MSIX), BCP-47 tags, and the eleven_multilingual_v2 model identifier
  are kept verbatim across all locales.
* SettingsMcpDescription.Text in all five locales now lists "microphone"
  and "speakers" alongside camera/screen/canvas so the local MCP-server
  description reflects the full Phase 1 + Phase 2 voice surface.

Tests:
* Two new privacy regression tests in CapabilityTests verify that an
  invalid language and a thrown handler exception never leak their text
  into the response error.
* New NodeInvokeActivityFormatterTests pin the privacy-class table, the
  sanitized details for privacy-sensitive failures, and the full
  ActivityStreamService.BuildSupportBundle path.
* New NodeCapabilityGatingTests pin that tts.speak and stt.transcribe
  default off (including for null settings) and that the two capabilities
  are independent consent surfaces.
* New SettingsWindowLocalizationCoverageTests parses SettingsWindow.xaml
  and asserts every new TTS/STT x:Uid resolves to the expected
  .Header/.Text/.Content/.PlaceholderText keys in en-us.
* ActivityStreamServiceTests and NodeInvokeActivityFormatterTests now
  share a non-parallel xUnit collection because ActivityStreamService is
  a static singleton; running both classes in parallel could otherwise
  cause flaky support-bundle assertions.
* NodeCapabilityGatingTests cleans up its temp settings directories.

Cleanup:
* Drop "Phase 2" wording from SpeechToTextService.cs; the resw section
  comments referring to "Phase 1 TTS / Phase 2 STT" are likewise
  reworded to plain "TTS / STT settings". Phase numbering is a planning
  artifact and should not appear in the codebase.

Validation:
* .\build.ps1
* dotnet test tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj
  --no-restore  (1173 passed, 20 skipped)
* dotnet test tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj
  --no-restore  (465 passed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove fake/sample data from 6 UI pages

Replace constructor-injected sample data with empty/loading states:
- UsagePage: remove fabricated provider costs and daily data
- SessionsPage: remove 3 fake AI conversation sessions
- NodesPage: remove fake Desktop-PC/MacBook-Pro nodes
- ChannelsPage: remove fake Telegram/WhatsApp channels
- SkillsPage: remove fake skills and stale 'API not yet wired' warning
- CronPage: remove fake cron jobs, stale warning, fix hardcoded defaults

All pages now show proper empty states until real gateway data arrives.
The Skills and Cron APIs were already fully wired; the warnings were
simply outdated and misleading.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: add voice/audio support with local Whisper STT

Add full voice interaction capabilities to the Windows node:

Core audio pipeline:
- NAudio WASAPI microphone capture with MTA thread initialization
- Energy-based voice activity detection with hysteresis
- Whisper.net speech-to-text with multi-threaded inference
- Pre-buffer to capture speech onset before VAD triggers
- Auto-download of Whisper models from HuggingFace

Voice overlay window:
- Modern WinUI 3 floating window with Mica backdrop and custom title bar
- Chat-style transcript bubbles with segment consolidation
- Real-time audio level visualization
- Start/Stop, Mute, and Settings controls

STT node capability:
- stt.listen and stt.status MCP commands for agent-initiated listening
- Follows existing capability pattern (like TTS)

Voice settings page:
- Model size selection (tiny/base/small) with download management
- Language selection (auto-detect + 9 languages)
- Silence timeout slider
- TTS voice picker with Windows neural voice enumeration
- ElevenLabs provider configuration
- Voice preview button

Integration:
- Tray menu Voice item
- Ctrl+Alt+Shift+V global hotkey for push-to-talk
- Deep links: openclaw://voice, openclaw://voice-stop
- Gateway chat responses shown in voice overlay
- TTS response playback with mic muting to prevent echo
- Capabilities page STT toggle
- Hub navigation Voice & Audio page

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Merge master into user/rbrid/stt-capability

Master refactored 8 separate windows into a unified Hub app (#272), which
removed src/OpenClaw.Tray.WinUI/Windows/SettingsWindow.xaml(.cs) and
WebChatWindow.xaml.cs. Node-capability toggles now live in
Pages/CapabilitiesPage as a code-built list (one icon + label per
capability) instead of an XAML page with x:Uid-localized headers.

Conflict resolution and re-integration:

* Accepted master's deletion of SettingsWindow.xaml, SettingsWindow.xaml.cs,
  and WebChatWindow.xaml.cs. The TTS/STT controls and code-behind that this
  branch added to those files are obsolete with the new Hub UI.

* Pages/CapabilitiesPage.xaml.cs gains a Speech-to-Text toggle alongside the
  existing Camera/Canvas/Screen/Location/TTS toggles, plus 'stt' in the
  active-capabilities summary string. This is the natural minimal alignment
  with the new pattern: one capability = one entry in the toggle list.

* The TTS provider / ElevenLabs key/voice/model UI that this branch had
  added is dropped because master removed the corresponding settings
  surface entirely. The backend services (TextToSpeechService,
  ElevenLabsTextToSpeechClient) and the SettingsManager keys are intact;
  the values can be set via direct settings.json edit until a new UI
  surface lands.

* Resolved 5 .resw conflicts (en-us, fr-fr, nl-nl, zh-cn, zh-tw) by taking
  master's content. All TTS/STT resource keys this branch had added are
  removed because the controls referencing them are gone. The earlier
  SettingsMcpDescription update (adding 'microphone' and 'speakers' to
  the capability list) is outside the conflict region and is preserved.

* Deleted tests/OpenClaw.Tray.Tests/SettingsWindowLocalizationCoverageTests.cs.
  It pinned that 14 specific x:Uids on SettingsWindow.xaml had matching
  resw entries; the controls and the file no longer exist.

Refactors from this branch survived the auto-merge cleanly:
* App.xaml.cs OnNodeInvokeCompleted still delegates to
  NodeInvokeActivityFormatter for privacy-class scrubbing.
* NodeService.RegisterCapabilities still calls NodeCapabilityGating
  predicates for every optional capability, including TTS and STT.

Validation:
* .\build.ps1
* dotnet test tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj
  --no-restore  (1183 passed, 20 skipped)
* dotnet test tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj
  --no-restore  (418 passed; restore required first because master's
  Tray.Tests now links GatewayDiscoveryService.cs which needs Zeroconf)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* stt/tts: refill settings UI gaps after the unified Hub merge

Master's Hub refactor (#272) removed the per-capability detail UI that
previously lived on SettingsWindow. The capability backends are intact but
have no in-app surface anymore: STT had no way to set the BCP-47 language
tag, and TTS had no way to pick the provider, ElevenLabs API key, voice
ID, or model without hand-editing settings.json.

CapabilitiesPage.xaml gains two new detail cards beneath the capability
toggle grid, mirroring the existing McpCard pattern (visible only when
the capability is enabled):

* SttCard:
    - Language TextBox bound to SttLanguage.
    - Commits on LostFocus or Enter.
    - Empty input restores the "en-US" default rather than persisting "".
    - Validates with SttCapability.NormalizeLanguageTag before saving so a
      typo in Settings cannot ship a broken default to the WinRT recognizer.
    - Status text never echoes the user-supplied tag back on the failure
      path; only the local UI affordance shows it (the activity stream
      / support bundle path was already privacy-scrubbed by an earlier
      commit on this branch).

* TtsCard:
    - Provider ComboBox (Windows built-in / ElevenLabs).
    - ElevenLabs sub-panel becomes visible only when that provider is
      selected. Holds API key (PasswordBox), voice ID, and model.
    - API key handling: when a key is already saved we render a fixed
      mask sentinel ("••••••••") instead of any plaintext. Saving the
      form treats the sentinel as "keep current key" so the user can
      change voice ID / model without retyping the key, and rotation
      requires explicitly typing a new key. The on-disk DPAPI encryption
      done by SettingsManager is unchanged.
    - All ElevenLabs fields commit on LostFocus.

SttCapability.NormalizeLanguageTag is promoted from private to public so
the UI validates against exactly the rule the wire protocol applies. No
behavior change for the capability itself.

Validation:
* .\build.ps1
* dotnet test tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj
  --no-restore  (1183 passed, 20 skipped)
* dotnet test tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj
  --no-restore  (418 passed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: extend privacy class + tests for stt.listen and stt.status

NodeInvokeActivityFormatter.GetPrivacyClass now classifies any stt.*
command as privacy-sensitive, not just stt.transcribe. This catches
stt.listen (microphone capture) and stt.status (engine internals)
under the same scrubbing rules in the activity stream / support
bundle, and keeps the rule simple ("anything in the stt namespace").

Tests added:
* GetPrivacyClass: stt.listen, stt.status, stt.future-command rows.
* PrivacySensitive_FailedInvoke_OmitsErrorTextFromDetails: theory
  rows for stt.listen and stt.status alongside the existing
  stt.transcribe / camera.* / screen.* coverage.
* SttCapabilityTests: full coverage of the unified surface
  - Listen: timeoutMs clamps (below min, above max), default language
    "auto", invalid language rejected without echo, handler not
    wired, handler exception sanitized to "Listen failed", segments +
    engine metadata round-trip, cancellation.
  - Status: handler not wired, handler exception sanitized to
    "Status failed", per-engine readiness round-trip with download
    progress.
  - NormalizeLanguageTag: BCP-47 tags + "auto" sentinel
    (case-insensitive, normalized to lowercase) accepted; underscore
    / spaces / "automatic" rejected.
* SettingsRoundTripTests: round-trips SttEngine, SttModelName,
  SttSilenceTimeout, VoiceTtsEnabled, VoiceAudioFeedback through
  SettingsData.ToJson / FromJson.

Validation:
* .\build.ps1
* dotnet test tests/OpenClaw.Shared.Tests --no-restore  (1266 passed,
  20 skipped)
* dotnet test tests/OpenClaw.Tray.Tests --no-restore  (425 passed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: STM, locale audit, and coverage tests for STT/TTS card

* Added E:\OpenClawWindowsNode\Audio_STM.md — full STRIDE analysis
  of the merged audio surface (assets, trust boundaries, per-component
  threats, cross-references to code + tests, follow-up backlog).
* Promoted every new STT/TTS card string in CapabilitiesPage.xaml to
  x:Uid + resw entries across all five locales (en-us, fr-fr, nl-nl,
  zh-cn, zh-tw): engine picker labels, language input + help, "More
  voice settings…" link, TTS provider picker, ElevenLabs sub-panel
  fields. Brand names (ElevenLabs), the "auto" BCP-47 sentinel, and
  the eleven_multilingual_v2 model identifier are kept verbatim and
  registered as InvariantOrDeferred in LocalizationValidationTests.
* Added CapabilitiesPageLocalizationCoverageTests — pins every new
  STT/TTS x:Uid against expected resw key suffixes (.Text, .Header,
  .Content, .PlaceholderText) so a future hardcoded-string regression
  fails fast.

Validation:
* .\build.ps1
* dotnet test tests/OpenClaw.Shared.Tests --no-restore  (1266 passed,
  20 skipped)
* dotnet test tests/OpenClaw.Tray.Tests --no-restore  (461 passed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: extract SttEngineSelector + tests for engine selection rules

The engine-selection logic that NodeService.OnSttTranscribeAsync /
OnSttListenAsync / OnSttStatusAsync inline-implemented is now a pure
helper in Services/SttEngineSelector.cs and is consumed identically
from all three handlers. No behavior change.

Selector rules (pinned by SttEngineSelectorTests, 21 cases):
* Whisper preference + Whisper ready → Whisper, no fallback.
* Whisper preference + Whisper NOT ready + WinRT ready → WinRT,
  fallbackReason="whisper-model-not-ready". Happy degradation while
  the model downloads on first launch.
* Whisper preference + neither ready → keep Whisper preference,
  fallbackReason="whisper-and-winrt-unavailable". Dispatch fails;
  the user's preference is reported unchanged so stt.status is
  honest about what they asked for.
* WinRT preference + WinRT ready → WinRT, no fallback.
* WinRT preference + WinRT ready + Whisper ALSO ready → still WinRT.
  Critical invariant: explicit user choice is never silently upgraded
  to Whisper when the model finishes downloading.
* WinRT preference + WinRT NOT ready → keep WinRT,
  fallbackReason="winrt-unavailable". Same invariant: do not fall
  back to Whisper without explicit user opt-in.
* null/empty/whitespace/unknown engine string → treat as Whisper
  preference. A typo in settings.json must not hard-fail STT.
* Case- and whitespace-insensitive parsing of "whisper" / "winrt".

Engine identifier constants are mirrored locally on
SttEngineSelector.SharedConstants (free of cross-assembly deps);
MirroredConstantsMatchSttCapability pins they stay in sync.

Validation:
* .\build.ps1
* dotnet test tests/OpenClaw.Shared.Tests --no-restore  (1266 passed,
  20 skipped)
* dotnet test tests/OpenClaw.Tray.Tests --no-restore  (482 passed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: security review fixes from STM walkthrough

Findings from the post-merge security review (full review recorded in
the session at files/security-review.md and reflected in the STM
follow-up backlog):

CRITICAL (1 fixed, 1 deferred):
* I-1 — UI now warns that selecting WinRT honors the Windows Online
  speech recognition toggle and may upload audio to Microsoft when
  that toggle is on. CapabilitiesPage SttEngineHint text updated to
  steer users to Whisper for fully local processing.
* S-4 / T-1 — DEFERRED: SHA-256 verification of the Whisper model
  (download AND load time) requires embedding canonical hashes for
  tiny / base / small from HuggingFace. Tracked as a Critical
  pre-GA follow-up in Audio_STM.md section 6, not blocking this
  merge. (Existing TLS + system trust chain remains the only check.)

HIGH (3 fixed):
* S-3 / D-1 — NodeService.OnSttListenAsync now enforces a 1-second
  cooldown between successive stt.listen invocations. Imperceptible
  to a real user but throttles a hostile loop from a compromised
  gateway. Throws InvalidOperationException("Listen rate limit")
  which the SttCapability sanitization wraps as "Listen failed".
* D-7 — AudioPipeline.CleanupCapture now wraps event-detach,
  capture.Dispose, and CTS dispose in independent try/catch blocks
  so a failure in one step doesn't leak the NAudio WasapiCapture
  COM object (which would hold the mic LED lit until process exit).
  Also added CleanupCapture() calls in StartAsync's two catch
  branches so the mic is released after a failed start.
* I-2 — VoiceOverlayWindow audit confirmed no transcript text reaches
  ActivityStreamService. Status: PIN, no code change needed.

MEDIUM (1 fixed):
* NEW-1 — TtsCapability previously returned \$"Speak failed: {ex.Message}",
  which can leak ElevenLabs key prefixes from 401 responses or
  device names from OS audio errors into the support bundle.
  Now returns a fixed "Speak failed" matching the SttCapability
  pattern. NodeInvokeActivityFormatter.GetPrivacyClass also now
  classifies tts.* as privacy-sensitive (was metadata) so failed-
  invoke details are uniformly scrubbed.

PIN (no change needed, confirmed by review):
* T-3 — SttModelName path-traversal: WhisperModelManager validates
  against the {tiny, base, small} allow-list before any Path.Combine.
* I-4 — ElevenLabs key DPAPI-encrypted at rest.
* I-5 — ElevenLabs key UI shows masked sentinel; plaintext never
  re-rendered after save.
* I-8 / PI-5 — stt.status response carries no PII (only readiness
  strings, engine name, capability flags, numeric download progress).
* PI-3 — Validation/handler errors don't echo caller input or
  exception text across stt.* and now tts.* as well.

Test additions:
* Speak_HandlerException_DoesNotLeakExceptionMessageIntoError —
  pins the new TTS privacy invariant with an "ElevenLabs 401:
  invalid key sk-secret-prefix" payload.
* Speak_ReturnsError_WhenHandlerThrows updated to assert the exact
  sanitized "Speak failed" message instead of leaking ex.Message.
* GetPrivacyClass theory rows now cover tts.speak and
  tts.future-command as privacy-sensitive (was metadata).

Validation:
* .\build.ps1
* dotnet test tests/OpenClaw.Shared.Tests --no-restore  (1271
  passed, 20 skipped)
* dotnet test tests/OpenClaw.Tray.Tests --no-restore  (483 passed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: drop WinRT SpeechRecognizer + SAPI fallback; Whisper-only

Both legacy stacks are removed; SttCapability now dispatches every
stt.* call to a single Whisper engine via VoiceService. When the
Whisper model is not yet downloaded, handlers return a clear error
pointing the caller at the Voice Settings page download button —
there is no automatic fallback engine.

Rationale (from the discussion with Ranjesh):
* WinRT SpeechRecognizer is an old API that fails to activate in
  unpackaged tray builds (the long-standing 0x800455A0 issue) and,
  when the OS Online speech recognition toggle is on, may upload
  audio to Microsoft cloud — at odds with our local-first posture.
* System.Speech (desktop SAPI) is even older and has no value over
  Whisper for any modern scenario.
* Carrying two engines complicated the merge with no real upside
  now that Whisper.net runs reliably on every supported PC.

Removed:
* src/OpenClaw.Tray.WinUI/Services/SpeechToText/SpeechToTextService.cs
  (the WinRT + SAPI engine).
* src/OpenClaw.Tray.WinUI/Services/SttEngineSelector.cs (no engines
  to select between).
* tests/OpenClaw.Tray.Tests/SttEngineSelectorTests.cs.
* System.Speech NuGet package reference (was duplicated; both
  copies removed).
* SttEngine setting (SettingsData + SettingsManager round-trip).
* SttCapability.EngineWinRt and DefaultEngine constants.
* SttTranscribeResult.EngineFallbackReason and
  SttListenResult.EngineFallbackReason — no fallback to report.
* CapabilitiesPage Engine ComboBox + the engine-related UI strings
  in all five locales.
* The "Windows built-in may upload audio" caveat (no longer relevant).

Simplified:
* SttStatusResult: replaced PreferredEngine/EffectiveEngine plus
  per-engine readiness blocks with a single Engine + Readiness pair
  (engine is always "whisper" today; the field stays so a future
  engine doesn't break the wire).
* NodeService.OnSttTranscribeAsync / OnSttListenAsync /
  OnSttStatusAsync: dropped selector logic + WinRT marshalling.
  When VoiceService.IsWhisperReady is false, throw clear
  "Whisper model not downloaded" — wrapped to "Transcribe failed"
  / "Listen failed" by SttCapability's privacy sanitizer.
* CapabilitiesPage STT card hint surfaces model download state
  ("Whisper model is ready" / "downloading" / "not downloaded —
  open More voice settings…").
* McpToolBridge curated descriptions: drop engineFallbackReason
  field and the per-engine blocks from stt.status.

Tests:
* CapabilityTests.Status_ReturnsEngineReadiness rewritten for the
  flat shape; now also asserts no language/path strings appear in
  the JSON (tightens PI-5 enforcement).
* SettingsRoundTripTests: dropped SttEngine field assertions.
* CapabilitiesPageLocalizationCoverageTests: dropped engine ComboBox
  Uids from the contract list.
* LocalizationValidationTests: removed the engine ComboBox keys
  from the InvariantOrDeferred allow-list (no longer needed; the
  invariants list now only protects "auto", "ElevenLabs", and
  "eleven_multilingual_v2").

Validation:
* .\build.ps1
* dotnet test tests/OpenClaw.Shared.Tests --no-restore  (1271 passed,
  20 skipped)
* dotnet test tests/OpenClaw.Tray.Tests --no-restore  (456 passed)

Audio_STM.md and Audio_FollowUps.md updated to reflect the engine
removal (smaller test-seam refactor surface; I-1 "WinRT online
speech caveat" follow-up is retired).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: add Piper TTS provider via Sherpa-ONNX

Adds a third TTS provider, "piper", that runs Piper voices fully
locally on this PC through the official Sherpa-ONNX .NET binding
(org.k2fsa.sherpa.onnx 1.13.0). No cloud egress; the voice model
downloads once to %LOCALAPPDATA%\OpenClawTray\models\piper\<voice-id>\
and is reused across calls.

Backend (OpenClaw.Shared/Audio/PiperVoiceManager.cs):
* Curated catalog of 6 starter voices (en-US ×2, en-GB, fr-FR, de-DE,
  zh-CN) sourced from the sherpa-onnx tts-models GitHub release
  tarballs — these are repackaged Piper voices that include the
  language-specific espeak-ng-data, so the user only downloads one
  archive per voice instead of model + tokens + espeak separately.
* Download with progress callback; extraction via OS-bundled tar.exe
  (Win10 1803+); atomic per-voice directory layout; cleanup of
  partial files on failure or cancellation.
* IsVoiceDownloaded / GetVoiceSize / DeleteVoice for the (forthcoming)
  Voice Settings page UI.
* TODO marker for SHA-256 verification (Audio_FollowUps.md §2).

Tray service (OpenClawTray/Services/TextToSpeech/PiperTextToSpeechClient.cs):
* Wraps SherpaOnnx.OfflineTts; loads one voice at a time and reuses
  the loaded model across calls (load is the expensive ~200-500 ms
  step). Single-flight gate prevents concurrent generates from
  racing the same TTS instance.
* Inference runs on a background Task so cancellation can race the
  synthesis.
* Converts Sherpa's 32-bit float PCM samples to a standard 16-bit
  PCM mono WAV blob the WinUI MediaPlayer can play with no further
  transcoding.

Wiring (OpenClaw.Tray.WinUI/Services/TextToSpeech/TextToSpeechService.cs):
* Third branch in SpeakAsync's provider dispatch. SpeakWithPiperAsync
  resolves the voice from args.VoiceId or settings.TtsPiperVoiceId,
  fails with a "voice not downloaded" error pointing the user at
  Voice Settings if the file isn't present, and otherwise reuses the
  cached PiperTextToSpeechClient (rebuilds it only when the voice id
  changes).
* TextToSpeechService.PiperVoices exposed so the Voice Settings page
  can drive download / delete from the same instance.

UI (OpenClaw.Tray.WinUI/Pages/CapabilitiesPage.xaml + .xaml.cs):
* Added Piper as the first ComboBoxItem on the TTS provider picker
  ("Piper (local ML, recommended)"). Resw entries across all 5
  locales (en-us, fr-fr, nl-nl, zh-cn, zh-tw).
* UpdateTtsCard reads TtsProvider with a 3-way switch (piper /
  windows / elevenlabs); unknown / null defaults to Piper.

Capability + settings:
* TtsCapability.PiperProvider = "piper" wire constant.
* SettingsData.TtsPiperVoiceId / SettingsManager.TtsPiperVoiceId,
  default "en_US-amy-low" (~50 MB, smallest English voice).
  Round-trip preserved through Save/Load.

Tests:
* SettingsRoundTripTests asserts TtsPiperVoiceId persists.
* CapabilitiesPageLocalizationCoverageTests pins the new
  CapabilitiesPage_TtsProviderPiper x:Uid against en-us.
* PiperVoiceManager + PiperTextToSpeechClient have no unit tests
  yet — same blocker as the rest of the audio engine layer
  (Audio_FollowUps.md §1: needs interface extraction first).

Audio_FollowUps.md §3 updated with a "Status update — basic Piper
plumbing landed" subsection enumerating exactly what shipped and
what remains (Voice download UI, manager tests, SHA-256 verification,
spike validation).

Validation:
* .\build.ps1
* dotnet test tests/OpenClaw.Shared.Tests --no-restore  (1271 passed,
  20 skipped)
* dotnet test tests/OpenClaw.Tray.Tests --no-restore  (462 passed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: voice download UI, Piper-as-default, first-listen polish

User-visible

* New Piper voice download panel on the Voice & Audio page
  (catalog of 6 voices, download with progress, delete, preview).
* Piper is now the default TTS provider for fresh installs.
* Read responses aloud toggle now drives every chat reply,
  not only voice-overlay sessions.
* Voice Overlay's Settings button opens the Voice & Audio page
  (was a no-op stub).
* First Whisper auto-download surfaces a status line in the
  Voice Overlay so the user knows the silent ~140 MB fetch is
  why nothing is being transcribed yet.
* Speech Model card refreshes its 'Model ready / Download
  required' status whenever the page becomes visible, even if
  NodeService hasn't wired its VoiceService yet.
* Stale 'Windows built-in' fallback text removed from the
  Speech-to-Text card description (5 locales). Whisper has been
  the only engine since ff11467.
* Width bumps so labels no longer truncate (the Speech Model
  size combo, the Provider combo).
* Dropped 'STT' jargon from the Language ComboBox header.
* Fixed misleading '~50-80 MB each' Piper size copy (real range
  is ~25-150 MB depending on quality).

Plumbing

* New SettingsRequested event on VoiceOverlayWindow; App hooks
  it to ShowHub('voice').
* TtsCapability.ResolveProvider falls back to Piper.
* App.OnNotificationReceived no longer gates TTS on
  VoiceMode != Inactive.
* VoiceSettingsPage.UpdateModelStatus queries the file system
  via WhisperModelManager directly so it works before
  NodeService finishes lazy-init of VoiceService.
* VoiceService.InitializeAsync fires DiagnosticMessage events
  around silent VAD/Whisper auto-downloads.

Tests: Shared 1271 / Tray 462 (default-provider asserts updated).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: address rubber-duck review (Highs #2-#5, Mediums #6-#8, Low #9)

High #2: Reuse a singleton TextToSpeechService for chat replies
* App.SpeakResponseAsync now goes through NodeService.TextToSpeech
  (a new public accessor on the existing _textToSpeechService field)
  instead of constructing a fresh service per call. Cached Piper
  client is reused across replies; the service-internal _playbackGate
  + _activePlayer now actually serialize back-to-back replies, and
  Interrupt=true takes effect.

High #3: Per-provider VoiceId routing
* New TtsWindowsVoiceId setting (round-tripped via SettingsManager
  + SettingsData; SettingsRoundTripTests assert it).
* SpeakResponseAsync no longer passes _settings.TtsElevenLabsVoiceId
  as a generic VoiceId; the per-provider Speak* paths each look up
  their own setting (TtsPiperVoiceId / TtsWindowsVoiceId /
  TtsElevenLabsVoiceId).
* SpeakWithWindowsAsync falls back to TtsWindowsVoiceId when
  args.VoiceId is blank.
* VoiceSettingsPage.OnWindowsVoiceChanged writes TtsWindowsVoiceId
  (was overwriting TtsElevenLabsVoiceId, a real cross-provider bug).

High #4: stt.listen returns a complete utterance, not the first segment
* New AudioPipeline.UtteranceTranscribed event fires once per silence-
  bounded utterance with all Whisper segments aggregated and an
  immutable Segments snapshot.
* VoiceService bubbles it as UtteranceCompleted.
* ListenOnceAsync subscribes to UtteranceCompleted (drops the
  per-fragment accumulator) so multi-segment utterances no longer
  return truncated text.

High #5: Voice Overlay submits one chat message per utterance
* OnTranscriptionReceived keeps the per-fragment streaming bubble
  update; chat submission moved to a new OnUtteranceCompleted
  handler so the gateway sees one message per spoken utterance.

Medium #6: Per-asset cancellation tokens in VoiceSettingsPage
* Split _downloadCts into _whisperDownloadCts and _piperDownloadCts
  so starting a Piper download no longer cancels an in-flight
  Whisper download (and vice versa).

Medium #7: Preflight tar.exe before Piper download
* PiperVoiceManager.EnsureExtractorAvailable runs a fast
  `tar --version` check before any network I/O. Downlevel Windows
  users now get a clear actionable error instead of a wasted ~50-150
  MB download that would later fail at extraction.

Medium #8: Refresh stale MCP tool descriptions
* stt.transcribe / stt.listen / stt.status now describe the single
  Whisper engine surface (no preferredEngine / effectiveEngine /
  engineFallbackReason); stt.listen description explicitly notes
  the result is the full silence-bounded utterance.
* tts.speak description includes `piper` in the provider list and
  notes the fresh-install default.
* Updated McpToolBridgeTests assertion for the new shape.

Low #9: Per-asset single-flight in download managers
* Both WhisperModelManager and PiperVoiceManager wrap their
  Download*Async in a static ConcurrentDictionary<string,Task> keyed
  on the canonical asset ID. Concurrent calls for the same asset
  await the same in-flight Task instead of racing on the same .tmp
  file. Failed downloads remove themselves from the table so a fresh
  retry isn't blocked.

Tests: Shared 1271 / Tray 462. Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: SHA-256 verification of Whisper models and Piper voices

Critical (rubber-duck #1) — fail-closed integrity check before install.

* New `Sha256` field on `WhisperModelInfo` and `PiperVoiceInfo`.
* All 9 catalog entries (3 Whisper models + 6 Piper voices) carry a
  pinned lowercase-hex SHA-256, captured against the live HuggingFace
  and sherpa-onnx GitHub releases on 2026-05-05.
* Download core methods now:
    1. Refuse outright if the catalog entry has no pinned hash
       (`InvalidOperationException`).
    2. Compute SHA-256 of the temp file BEFORE the atomic rename
       (Whisper) or BEFORE the tar extraction (Piper).
    3. On mismatch, throw `System.Security.SecurityException`,
       delete the temp file, and let the catch block tear down any
       half-installed directory. Sanitized message — does NOT echo
       the actual hash (no confirmation oracle).
* New `AssetHashPinningTests` enforces that every catalog entry has
  a 64-hex-char SHA-256 and an https URL — future additions that
  forget the hash now break the build.

Audio_FollowUps.md §2 updated:
* Status block at the top documents what landed today.
* Pre-public-release TODO list trimmed to: independent re-verification
  of the pinned hashes, on-load verification (not just on download),
  and a future signed-manifest format so updates don't require a tray
  rebuild. The original detailed design notes are preserved as the
  spec for that next iteration.

Tests: Shared 1275 / Tray 462. Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: Download Model button works without VoiceService

OnDownloadClick previously routed through VoiceService.DownloadModelAsync,
which silently no-op'd whenever _voiceService was null — and _voiceService
is only constructed inside NodeService.RegisterCapabilities (which runs on
Connect / StartLocalOnly, and only when NodeSttEnabled is true). A user
who toggled STT on without reconnecting, or who hadn't enabled MCP-only
mode, would tap Download and see nothing happen.

Construct a WhisperModelManager directly from
SettingsManager.SettingsDirectoryPath and download via that. Same
on-disk result as the VoiceService auto-download path, but available
regardless of NodeService lifecycle state. Same SHA-256 verification
applies (the manager owns it).

Tests: Tray 462 (no change in surface).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ux: Companion rename, expanded NavView memory, right-click opens Hub

Three coordinated tweaks based on the morning UX review.

1. Right-click on the tray icon now opens BOTH the popup quick-menu
   AND the companion app window. ShowHub gained an `activate` flag;
   for this code path we call ShowHub(activate:false) so the Hub
   surfaces via AppWindow.Show(activateWindow:false) and the popup
   (which is light-dismiss) stays the foreground window. Without this
   the Hub's Activate() would steal focus and dismiss the popup.

2. NavigationView pane mode is now expanded by default and remembered
   across sessions. PaneDisplayMode flipped from Auto to Left, and a
   new HubNavPaneOpen setting (default true) is round-tripped via
   SettingsManager / SettingsData. PaneOpening / PaneClosing handlers
   on HubWindow persist the user's last toggle. SettingsRoundTripTests
   covers the new field.

3. Renamed the mascot from 'Molty' to 'Companion' across the surface:

   User-facing strings:
   * VoiceOverlayWindow Title and header text → `Companion Voice`.
   * VoiceSettingsPage section header → `🔊 Companion Voice`.
   * Both Preview-button sample texts (Windows + Piper) now say
     `Hello! This is your Companion speaking.`.

   Code identifiers (HomePage):
   * MoltyRing → CompanionRing
   * MoltyProgressRing → CompanionProgressRing
   * UpdateMoltyRing → UpdateCompanionRing
   * Comment `<!-- Molty mascot -->` → `<!-- Companion mascot -->`

   `grep -i molty src/` returns zero hits.

Tests: Shared 1275 / Tray 462. Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ux: rubber-duck #2 — restore minimized Hub on right-click; pin pane default

Two findings from the second rubber-duck pass.

Medium: ShowHub(activate:false) was a no-op when the Hub was previously
minimized. AppWindow.Show(activateWindow:false) does not restore minimized
windows. Detect OverlappedPresenter.State == Minimized first and
Restore(activateWindow:false) so the window actually surfaces behind the
popup, then call Show.

Low: regression test for HubNavPaneOpen migration. Settings files written
before this field existed must deserialize to true (NavView expanded).
Added an explicit FromJson(\"{}\") assertion plus pinned the field's
default in MissingFields_UseDefaults and BackwardCompatibility_OldSettings*
so a future refactor can't silently flip new installs to a collapsed pane.

Tests: Tray 463 (one new). Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: deep-link parser strips trailing slash before query (#-)

The Windows shell canonicalizes openclaw://send?args=... to
openclaw://send/?args=... before handing it to us. The previous
implementation called TrimEnd('/') on the WHOLE remainder before
splitting off the query, so the trailing slash before the '?' was
never trimmed and Path came out as 'send/' instead of 'send'.

Trim the slash from the path SEGMENT after splitting off the query.

Three new theory cases pin the regression for send / agent / activity
deep links — categories that all carry query parameters in the
launcher canonicalized form. Existing TrailingSlash test (no query)
still passes with the new placement.

Credit to the parallel Copilot session for catching this.

Tests: Shared 1275 / Tray 466 (3 new). Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: don't drop final utterance on stop or timeout; bound transcription queue; normalize BCP-47

Three coordinated STT pipeline fixes from the latest review.

#1 (High) — Buffered speech was discarded on Stop/Timeout
* AudioPipeline.StopAsync used to call _cts.Cancel() BEFORE flushing,
  and the flush passed the canceled token straight into Whisper.net
  (which honored cancel and dropped the final utterance). Reordered
  to: stop capture -> flush with a fresh CancellationToken.None ->
  cancel _cts -> cleanup. Adds an overrideToken parameter on
  TranscribeSamplesAsync so the flush can opt out of the pipeline cancel.
* VoiceService.ListenOnceAsync used to throw TimeoutException as soon
  as the linkedCts fired, even when speech was actively buffered. It
  now waits on Task.WhenAny(utteranceTcs, timeoutSentinel), and on
  timeout it gives pipeline.StopAsync up to 2 s to flush — only then
  reports timeout. stt.transcribe inherits this fix.

#3 (Medium) — Whisper.net language mismatch
* SpeechToTextService.NormalizeForWhisper trims BCP-47 input down to
  the 2-letter ISO 639-1 primary subtag that Whisper.net's WithLanguage
  call expects. `en-US` -> `en`, `zh-Hans-CN` -> `zh`, garbage
  -> `auto`. Capability validator + MCP docs continue to advertise
  the wider BCP-47 shape (no breaking change for callers); this fixes
  the gap to Whisper.
* Result.Language now echoes the normalized form so the caller sees
  what Whisper actually used.

#4 (Medium) — Unbounded transcription queue
* Each VAD-bounded segment fired `_ = Task.Run(TranscribeSamplesAsync)`
  with no in-flight cap. SpeechToTextService gates Whisper work but
  callbacks accumulate behind the gate, each holding a sample buffer.
  Now bounded with Interlocked counter + MaxConcurrentTranscriptions
  cap (2). Excess segments are dropped with a clear DiagnosticMessage
  rather than silently queued — better UX than getting stale utterances
  arriving minutes after the user stopped speaking.

Tests: Shared 1291 / Tray 466 (16 new normalizer tests). Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: SHA-256 verification of Silero VAD model

Closes the inconsistency the buddy review flagged: Whisper and Piper
download paths are hash-pinned and fail closed on mismatch, but the
Silero VAD download path (VoiceService.DownloadVadModelAsync) was just
HTTPS + system trust chain — no integrity verification before File.Move
into the models directory.

* New SileroVadModelManifest holds the URL, SHA-256, and approximate
  size as public constants in OpenClaw.Shared.Audio. Hash captured from
  the upstream raw URL on 2026-05-05; same pre-public-release re-verify
  TODO as the other manifests (Audio_FollowUps.md §2).
* DownloadVadModelAsync now hashes the temp file with SHA-256 BEFORE
  the atomic rename. On mismatch it throws SecurityException and the
  catch block tears down the .tmp file. Sanitized error — does not echo
  the actual hash (no confirmation oracle).
* AssetHashPinningTests gains a SileroVadModel_HasPinnedSha256 case so
  a future renaming/forgetting of the constant trips the build.

Tests: Shared 1292 (1 new). Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: bring skill.md back in sync with capability registry

The SkillMdDriftTests pinning test was failing — 14 commands present in
McpToolBridge.KnownCommands had no matching ### heading in skill.md:

* The 4 new entries this branch added: stt.transcribe, stt.listen,
  stt.status, tts.speak.
* 10 pre-existing app.* entries (app.navigate, app.status, app.sessions,
  app.agents, app.nodes, app.config.get, app.settings.get, app.settings.set,
  app.menu, app.search) that already drifted before the audio work.

Fixing them all in one pass so the test goes green and stays green.
Each new section follows the existing format: H3 heading, brief
description, JSON-shaped param block, return shape.

Privacy + provider notes added for stt.* and tts.* so agent readers
understand: stt.* is local Whisper only and requires NodeSttEnabled,
tts.* defaults to Piper (local neural).

Tests: SkillMdDriftTests now passes. Shared 1292 / Tray 466. Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ux: throttle Whisper/Piper download progress UI; wire Re-download button

Two manual-test follow-ups on the Voice Settings page.

* Throttle progress UI updates to >=150 ms intervals on both the
  Whisper and Piper download paths. The streaming downloads emit a
  progress callback every ~80 KB chunk, so a 466 MB model produces
  ~5,800 dispatcher hops (Progress<T> + DispatcherQueue.TryEnqueue
  doubled the load). The dispatcher queue saturated and the app
  appeared frozen mid-download. Coalescing limits the rate to a few
  updates per second, with a forced final 100% report so the user
  never sees a stuck "99%" right before "Model ready". Also dropped
  the redundant inner DispatcherQueue.TryEnqueue (Progress<T> already
  marshals to the captured UI SyncContext).

* Re-download button now actually re-downloads. WhisperModelManager
  short-circuits DownloadModelAsync when the file is already present,
  so OnDownloadClick now calls the existing DeleteModel(modelName)
  first when the file is on disk. Net effect: delete -> fresh fetch
  -> SHA-256 re-verify -> atomic rename. Same on-disk result.

Tests: Shared 1292 / Tray 466 (no test surface change). Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* i18n: localize VoiceSettingsPage and VoiceOverlayWindow surfaces

Closes the buddy review's last finding. The new voice UI was English-only
hard-coded in both XAML and code-behind, while the rest of the tray (and
the freshly redone CapabilitiesPage STT/TTS card) reads from .resw via
x:Uid + LocalizationHelper.GetString.

Coverage:

* VoiceSettingsPage.xaml — every user-facing TextBlock / Header /
  ComboBoxItem / Button content / placeholder gets x:Uid (page title,
  card headers, STT toggle, model + language combos, voice chat
  controls, all 3 TTS provider items, Piper download/delete/preview,
  ElevenLabs slot, privacy note).
* VoiceOverlayWindow.xaml — header text, status badge, empty state,
  status text, start/stop label, mute + settings tooltips.
* VoiceSettingsPage.xaml.cs and VoiceOverlayWindow.xaml.cs — runtime
  status messages (download progress, model-ready, preview failures,
  pipeline state transitions, mute/listen state) now read from
  LocalizationHelper.GetString. Format strings use Lf(...) so {0}/{1}
  placeholders are honored under CurrentCulture.

Translations pinned for en-us / fr-fr / nl-nl / zh-cn / zh-tw —
~95 new keys per locale (475 total resw entries). Translations are
best-effort; native speakers should review pre-public-release.

LocalizationValidationTests:
* AllLocales_HaveExactlySameKeysAsEnUs ✅
* Resources_AreTranslatedAllOrNoneAcrossNonEnglishLocales ✅
  (added VoiceSettingsPage_StatusError + ElevenLabs sample-ID
  placeholder keys to the InvariantOrDeferred list — they're
  intentionally identical across locales)

Build green. Shared 1292 / Tray 466.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: stt.transcribe is now a true fixed-duration capture

Closes the buddy review's stt.transcribe finding. The handler used to
adapt SttTranscribeArgs into SttListenArgs and call ListenOnceAsync,
which inherited VAD-based silence shutdown — so a 5 000 ms request
would return after 1 s if the user stopped speaking. The advertised
contract (skill.md, McpToolBridge) promises bounded fixed-duration
capture, not silence-bounded.

Implementation:

* AudioPipeline.CaptureFixedDurationAsync — new top-level method that
  starts WASAPI capture, accumulates every resampled+gain-applied
  16 kHz mono sample into _fixedCaptureBuffer for exactly durationMs
  (or until cancellation), then returns the buffer. OnDataAvailable
  branches on a new _fixedCaptureMode flag and bypasses the VAD path
  entirely in this mode.

* VoiceService.TranscribeFixedDurationAsync — wraps
  CaptureFixedDurationAsync + SpeechToTextService.TranscribeAsync and
  returns SttTranscribeResult directly. Empty buffer (cancelled
  immediately or no audio) returns transcribed=false rather than
  throwing.

* NodeService.OnSttTranscribeAsync now calls TranscribeFixedDurationAsync
  instead of bouncing through ListenOnceAsync.

stt.listen behavior is unchanged.

Tests: Shared 1292 / Tray 466. Build green. (No new tests — exercising
this path requires a real WASAPI device. The capture/transcribe
boundary is tightly coupled to NAudio + Whisper.net, which were the
test seams already deferred to Audio_FollowUps.md §1.)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* i18n: localize VoiceOverlayWindow root window title

Adds x:Uid="VoiceOverlayWindow" on the WindowEx root, plus the
VoiceOverlayWindow_winexWindowEx_2.Title key in all 5 locale resw
files. Listed in InvariantOrDeferredResourceKeys so the parity test
allows the title to read identical "Companion Voice" in every locale —
matches the existing convention for ChatWindow / HubWindow /
CanvasWindow / TrayMenuWindow.

The visible header text and runtime status messages were already
localized; this just closes the gap on the actual OS-level window
title (alt-tab, taskbar).

Build green. Shared 1292 / Tray 466.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: gate stt.* on file presence, not in-memory load state

The MCP / wire-side stt.transcribe and stt.listen entry points
short-circuited with "Whisper model not downloaded" whenever
_voiceService.IsWhisperReady was false. That property reads
SpeechToTextService.IsModelLoaded — which is true only after the
model has been LOADED INTO MEMORY by EnsureInitializedAsync.

On a freshly-launched tray (or any state where the user hasn't
opened the Voice Overlay yet), the .bin file is on disk but the
model isn't loaded. The pre-flight check rejected the call before
the inner TranscribeFixedDurationAsync / ListenOnceAsync could run
EnsureInitializedAsync to load it lazily.

Net result: every first MCP STT call after launch failed with a
misleading "model not downloaded" error, even though the file was
right there.

Switch the pre-flight check to IsModelDownloaded (file on disk).
The lazy load happens inside the inner call as it always did.

Verified end-to-end via the local MCP HTTP server: tools/call
stt.transcribe with maxDurationMs:5000 returned a real transcript
("Hello, how is everybody doing?") on first invocation after a
fresh tray launch.

Tests: Shared 1292 / Tray 466. Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ux: voice UI testing round — localization, shutdown, and Capabilities cleanup

Three buckets of fixes from this afternoon's manual testing pass.

i18n: dot-suffix lookup bug in code-behind
* LocalizationHelper.GetString(X.Text) returns the raw key when the
  resource name has a dot — XAML x:Uid resolution interprets the
  trailing .Text as a property suffix, but direct programmatic
  lookup doesn't, so the resource map can't find it. Six call sites
  were displaying literal keys like "VoiceOverlayWindow_StatusBadge.Text"
  in the running UI.
* Added six dot-free code-only keys (BadgeReady, StatusReadyMessage,
  ButtonStartListening, ButtonDownloadModel, PiperButtonDownloadVoice,
  PreviewVoiceButtonContent) translated across all 5 locales, and
  swapped the call sites in VoiceOverlayWindow.xaml.cs and
  VoiceSettingsPage.xaml.cs to use them.

audio: Voice Overlay "Failed to encode audio features" on Stop
* Mid-encode interruptions from Whisper.net don't surface as a clean
  OperationCanceledException — they bubble up as misleading errors
  like "Failed to encode audio features." Pressing Stop while a
  transcription Task.Run was in-flight produced exactly that toast.
* AudioPipeline.StopAsync now drains in-flight transcriptions for up
  to 3 s before cancelling \_cts, so the user's last utterance has a
  chance to actually complete.
* TranscribeSamplesAsync's catch block suppresses errors when
  \_isStopping or the cancel token is set — those are expected
  shutdown-induced interruptions, not user-visible failures. Also
  sanitized the diagnostic toast (no raw ex.Message).

Capabilities page rework
* Removed the redundant Language TextBox + label + help + status
  block. The Voice & Audio page already owns the language picker via
  a curated ComboBox (the textbox accepted any string and silently
  failed validation on garbage like "foobar", which was a paper cut).
* "More voice settings…" hyperlink stays as the deep-link.
* Speech-to-Text card hint now reads file presence directly via a
  fresh WhisperModelManager rooted at SettingsManager.SettingsDirectoryPath
  (instead of hub.VoiceServiceInstance?.IsWhisperReady, which is null
  on a freshly-launched tray and reads "loaded into memory" rather
  than "file on disk"). Same trick used by VoiceSettingsPage's
  UpdateModelStatus.
* Updated the Capabilities help text in all 5 locales to say "Two-letter
  ISO 639-1 code (e.g. en, fr, ja)" instead of "BCP-47 tag (e.g. en-US,
  fr-FR, ja-JP)" — matches what NormalizeForWhisper actually accepts
  (region is stripped). (Help text is now only consumed by the language
  picker on Voice & Audio, but the resw key was renamed/repurposed to
  match.)
* Dropped the now-orphan SttLanguageLabel/TextBox/Help resw entries
  from all 5 locales, the CapabilitiesPageLocalizationCoverageTests
  catalog, and the LocalizationValidationTests invariant list.

Tests: Shared 1292 / Tray 460 (6 fewer cases — the
CapabilitiesPageLocalizationCoverageTests theory shrank by 3 keys ×
2 non-en locales). Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* i18n: align VoiceOverlayWindow root x:Uid with WindowEx convention

The Title key in resw was VoiceOverlayWindow_winexWindowEx_2.Title, but
the root x:Uid was just "VoiceOverlayWindow" — so WinUI's auto-derived
property-suffix lookup (Window-typed elements get the _winexWindowEx_2
suffix) couldn't find a match and the title fell back to the XAML
default. Aligned the x:Uid to "VoiceOverlayWindow_winexWindowEx_2",
matching the existing pattern used by ChatWindow / HubWindow /
CanvasWindow / TrayMenuWindow.

(Also: the buddy's parallel "trailing whitespace in resw" finding is
already addressed by subsequent commits — XmlDocument.Save normalized
the formatting; `Get-Content | -match '\s+\$'` returns 0 on every
locale today.)

Build green. Tray 460.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* privacy: stop leaking ex.Message into voice UI status text

The voice settings handlers and the Voice Overlay's start/stop catch
were formatting raw exception messages straight into user-facing UI
status text (and from there potentially into screenshots, error toasts,
support bundles, the activity stream). ex.Message can carry URLs,
local paths, hash digests, HTTP body fragments, or other implementation
detail that the user shouldn't see.

Seven call sites updated:
* VoiceSettingsPage.xaml.cs — Whisper download error, Piper download
  failure, Piper delete failure, Piper preview failure, Windows voice
  enumeration failure, Windows preview failure (6 sites).
* VoiceOverlayWindow.xaml.cs — overlay start/stop catch (1 site).

For each: full ex (message + type + stack) is logged via Logger.Error
or _logger.Error; the UI shows a generic localized message that ends in
"(see Debug log)" so users know where the detail lives.

Resw side:
* Six error-string keys in all 5 locales had their {0} format
  placeholders replaced with self-contained generic messages
  (translated, not just placeholder-stripped).
* VoiceSettingsPage_StatusError dropped from
  LocalizationValidationTests.InvariantOrDeferredResourceKeys — it
  used to be flagged invariant because the placeholder made every
  locale identical; with real translations it now varies and shouldn't
  be exempt.

Tests: Tray 460. Build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* audio: include stt.listen + stt.status in DangerousCommands

These two commands were already wired up in NodeService and advertised by
SttCapability, but the gateway's Windows platform-default policy hides any
command that isn't either platform-default (system.*, browser.proxy) or in
the node's DangerousCommands opt-in list. Only stt.transcribe was in that
list, so chat agents only saw stt.transcribe even when NodeSttEnabled was
on.

Adding stt.listen and stt.status lets them get the same explicit gateway
opt-in treatment as stt.transcribe, so once the operator allows them in
gateway.nodes.allowCommands they flow through to the agent's tools list.

Verified end-to-end: after re-pair, chat reports the full 24-command list
including stt.listen, stt.status, and tts.speak.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(audio): isolate shared download cancellation

Keep Whisper model and Piper voice single-flight downloads alive when one caller cancels its wait, and cover retry/cancellation behavior with focused tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(tray): keep right-click to context menu only

Restore tray right-click behavior so it opens only the menu instead of also showing the companion hub.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(voice): allow local overlay without node pairing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Ranjesh Jaganathan <ranjeshj@microsoft.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
indierawk2k2 referenced this pull request in indierawk2k2/openclaw-windows-node May 12, 2026
… required postconditions (PR openclaw#310 blockers #2 + #3)

Blocker #2 — GatewayRegistry cleanup:
- Add required GatewayRegistry param to ctor (Build factory auto-resolves
  from data dir when caller doesn't supply one).
- Insert new Step 6a 'Remove local gateway records' between Step 6 (Reset
  autostart) and Step 7 (Null device token).
- Snapshot _localGatewayIdsSnapshot BEFORE any mutation so identity-dir
  postconditions verify the full candidate set even on partial-remove failures.
- Defense-in-depth predicate: r.IsLocal || LocalGatewayUrlClassifier.IsLocalGatewayUrl(r.Url)
  — catches legacy migrated records whose IsLocal flag was never set.
- Step deletes registry records, per-gateway identity dirs, and clears the
  active gateway when it was a local record (GatewayRegistry.Remove handles
  the active-id reset already).
- Custom DryRun block reports candidate IDs without mutating disk.

Blocker #3 — Postcondition-gated Success:
- Add LocalGatewayRecordsAbsent (reload-from-disk via fresh GatewayRegistry)
  and LocalGatewayIdentityDirsAbsent (snapshot vs disk) postconditions.
- AllRequiredPostconditionsMet covers 8 required postconditions; McpTokenPreserved
  remains informational.
- AppendPostconditionErrors runs unconditionally when not DryRun, so the Errors
  list always contains both step exceptions AND residual-artifact failures.
- Success = _errors.Count == 0 && (DryRun || AllRequiredPostconditionsMet).

Tests (14 new; 44 total in LocalGatewayUninstallTests):
- Run_LocalGatewayRecordsCleared_PostconditionTrue
- Run_OnlyLocalRecordsRemoved_RemoteGatewaysPreserved
- Run_LocalAndRemoteWithSameUrl_OnlyIsLocalRemoved
- Run_LegacyRecordWithoutIsLocal_RemovedByUrlClassifier
- Run_RegistryRecordsRemain_ReturnsSuccessFalse
- Run_IdentityDirPersists_ReturnsSuccessFalse
- Run_PostconditionFailed_ErrorListed
- Run_StepErrorAndPostconditionFailure_BothInErrors
- Run_ActiveGatewayCleared_WhenLocalRemoved
- Run_ActiveRemoteGateway_StaysActive
- Run_NoGatewaysJson_SucceedsWithPostconditionsTrue
- Run_PostconditionUsesFreeDiskRegistry_NotInMemory
- DryRun_RegistryNotMutated
- DryRun_SuccessTrue_PostconditionsSkipped

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bkudiess referenced this pull request in bkudiess/openclaw-windows-node May 12, 2026
…che TTL, tests)

Hanselman pass #2 surfaced two blockers and three quality items. All fixed.

Blockers:

- Argument quoting parity with LocalCommandRunner. The bridge previously
  always-quoted every arg, which broke PowerShell parameter binding: a
  switch like `-Name` was passed as the literal string `'-Name'` so
  `Get-ChildItem -Name` failed to bind. And the cmd.exe path applied
  `.replace(/"/g, '""')` to the whole concatenated command line AFTER
  quoteArg had already doubled quotes, producing quadruple `""""`
  sequences cmd parsed as garbage. Fix: port ShellQuoting.NeedsQuoting
  to JS (same metacharacter set as the C# side) so only args that need
  quoting get quoted, and drop the outer .replace in the cmd path —
  /S /C strips exactly the first and last " of the operand so the
  inner content (already quoteArg-escaped) goes through verbatim.

- JS bridge deny-filter bypass. The C# MxcPolicyBuilder.FilterOutDenied
  strips any allow-list entry equal to or nested inside a denied path,
  protecting ~/.ssh, OpenClaw settings, and browser profile roots. But
  the JS bridge's mergePolicy then injects @microsoft/mxc-sdk's tools
  policy (PSReadLine history readwrite, %LOCALAPPDATA% readonly parents
  in some configs) without re-applying that filter. Custom grants could
  therefore overlap with denied paths via SDK additions. Fix: port
  FilterOutDenied to JS as filterOutDenied(allowed, denied) and apply
  it after mergePolicy, before passing to createConfigFromPolicy. Same
  semantics as the C# side: case-insensitive, normalize with
  path.resolve, exact-match OR prefix-with-separator.

Quality items:

- MxcAvailabilityTests.Probe_Result_IsConsistent asserted the OLD
  HasAnyBackend formula (just OR of the two backend flags), but
  HasAnyBackend was tightened in commit 43dfd1c to also require
  IsWxcExecResolvable AND RunCommandScriptPath != null. The test
  would pass when both new conjuncts happened to be true on the test
  host, giving false confidence. Updated assertion to match the
  current property.

- Added two missing tests:
  * RunAsync_SandboxUnavailableException_InvalidatesAvailabilityCache
    — verifies the new invalidate-availability callback is invoked
    when the executor throws SandboxUnavailableException.
  * RunAsync_GenericException_ReturnsDeny_DoesNotPropagate — covers
    the generic catch in MxcCommandRunner.RunAsync added in 43dfd1c.
    Without a test, a future refactor could remove the catch and let
    a bridge crash escape the node loop.
  * RunAsync_OperationCanceled_Propagates — verifies the ONE exception
    type that escapes the catch-all does in fact propagate.

- Stale availability cache. NodeService._mxcAvailability was cached
  once and never re-probed, so if MXC components were uninstalled
  mid-session (or wxc-exec.exe moved on disk) the runner would happily
  route to a sandbox that no longer exists. Fix: MxcCommandRunner now
  takes an Action? invalidateAvailability callback. On
  SandboxUnavailableException, it invokes the callback. NodeService
  wires it to `() => _mxcAvailability = null`, so the next command
  re-probes via `??=`. SandboxPage already constructs a fresh page
  per navigation so it doesn't need this fix.

Deferred (low-severity, documented in tracking):

- NormalizePath doesn't resolve junctions/symlinks (defense-in-depth;
  AppContainer likely enforces deny at the NT object path level).
- _mxcAvailability lacks a volatile annotation (benign on x64 CLR;
  ref reads are atomic).
- Scratch dir rmSync can race with forked subprocesses (already
  best-effort with try/catch, OS reaps %TEMP% eventually).

Tests: 1483 shared + 3 integration (real AppContainer + scoped policy)
+ 914 tray, all green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
indierawk2k2 referenced this pull request in indierawk2k2/openclaw-windows-node May 14, 2026
Four items flagged by @shanselman after re-reviewing 488c94f. All four
addressed in this commit:

1. _engineStarted permanent-block bug (V2Bridge.EnsureEngineStarted)
   The flag was set BEFORE the existing-config guard, so a guarded early
   return left _engineStarted=true. A later call after the user confirmed
   replacement would hit if (_engineStarted) return; and never construct
   the engine — permanently locking the user out of local setup.

   Fix: move _engineStarted = true to AFTER all preflight guards pass.
   Also reset _engineStarted to false in the engineFactory catch path so
   transient construction failures are recoverable via Try-again.
   Mark the synthetic existing-config block as retryable so the user can
   confirm replace and retry without restarting onboarding.

2. Stale retry continuation race (V2Bridge.OnRetryRequested + EnsureEngineStarted)
   Added monotonic _engineGeneration counter. Bumped when retry resets
   engine state. Captured in the RunLocalOnlyAsync().ContinueWith(...)
   before the new run starts. Continuation no-ops if generation has been
   bumped — preventing an old run's final state from auto-advancing the
   V2 flow (LocalSetupProgress → GatewayWelcome) after the user clicked
   "Try again".

3. Try-again rendered for terminal/blocked failures (V2State + Bridge + Page)
   Added OnboardingV2State.LocalSetupCanRetry (default false). Bridge
   OnEngineStateChanged sets it true ONLY for FailedRetryable; terminal
   and blocked failures clear it. LocalSetupProgressPage.BuildErrorCard
   accepts a nullable Action? onTryAgain and omits the button when null;
   single-column grid layout when no retry button is shown. Bridge
   OnRetryRequested is gated on LocalSetupCanRetry as defense-in-depth
   so a stale UI event from before the page re-rendered cannot restart
   the engine on a terminal failure.

4. New source projects added to slnx
   Added OpenClawTray.OnboardingV2 (no platform mapping) and
   OpenClaw.SetupPreview (with x64/ARM64 mapping like Tray.WinUI, since
   it's WindowsAppSDKSelfContained=true and AnyCPU would need a RID).
   Now visible in VS/Rider solution view.

Tests added (OpenClawTray.OnboardingV2.Tests):
- LocalSetupCanRetry_DefaultsToFalse
- LocalSetupCanRetry_SetTrue_FiresStateChanged
- LocalSetupCanRetry_SetSameValue_DoesNotFireStateChanged

Bridge-level integration tests for fixes #1 and #2 (the simulation Scott
asked for) require constructing OnboardingV2Bridge with a mock engine
factory — but the bridge depends on App.xaml.cs (gateway client reseeding),
which can't be easily unit-tested without WinUI runtime. The fixes are
covered with strong inline contracts/comments documenting the invariants.

Validation (worktree, OPENCLAW_REPO_ROOT set):
- ./build.ps1 ✅
- Shared.Tests — 1548 passed / 28 skipped / 0 failed ✅
- Tray.Tests — 1197 passed / 0 failed ✅ (was 1178; +19 from master merge)
- OpenClawTray.OnboardingV2.Tests — 7 passed / 0 failed ✅ (was 4; +3 CanRetry)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
indierawk2k2 referenced this pull request in indierawk2k2/openclaw-windows-node May 15, 2026
Four items flagged by @shanselman after re-reviewing 488c94f. All four
addressed in this commit:

1. _engineStarted permanent-block bug (V2Bridge.EnsureEngineStarted)
   The flag was set BEFORE the existing-config guard, so a guarded early
   return left _engineStarted=true. A later call after the user confirmed
   replacement would hit if (_engineStarted) return; and never construct
   the engine — permanently locking the user out of local setup.

   Fix: move _engineStarted = true to AFTER all preflight guards pass.
   Also reset _engineStarted to false in the engineFactory catch path so
   transient construction failures are recoverable via Try-again.
   Mark the synthetic existing-config block as retryable so the user can
   confirm replace and retry without restarting onboarding.

2. Stale retry continuation race (V2Bridge.OnRetryRequested + EnsureEngineStarted)
   Added monotonic _engineGeneration counter. Bumped when retry resets
   engine state. Captured in the RunLocalOnlyAsync().ContinueWith(...)
   before the new run starts. Continuation no-ops if generation has been
   bumped — preventing an old run's final state from auto-advancing the
   V2 flow (LocalSetupProgress → GatewayWelcome) after the user clicked
   "Try again".

3. Try-again rendered for terminal/blocked failures (V2State + Bridge + Page)
   Added OnboardingV2State.LocalSetupCanRetry (default false). Bridge
   OnEngineStateChanged sets it true ONLY for FailedRetryable; terminal
   and blocked failures clear it. LocalSetupProgressPage.BuildErrorCard
   accepts a nullable Action? onTryAgain and omits the button when null;
   single-column grid layout when no retry button is shown. Bridge
   OnRetryRequested is gated on LocalSetupCanRetry as defense-in-depth
   so a stale UI event from before the page re-rendered cannot restart
   the engine on a terminal failure.

4. New source projects added to slnx
   Added OpenClawTray.OnboardingV2 (no platform mapping) and
   OpenClaw.SetupPreview (with x64/ARM64 mapping like Tray.WinUI, since
   it's WindowsAppSDKSelfContained=true and AnyCPU would need a RID).
   Now visible in VS/Rider solution view.

Tests added (OpenClawTray.OnboardingV2.Tests):
- LocalSetupCanRetry_DefaultsToFalse
- LocalSetupCanRetry_SetTrue_FiresStateChanged
- LocalSetupCanRetry_SetSameValue_DoesNotFireStateChanged

Bridge-level integration tests for fixes #1 and #2 (the simulation Scott
asked for) require constructing OnboardingV2Bridge with a mock engine
factory — but the bridge depends on App.xaml.cs (gateway client reseeding),
which can't be easily unit-tested without WinUI runtime. The fixes are
covered with strong inline contracts/comments documenting the invariants.

Validation (worktree, OPENCLAW_REPO_ROOT set):
- ./build.ps1 ✅
- Shared.Tests — 1548 passed / 28 skipped / 0 failed ✅
- Tray.Tests — 1197 passed / 0 failed ✅ (was 1178; +19 from master merge)
- OpenClawTray.OnboardingV2.Tests — 7 passed / 0 failed ✅ (was 4; +3 CanRetry)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
indierawk2k2 referenced this pull request in indierawk2k2/openclaw-windows-node May 16, 2026
…reNodeConnectedAsync + NodeService.AttachClient safety

Adversarial dual-model code review (Opus + Codex) on PR openclaw#413 surfaced 5
actionable findings. This commit fixes all 5:

#1 NodeService.AttachClient/DisconnectAsync — event subscription bookkeeping
   was neither idempotent (Opus) nor thread-safe (Codex). Added a dedicated
   _clientLock that AttachClient and DisconnectAsync both take while reading/
   writing _nodeClient and wiring/unwiring its handlers. Subscribe is now
   unconditional unsubscribe-then-subscribe so a re-attach of the same client
   reference after a DisconnectAsync (which nulled _nodeClient) doesn't
   double-subscribe.

#2 ConnectionManagerWindowsNodeConnector defensive create — was hardcoding
   IsLocal=true. Derived from URL via LocalGatewayUrlClassifier.IsLocalGatewayUrl
   so a future remote-gateway caller isn't silently misclassified.

#3 EnsureNodeConnectedAsync timeout — was applying CancelAfter(35s) even when
   the caller passed a longer-lived token, contradicting the docstring
   contract. Now only applies the default 35s when !cancellationToken.CanBeCanceled.

openclaw#4 EnsureNodeConnectedAsync entry guard — added cancellationToken
   .ThrowIfCancellationRequested() before any side effects.

openclaw#5 EnsureNodeConnectedAsync silent-hang on no-credential — Opus's
   symmetric-defect check flagged 3 silent-return paths in
   StartNodeConnectionAsync (null connector / missing gateway record / no
   node credential). EnsureNodeConnectedAsync now re-reads the snapshot
   after StartNodeConnectionAsync and throws InvalidOperationException
   immediately if NodeState is still Idle or Disabled — rather than
   waiting 35s for a misleading TimeoutException. The underlying
   diagnostic was already recorded via _diagnostics.Record('node', ...).

Deferred (LOW-consensus, would need deeper changes):
- Active gateway mismatch check in ConnectionManagerWindowsNodeConnector
  (Codex MEDIUM, Opus didn't flag). In practice the operator connector
  runs first in the same engine setup and switches the manager to the
  right record. Adding an explicit SwitchGatewayAsync here would race with
  the manager's own gateway-switching logic — defer until reproducer.
- Post-onboarding OperatorState==Connecting snapshot race (Opus LOW,
  self-resolved as no-fix-needed — auto-reconnect timer covers the
  benign race).

Tray.Tests 962/962 ✅, Connection.Tests 224/224 ✅, build green ✅

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bkudiess referenced this pull request in bkudiess/openclaw-windows-node May 16, 2026
Replaces the gray Segoe Fluent monochrome glyphs in the Hub sidebar with
20 colorful Microsoft Fluent UI System Color SVGs, and replaces the
pixel-art lobster (tray, taskbar, Alt-Tab, window titlebars, chat
zero-state, tray menu header, MSIX tile, lock screen, store badge) with
the Fluent Emoji 3D Lobster already shipping at Assets/Setup/Lobster.png.

Both icon families are MIT-licensed:
- Sidebar: github.com/microsoft/fluentui-system-icons (Color variant)
- Lobster: github.com/microsoft/fluentui-emoji (Lobster 3D PNG)

Attribution documented in src/OpenClaw.Tray.WinUI/THIRD_PARTY_NOTICES.md.

Sidebar sizing tuned to match Windows 11 Settings — 22px icon box,
default 36px MinHeight (gives ~50px row pitch), tightened separator
margin (0,2,0,2). NavigationView.Resources holds shared SvgImageSource
keys (one per icon) so each NavigationViewItem references the same
decoded SVG instead of re-parsing per instance.

High-contrast accessibility: HubWindow constructor calls
ApplyHighContrastFallbackIfNeeded() which detects HC via
Windows.UI.ViewManagement.AccessibilitySettings and, when active, walks
NavView.MenuItems/FooterMenuItems and swaps each Icon to a FontIcon
glyph from Segoe Fluent Icons. FontIcon auto-adapts to all HC variants
(HC Black/White/#1/#2) via the system foreground brush. Dynamic agent
items respect the same flag via BuildAgentItemIcon().

Adjacent fixes:
- App.xaml.cs:2707 was looking for the non-existent claw.ico for toast
  notifications; renamed to openclaw.ico.
- HubWindow.xaml: added missing x:Uid on Voice and Sandbox nav items so
  future localization picks them up.
- THIRD_PARTY_NOTICES.md placed at project root rather than inside
  Assets/ so it does not get bundled into the runtime output via the
  Assets/**/* content glob.

Adversarial dual-model code review (Opus + GPT Codex) raised 7 findings;
all addressed in this change.

Validation
- ./build.ps1 .................. ✅
- Shared.Tests --no-restore .... 1620 passed, 28 skipped, 0 failed
- Tray.Tests --no-restore ...... 992 passed, 0 failed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bkudiess referenced this pull request in bkudiess/openclaw-windows-node May 17, 2026
Replaces the gray Segoe Fluent monochrome glyphs in the Hub sidebar with
20 colorful Microsoft Fluent UI System Color SVGs, and replaces the
pixel-art lobster (tray, taskbar, Alt-Tab, window titlebars, chat
zero-state, tray menu header, MSIX tile, lock screen, store badge) with
the Fluent Emoji 3D Lobster already shipping at Assets/Setup/Lobster.png.

Both icon families are MIT-licensed:
- Sidebar: github.com/microsoft/fluentui-system-icons (Color variant)
- Lobster: github.com/microsoft/fluentui-emoji (Lobster 3D PNG)

Attribution documented in src/OpenClaw.Tray.WinUI/THIRD_PARTY_NOTICES.md.

Sidebar sizing tuned to match Windows 11 Settings — 22px icon box,
default 36px MinHeight (gives ~50px row pitch), tightened separator
margin (0,2,0,2). NavigationView.Resources holds shared SvgImageSource
keys (one per icon) so each NavigationViewItem references the same
decoded SVG instead of re-parsing per instance.

High-contrast accessibility: HubWindow constructor calls
ApplyHighContrastFallbackIfNeeded() which detects HC via
Windows.UI.ViewManagement.AccessibilitySettings and, when active, walks
NavView.MenuItems/FooterMenuItems and swaps each Icon to a FontIcon
glyph from Segoe Fluent Icons. FontIcon auto-adapts to all HC variants
(HC Black/White/#1/#2) via the system foreground brush. Dynamic agent
items respect the same flag via BuildAgentItemIcon().

Adjacent fixes:
- App.xaml.cs:2707 was looking for the non-existent claw.ico for toast
  notifications; renamed to openclaw.ico.
- HubWindow.xaml: added missing x:Uid on Voice and Sandbox nav items so
  future localization picks them up.
- THIRD_PARTY_NOTICES.md placed at project root rather than inside
  Assets/ so it does not get bundled into the runtime output via the
  Assets/**/* content glob.

Adversarial dual-model code review (Opus + GPT Codex) raised 7 findings;
all addressed in this change.

Validation
- ./build.ps1 .................. ✅
- Shared.Tests --no-restore .... 1620 passed, 28 skipped, 0 failed
- Tray.Tests --no-restore ...... 992 passed, 0 failed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
shanselman pushed a commit that referenced this pull request May 17, 2026
…le-app-model)

Replaces the legacy raw-Expander Debug page with a Fluent-style
"Diagnostics" surface composed of CommunityToolkit SettingsCard /
SettingsExpander, an in-page detail view, and a preview-then-share
diagnostics bundle dialog. The internal route stays Tag="debug" so
every existing command-palette / deep-link alias keeps working.

Rebased onto Ranjesh's single-app-model branch
(user/ranjeshj/singleappmodel). The page now observes AppState
directly per docs/DATA_FLOW_ARCHITECTURE.md and routes user
intents through IAppCommands — no HubWindow dependency:

- DebugPage.Initialize() takes no arguments; pulls AppState +
  ConnectionManager from ((App)Application.Current) and
  subscribes to AppState.PropertyChanged for live Status /
  GatewaySelf updates.
- "Manage on Connection page" hyperlink calls
  ((IAppCommands)CurrentApp).Navigate("connection").
- "Reconfigure..." confirmation calls
  ((IAppCommands)CurrentApp).ShowOnboarding() after the
  ContentDialog returns Primary.
- Diagnostics-bundle preview pulls the GatewayCommandCenterState
  via App.BuildCommandCenterState() (made internal so pages can
  call it without the now-deleted HubWindow.GetCommandCenterStateAction).
- DiagnosticsBundleDialog gets its parent HWND via the new
  App.GetHubWindowHandle() (mirrors App.GetOnboardingWindowHandle).
- AboutPage.OnCopySupportClick unifies on the same BuildCommandCenterState
  path; "More diagnostics ->" hyperlink uses IAppCommands.Navigate("debug").

## Page

- Renames the nav-item label "Debug" -> "Diagnostics" (resw
  HubWindow_NavigationViewItem_145.Content). The Tag, route table,
  and command palette aliases ("debug", "connectionstatus", "log",
  "supportcontext", "debugbundle", "browsersetup", "portdiagnostics",
  "capabilitydiagnostics") are unchanged.
- Three task-oriented sections instead of an inventory of features:
    1. Share diagnostics with support — primary "Create diagnostics
       bundle" SettingsCard opens a preview ContentDialog (Copy /
       Save to file / Close). Four previously invisible
       diagnostic-text commands (debug bundle, browser setup, port
       diagnostics, capability diagnostics) are surfaced behind a
       "Copy specific diagnostic text" SettingsExpander.
    2. Inspect local diagnostics — Connection event timeline +
       Recent log cards (both open the new in-page detail view),
       plus a Device identity SettingsExpander with click-to-copy
       Device ID and Public key.
    3. Developer tools — Warning InfoBar, Chat surface overrides
       SettingsExpander, Open chat explorations, and Reconfigure...
       (canonical verb per naming.md) with ContentDialog
       confirmation.
- All cards live in a single centered Width=900 StackPanel so the
  title block and cards share the exact same column and left edge.

## In-page detail view

- Replaces the separate ConnectionStatusWindow entry on this page
  with an in-page detail view that swaps in via Visibility, matching
  ConnectionPage.AddGatewayPanel.
- Two modes share a single RichTextBlock body:
    - Timeline — subscribes to ConnectionDiagnostics.EventRecorded
      and renders live state-machine / websocket / handshake /
      credential events with the same direction-arrow + category
      tag + severity coloring used by ConnectionStatusWindow.
    - Log — reads the last 200 lines of openclaw-tray.log and
      colors each line by severity ([info]/[warn]/[error]/[debug]/
      [trace] + legacy uppercase forms) with the timestamp prefix
      rendered dim.
- Both modes resolve their severity brushes from system theme
  tokens (SystemFillColorCritical/Caution/Attention/Success +
  TextFillColorSecondary) so the colors track light/dark/HC.

## Design-reference compliance (docs/design/*)

- Eight new Segoe Fluent Icons constants in FluentIconCatalog
  (Bug, Briefcase, Folder, Copy, Document, Refresh, Clear,
  Develop), pinned in FluentIconCatalogTests, used via
  {x:Bind helpers:FluentIconCatalog.X, Mode=OneTime}.
- Canonical "Reconfigure..." verb (with ellipsis) per naming.md.

## Hanselman dual-model review fixes (Opus + Codex consensus)

- #1 OnNavigatedFrom + Unloaded both unsubscribe the
  ConnectionDiagnostics.EventRecorded handler so the page can no
  longer leak its subscription when the user navigates away.
- #2 ReadLogTail uses FileStream with FileShare.ReadWrite to
  coexist with Logger.cs:109's active writer; the previous
  File.ReadLines silently failed on every Logger-active read.
- #3 Timeline subscribes before snapshotting and dedupes via
  HashSet<ConnectionDiagnosticEvent> (records use value equality)
  so events arriving during the snapshot/subscribe window are not
  dropped.
- #4 Log tail reads on a background thread via Task.Run so a
  large rotated log doesn't stall the UI thread.
- #5 RichTextBlock.Blocks + plain-text Queue<string> mirror are
  capped at MaxTimelineRows=500 (matches ConnectionDiagnostics
  capacity).
- #6 DiagnosticsBundleDialog.OnSaveClick drops the dead
  GetDeferral/ContinueWith path; the dialog stays open after
  Save (mirrors Copy).

## Wiring

- App.BuildCommandCenterState() is now `internal` so pages can
  pull the GatewayCommandCenterState directly without going
  through HubWindow.
- App.GetHubWindowHandle() new helper for parenting file pickers
  from in-Hub pages (mirrors GetOnboardingWindowHandle).
- HubWindow loses GetCommandCenterStateAction (was only used for
  the now-removed forwarding chain).

## Tests

- DiagnosticsPageContractTests (17 source-level guards) updated
  for the new pattern: asserts CurrentApp / AppState observation,
  no HubWindow dependency, Width=900 column, and BuildCommandCenterState
  internal accessor.
- FluentIconCatalogTests.ExpectedConstants extended with the eight
  new constants.
- Required validation per AGENTS.md is green:
    ./build.ps1                                                                                    -> all four projects build
    dotnet test ./tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj --no-restore             -> 1620 passed, 28 skipped
    dotnet test ./tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj --no-restore                 -> 984 passed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
RBrid added a commit to RBrid/openclaw-windows-node that referenced this pull request May 22, 2026
Fixes from dual-model code review on user/regisb/WSLInstall1 branch.

openclaw#1 + openclaw#2 + openclaw#6: SuppressNodeAutoApprove (process-global bool, racy, idempotency-fragile) replaced with IDisposable AcquireNodeAutoApproveSuppression() using volatile int + Interlocked counter. Token Dispose is idempotent via Interlocked.Exchange. Local-loopback URL gate scopes suppression to ws://localhost / ws://127.* so manual operator connects to remote gateways still surface their pending-approval toast.

openclaw#3: ElevatedWslPlatformInstaller.RunDefaultAsync now wraps WaitForExitAsync in try/catch(OperationCanceledException), attempts best-effort Kill(entireProcessTree:true) on the launcher (will fail for the elevated child — documented), then rethrows so the engine surfaces Cancelled cleanly.

openclaw#4: ConfigureWslInstance failure remap to wsl_firstboot_config_failed_after_install is now gated on a stderr-signature check (LooksLikePostInstallKernelIssue: WslRegisterDistribution, Hyper-V, 0x80370102, kernel, vmcompute, LxssManager, instance-is-corrupted, etc). Unrelated configure failures (apt repo issues, script bugs) keep their original error code so users don't get a misleading 'reboot to fix' suggestion.

openclaw#5 + openclaw#15: New tests — Engine_SelfHeal_ResetsCancelledStatus, Engine_ConfigureFails_AfterJustInstalled_DoesNotRemapUnrelatedFailure, LooksLikePostInstallKernelIssue_MatchesKnownKernelSignatures, PairAsync_AwaitsConfiguredDelay, PairAsync_CancellationDuringDelay_Propagates.

openclaw#7: OnboardingV2Bridge now treats LocalGatewaySetupStatus.RequiresAdmin like other terminal/blocking statuses — surfaces error message via LocalizeFailureMessage and suppresses the running-phase info card.

openclaw#8: WslPlatformInstall DefaultPostInstallProbeAttempts comment rewritten to reflect real worst case (~33s, but typically 1–2 probes) rather than the misleading '6 × 500ms = 3s'.

openclaw#9: Extracted WslInstallCycleIssueCodes static class with const issue codes + IReadOnlySet<string> All. Replaced 5 duplicated string literals (top-level self-heal RemoveAll + 4 EnsureWslEnabled outcome branches) with the constants.

openclaw#10: CreateLocalOnly factory now reads OPENCLAW_PAIR_RETRY_DELAY_MS env var at wire-up and passes it as SettingsWindowsTrayNodeProvisioner pairRetryDelay so operators can tune the retry without rebuilding.

openclaw#11: LocalizeFailureMessage now exposes an isGenericFallback signal; MaybeAppendSetupDiagnosticsHint skips the 'Setup diagnostics: …' wrap when we returned the V2_Progress_GenericFailure placeholder (the generic message already directs to logs).

openclaw#13: SetupExistingGatewayClassifier.ClassifyAsync accepts an optional IOpenClawLogger so WSL probe failures surface in the diagnostic stream instead of being silently swallowed by NullLogger.

openclaw#14: ComputeInfoMessage phase predicate tightened to (status==Running && phase<=EnsureWslEnabled) so the 'Check system' hint stops once the engine moves past EnsureWslEnabled, including transient non-Running states between phases.

openclaw#16: Preflight port-probe (host-side TCP check) now runs unconditionally rather than being gated on WSL platform.State==Installed, so users find out about host-side port conflicts before spending 30+ seconds on WSL install.

Validation: build.ps1 green; Shared 1891/29 skipped, Tray 1217 (+5), Connection 229 all pass; ARM64 publish succeeds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
kmahone pushed a commit that referenced this pull request Jun 12, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bkudiess referenced this pull request in bkudiess/openclaw-windows-node Jun 17, 2026
Follow-ups to the probe-based availability change, addressing two
review concerns.

#2 Probe error vs. unsupported host:
- ParseProbeOutput now classifies into MxcProbeOutcome {Supported,
  UnsupportedHost, ProbeError}. A negative exit (our timeout/launch
  sentinel) or exit 0 with no usable output is a transient ProbeError;
  a positive non-zero exit is a definitive UnsupportedHost.
- MxcAvailability exposes ProbeErrored. NodeService caches definitive
  verdicts for the process lifetime but re-probes (throttled, 5s) after
  a transient error, so a momentary glitch self-heals instead of pinning
  the whole process to uncontained execution. Keeps the issue openclaw#494
  fall-back-to-host policy. SandboxPage likewise re-probes an errored
  cache on next init.

#3 Degraded isolation tier:
- MxcAvailability exposes IsolationTier, NeedsDaclAugmentation, and a
  derived IsDegradedContainment (true for appcontainer-dacl, DACL
  augmentation, or an unrecognized tier). Any non-empty tier is still
  accepted as contained — refusing would drop the host to fully
  uncontained, which is strictly worse — but SandboxPage now surfaces a
  "limited containment" caution.

Adds unit coverage for outcome classification, ProbeErrored propagation,
and degraded-tier detection. Validated: build.ps1, Shared (2062), Tray (958).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bkudiess referenced this pull request in bkudiess/openclaw-windows-node Jun 17, 2026
Addresses a GPT-5.5 rubber-duck pass on the probe-error/tier changes.

- Fix self-heal bug (#1): DirectAppContainerExecutor took a frozen
  MxcAvailability snapshot, so a transient startup probe error pinned
  system.run to uncontained for the executor's lifetime even after the
  host recovered (the re-probe updated the runner's gate but not the
  executor, which then threw SandboxUnavailableException on every call).
  The executor now resolves availability lazily via Func<MxcAvailability>.

- Explicit probe status (#3): WxcProbeInvocation carries a WxcProbeStatus
  {Completed, TimedOut, LaunchFailed} instead of overloading exit code -1
  as a sentinel. ParseProbeOutput only inspects the exit code for a
  Completed run, so a real native (possibly negative) exit code can't be
  misread as our timeout/launch sentinel.

- Serialize probing (#2): NodeService.GetOrProbeMxcAvailability is now
  guarded by a lock and sets the retry timestamp before probing, so
  concurrent system.run calls can't spawn a storm of wxc-exec --probe
  processes during a slow/timeout probe.

- SandboxPage (openclaw#7): a transient probe error is no longer mislabeled as
  "your Windows version doesn't support sandboxing" — it shows
  "Couldn't verify sandbox availability" with a Retry that re-probes.
  Added an in-flight guard so refresh can't start overlapping probes.

Tests: lazy-resolution recovery (errored -> recovered picks up), status
classification (timeout/launch ignore exit code; completed-nonzero =
unsupported), updated injected-probe and golden tests. Validated:
build.ps1, Shared (2064), Tray (958).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bkudiess referenced this pull request in bkudiess/openclaw-windows-node Jun 18, 2026
Addresses 7 findings from a Claude Opus 4.8 + GPT-5.3-Codex adversarial review
(4 HIGH-consensus, 3 single-model).

Confirmed-resolution model (#1/#2 HIGH): a send-ack only means the approve/reject
frame left the socket, not that the gateway accepted it. PairingApprovalQueue now
treats a decision as optimistic-pending (MarkSubmitted) and reports it confirmed
only when the request actually leaves the pending list; the success toast fires on
that confirmation, not on send-ack. If the gateway never acts within 10s the
submission expires and the request re-surfaces for retry — no permanent hide, no
false "approved" toast. Also guards against a disconnect (empty list while not
connected) being misread as mass confirmations.

Stale/closed dialog continuation (#3 HIGH): PairingApprovalDialog.DecideAsync now
captures the decision key, bails if the window closed or the queue advanced while
the RPC was in flight, and re-arms the approve guard on failure instead of
force-enabling Approve (preserving the anti-clickthrough delay). Render() is
IsClosed-guarded.

Own-node before identity known (#4 HIGH): NodeId and FullDeviceId can differ, and
the filter failed open while FullDeviceId was null. The coordinator now defers ALL
node requests while node mode is active but the own device id is unknown; they
re-surface (correctly self-filtered) once it is known.

Minor (openclaw#5/openclaw#6/openclaw#7): foreground-steal is limited to once per reconnect burst;
legacy device-id-as-requestId fallback is now safe (re-surfaces) and logged;
Reset() clears in-flight/poll state.

Tests: PairingApprovalQueue tests updated for MarkSubmitted + 4 new cases
(confirmed resolution, reject flag, timeout re-surface, node deferral).
Connection 301, Shared 2049, Tray 959 all green; full build passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bkudiess referenced this pull request in bkudiess/openclaw-windows-node Jun 23, 2026
Review follow-up (#2): the inline Windows-permissions step always showed,
making SkipPermissions dead. Now when SkipPermissions is set, CapabilitiesPage
hides the permissions step (internal step 2): it's a 2-step flow (capabilities
-> review & install) with 'Step X of 2' labels, Next/Back route around the
hidden step, and the OS permission probe is not run. Preserves the config
contract (schema, default-config.json, E2E fixture) without touching the
SetupConfig schema.

Also remove the now-unused SetupWindow.NavigateToPermissions (the standalone
PermissionsPage is reached only via the dev preview route, which navigates to
it directly).

Validation: build.ps1 green; SetupEngine 237, Tray 958, Shared 2049 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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