dsh-codex-style-computer-use 1.3.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,7 @@ using System.Collections.Generic;
20
20
  using System.Collections.Concurrent;
21
21
  using System.Diagnostics;
22
22
  using System.Drawing;
23
+ using System.Drawing.Drawing2D;
23
24
  using System.Drawing.Imaging;
24
25
  using System.IO;
25
26
  using System.Linq;
@@ -36,19 +37,34 @@ using Windows.Graphics.Imaging;
36
37
 
37
38
  namespace DshComputerUse.Worker
38
39
  {
39
- // A live cycle observes physical holds, but only actual actuation can start a handoff.
40
+ // On-demand cadence: one monotonic timestamp, no timer, history list or disk writes.
41
+ // Only a successfully displayed cue advances the sliding three-minute window.
42
+ public sealed class PreparationCadence
43
+ {
44
+ public const int FullMs = 1000, RepeatMs = 100, RepeatWindowMs = 180000;
45
+ long lastShown = -1;
46
+ public int Duration(long now)
47
+ { long previous = Interlocked.Read(ref lastShown); return previous >= 0 && now >= previous && now - previous < RepeatWindowMs ? RepeatMs : FullMs; }
48
+ public void Presented(long now) { Interlocked.Exchange(ref lastShown, now); }
49
+ }
50
+
51
+ // A live cycle observes physical holds; actuation or an explicit scene hold can start a handoff.
40
52
  // Once yielded, three quiet seconds with no held input returns control to the agent.
41
53
  public sealed class HumanInputState
42
54
  {
43
55
  public const int SustainedMs = 2000, IdleMs = 3000;
44
56
  readonly object sync = new object();
45
57
  readonly HashSet<int> held = new HashSet<int>();
58
+ readonly Dictionary<int, int> releasedSamples = new Dictionary<int, int>();
59
+ readonly HashSet<int> injectedTouched = new HashSet<int>();
46
60
  bool enabled, acting, seen;
47
61
  string phase = "idle", lastKind = "none";
48
- long first, last, revision, burst;
62
+ long first, last, revision, burst, stateSeq;
63
+ int reconciledReleases;
49
64
  public string Phase { get { lock (sync) { return phase; } } }
50
65
  public long Revision { get { lock (sync) { return revision; } } }
51
66
  public bool Active { get { lock (sync) { return phase != "idle"; } } }
67
+ public bool HasHeld { get { lock (sync) { return held.Count != 0; } } }
52
68
  public string LastKind { get { lock (sync) { return lastKind; } } }
53
69
  public bool Held(int vk)
54
70
  {
@@ -69,7 +85,7 @@ namespace DshComputerUse.Worker
69
85
  lock (sync)
70
86
  {
71
87
  if (enabled == on) return;
72
- enabled = on;
88
+ enabled = on; stateSeq++;
73
89
  if (!on) { phase = "idle"; acting = false; }
74
90
  }
75
91
  }
@@ -77,30 +93,30 @@ namespace DshComputerUse.Worker
77
93
  {
78
94
  lock (sync)
79
95
  {
80
- acting = on;
96
+ if (acting != on) { acting = on; stateSeq++; }
81
97
  // Do not replay earlier ordinary activity. A key/button still physically
82
98
  // held does need ownership before we can inject a conflicting chord.
83
99
  if (on && enabled && phase == "idle" && held.Count > 0)
84
- { last = now; revision++; Start(now); }
100
+ { last = now; revision++; stateSeq++; Start(now); }
85
101
  }
86
102
  }
87
103
  void Start(long now) { phase = "yielding"; first = now; if (!seen) last = now; burst++; }
88
104
  public void Record(int key, bool down, bool up, long now) { Record(key, down, up, now, false); }
89
- /// A DELIBERATE pointer move (the 16-px tolerance in PointerHandoffTolerance has already
90
- /// filtered jitter and agent motion) yields even BETWEEN actuations. Human spec
91
- /// 2026-11-04: while a cycle is live, the human reaching for the mouse is a takeover no
92
- /// matter whether an injection happens to be in flight at that instant — the old gate
93
- /// (`idle && !acting`) only lit the yield during the few dozen milliseconds of each
94
- /// click, so an interruption in the gaps showed no light at all.
105
+ /// Pointer jitter is filtered before this call. The protection flag includes both
106
+ /// actual injection and a retained scene hold; free observation never takes ownership.
95
107
  public void Record(int key, bool down, bool up, long now, bool deliberate)
96
108
  {
97
109
  lock (sync)
98
110
  {
99
111
  bool continued = held.Count > 0 || (seen && now - last < IdleMs);
112
+ stateSeq++;
100
113
  lastKind = key == 0 ? "pointer-or-wheel" : key <= 6 ? "button" : "keyboard";
101
- if (key != 0) { if (down) held.Add(key); if (up) held.Remove(key); }
114
+ if (key != 0) {
115
+ if (down) held.Add(key); if (up) held.Remove(key);
116
+ releasedSamples.Remove(key); injectedTouched.Remove(key);
117
+ }
102
118
  seen = true; last = now;
103
- if (!enabled || (phase == "idle" && !acting && !deliberate)) return;
119
+ if (!enabled || (phase == "idle" && !acting)) return;
104
120
  revision++;
105
121
  if (phase == "idle" || !continued) Start(now);
106
122
  if (now - first >= SustainedMs) phase = "waiting";
@@ -111,19 +127,77 @@ namespace DshComputerUse.Worker
111
127
  lock (sync)
112
128
  {
113
129
  if (!enabled || phase == "idle") return;
114
- if (held.Count == 0 && now - last >= IdleMs) { phase = "idle"; return; }
115
- if (held.Count > 0 && now - first >= SustainedMs) phase = "waiting";
130
+ if (held.Count == 0 && now - last >= IdleMs) { phase = "idle"; stateSeq++; return; }
131
+ if (held.Count > 0 && now - first >= SustainedMs && phase != "waiting") { phase = "waiting"; stateSeq++; }
132
+ }
133
+ }
134
+ public void NoteInjected(int key)
135
+ {
136
+ // An injected UP can make async state look released while a human
137
+ // still physically holds the same key. Never reconcile that case.
138
+ lock (sync) {
139
+ foreach (int tracked in held)
140
+ if (tracked == key || ModifierFamily(tracked) != 0 && ModifierFamily(tracked) == ModifierFamily(key) ||
141
+ (tracked == 1 || tracked == 2) && (key == 1 || key == 2))
142
+ { injectedTouched.Add(tracked); releasedSamples.Remove(tracked); }
143
+ }
144
+ }
145
+ static int ModifierFamily(int key)
146
+ { return key == 0x10 || key == 0xA0 || key == 0xA1 ? 0x10 : key == 0x11 || key == 0xA2 || key == 0xA3 ? 0x11 : key == 0x12 || key == 0xA4 || key == 0xA5 ? 0x12 : 0; }
147
+ public static bool AsyncButtonMayBeDown(int key, bool swapped, Func<int, bool> isDown)
148
+ {
149
+ // Async state names PHYSICAL buttons. With swapped primary/secondary
150
+ // semantics, require BOTH physical buttons up before repairing either
151
+ // history entry; never infer the low-level hook's mapping from a UI label.
152
+ return swapped && (key == 1 || key == 2) ? isDown(1) || isDown(2) : isDown(key);
153
+ }
154
+ public int ReconcileReleased(Func<int, bool> isDown, bool trusted, long now)
155
+ {
156
+ lock (sync) {
157
+ if (!trusted) { releasedSamples.Clear(); return 0; }
158
+ int removed = 0;
159
+ foreach (int key in held.ToArray()) {
160
+ if (injectedTouched.Contains(key) || isDown(key)) { releasedSamples.Remove(key); continue; }
161
+ int count; releasedSamples.TryGetValue(key, out count);
162
+ if (++count < 3) { releasedSamples[key] = count; continue; }
163
+ held.Remove(key); releasedSamples.Remove(key); removed++;
164
+ }
165
+ if (removed > 0) {
166
+ // This is fresh release evidence, not permission from elapsed
167
+ // time: still require the full quiet interval after this point.
168
+ last = now; seen = true; lastKind = "reconciled-release";
169
+ reconciledReleases += removed; revision++; stateSeq++;
170
+ }
171
+ return removed;
116
172
  }
117
173
  }
118
174
  public object Snapshot(long now)
119
175
  {
120
176
  lock (sync) return Program.Dict("phase", phase, "active", phase != "idle",
121
- "revision", revision, "burst", burst, "heldCount", held.Count, "lastKind", lastKind,
177
+ "revision", revision, "stateSeq", stateSeq, "burst", burst, "heldCount", held.Count, "lastKind", lastKind,
178
+ "reconciledReleaseCount", reconciledReleases,
122
179
  "quietRemainingMs", phase == "idle" ? 0 : held.Count > 0 ? IdleMs : Math.Max(0, IdleMs - (now - last)),
123
180
  "sustainedMs", SustainedMs, "idleMs", IdleMs);
124
181
  }
125
182
  }
126
183
 
184
+ // Domain ownership survives renderer failures and visibility changes. Glow
185
+ // only reads this state; cycle/operation lifecycle explicitly changes it.
186
+ public static class SceneControl
187
+ {
188
+ static volatile bool acting, holding;
189
+ public static bool Acting { get { return acting; } }
190
+ public static bool Holding { get { return holding; } }
191
+ public static void SetHolding(bool on)
192
+ {
193
+ holding = on && Panic.CycleLit && !Panic.Exited && !Panic.Stopped && !Panic.Engaged;
194
+ Panic.UpdateInputProtection();
195
+ }
196
+ public static void BeginActuation() { acting = true; SetHolding(true); }
197
+ public static void EndActuation() { acting = false; Panic.UpdateInputProtection(); }
198
+ public static void Reset() { acting = holding = false; Panic.UpdateInputProtection(); }
199
+ }
200
+
127
201
  // The UAC secure desktop (and the lock screen) is the ONE surface automation must never
128
202
  // reach — Windows enforces that itself. The honest addition (human request 2026-11-04)
129
203
  // is to NOTICE the switch, refuse to dispatch while it stands, tell the human exactly
@@ -447,6 +521,10 @@ namespace DshComputerUse.Worker
447
521
  public static extern bool OpenProcessToken(IntPtr proc, uint access, out IntPtr token);
448
522
  [DllImport("advapi32.dll", SetLastError = true)]
449
523
  public static extern bool GetTokenInformation(IntPtr token, int cls, out uint info, uint len, out uint ret);
524
+ [DllImport("advapi32.dll", EntryPoint = "GetTokenInformation", SetLastError = true)]
525
+ public static extern bool GetTokenInformationBuffer(IntPtr token, int cls, IntPtr info, uint len, out uint ret);
526
+ [DllImport("advapi32.dll")] public static extern IntPtr GetSidSubAuthorityCount(IntPtr sid);
527
+ [DllImport("advapi32.dll")] public static extern IntPtr GetSidSubAuthority(IntPtr sid, uint index);
450
528
  public const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000, TOKEN_QUERY = 0x0008;
451
529
  public const int TokenElevation = 20;
452
530
  [DllImport("user32.dll")]
@@ -473,6 +551,8 @@ namespace DshComputerUse.Worker
473
551
  public static extern IntPtr CreateDIBSection(IntPtr dc, ref BITMAPINFO bmi, uint usage, out IntPtr bits, IntPtr hSection, uint offset);
474
552
  [DllImport("user32.dll")]
475
553
  public static extern bool GetCursorInfo(ref CURSORINFO pci);
554
+ [DllImport("user32.dll", EntryPoint = "LoadCursorW")]
555
+ public static extern IntPtr LoadCursor(IntPtr instance, IntPtr resource);
476
556
  [DllImport("user32.dll")]
477
557
  public static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, uint diFlags);
478
558
  [DllImport("user32.dll")]
@@ -484,6 +564,10 @@ namespace DshComputerUse.Worker
484
564
  public static extern IntPtr OpenInputDesktop(uint flags, bool inherit, uint access);
485
565
  [DllImport("user32.dll")]
486
566
  public static extern bool CloseDesktop(IntPtr h);
567
+ [DllImport("user32.dll")]
568
+ public static extern IntPtr GetThreadDesktop(uint threadId);
569
+ [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
570
+ public static extern bool GetUserObjectInformation(IntPtr obj, int index, StringBuilder info, int length, out int needed);
487
571
 
488
572
  // ---- low-level keyboard hook: the human's emergency brake ----
489
573
  public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
@@ -572,7 +656,6 @@ namespace DshComputerUse.Worker
572
656
  [DllImport("user32.dll")] static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);
573
657
 
574
658
  static Thread _thread;
575
- static volatile bool _acting;
576
659
  static volatile bool _preparing;
577
660
  static volatile int _preparationStart;
578
661
  static int _preparationSerial, _presentedPreparation, _completedPreparation;
@@ -595,9 +678,12 @@ namespace DshComputerUse.Worker
595
678
  "lastSuccessAgeMs", Volatile.Read(ref _presentSuccesses) == 0 ? (object)null : unchecked((uint)(now - _lastPresentTick)),
596
679
  "lastFailedSerial", Volatile.Read(ref _lastFailedSerial), "lastFailedWant", Volatile.Read(ref _lastFailedWant),
597
680
  "lastWin32Error", Volatile.Read(ref _lastFailedSerial) < 0 ? (object)null : _lastFailedError,
598
- "rebuiltSerial", Volatile.Read(ref _rebuiltSerial));
681
+ "rebuiltSerial", Volatile.Read(ref _rebuiltSerial), "pointerFault", AgentPointer.Fault);
599
682
  }
600
- public const int PreparationMs = 1200; // human spec 2026-11-04: shortened from 1500 after review
683
+ static readonly Stopwatch PreparationClock = Stopwatch.StartNew();
684
+ static readonly PreparationCadence PreparationFrequency = new PreparationCadence();
685
+ static volatile int _preparationMs = PreparationCadence.FullMs;
686
+ public static int PreparationMs { get { return _preparationMs; } }
601
687
  static volatile bool _enabled = true;
602
688
  static volatile string _label = "AI 正在操控此电脑 · 监听中";
603
689
 
@@ -605,22 +691,36 @@ namespace DshComputerUse.Worker
605
691
  public static void Touch(int holdMs)
606
692
  {
607
693
  if (!_enabled) return;
694
+ _sceneVisible = true;
608
695
  _thinking = true;
609
696
  EnsureStarted();
610
697
  }
611
- public static void Off() { _acting = false; _preparing = false; _thinking = false; }
612
- public static bool Acting { get { return _acting; } }
698
+ // Renderer visibility is deliberately separate from scene ownership.
699
+ // Hiding the indicator must neither release the scene nor resurrect it
700
+ // on the next frame solely because the authoritative hold is still true.
701
+ static volatile bool _sceneVisible = true;
702
+ public static void Off() { _sceneVisible = false; _preparing = false; _thinking = false; }
703
+ public static bool Acting { get { return SceneControl.Acting; } }
704
+ public static bool Holding { get { return SceneControl.Holding; } }
613
705
  public static bool Preparing { get { return _preparing; } }
614
706
  public static bool PreparationVisible { get { return _preparing && Volatile.Read(ref _presentedPreparation) == Volatile.Read(ref _preparationSerial); } }
615
707
  public static bool PreparationComplete { get { return _preparing && Volatile.Read(ref _completedPreparation) == Volatile.Read(ref _preparationSerial); } }
616
- public static void ActuationBegin() { _acting = true; EnsureStarted(); }
617
- public static void ActuationEnd() { _acting = false; }
708
+ public static void ActuationBegin() { _sceneVisible = true; EnsureStarted(); }
709
+ public static void ActuationEnd() { }
618
710
  public static void Prepare(bool on)
619
711
  {
620
712
  if (!on) { _preparing = false; return; }
621
713
  _preparationStart = 0;
714
+ _preparationMs = PreparationFrequency.Duration(PreparationClock.ElapsedMilliseconds);
622
715
  Interlocked.Increment(ref _preparationSerial);
623
- _preparing = true; _enabled = true; EnsureStarted();
716
+ _preparing = true; _enabled = true; _sceneVisible = true; EnsureStarted();
717
+ }
718
+
719
+ public static void PreparationPresented(int serial)
720
+ {
721
+ _preparationStart = Environment.TickCount;
722
+ PreparationFrequency.Presented(PreparationClock.ElapsedMilliseconds);
723
+ Volatile.Write(ref _presentedPreparation, serial);
624
724
  }
625
725
 
626
726
  // Nested injection sequences stay observable across drag/key/typing gaps.
@@ -629,7 +729,7 @@ namespace DshComputerUse.Worker
629
729
  public static void InputBegin() { Interlocked.Increment(ref _injecting); }
630
730
  public static void InputEnd() { if (Interlocked.Decrement(ref _injecting) < 0) _injecting = 0; }
631
731
  public static bool Injecting { get { return _injecting > 0; } }
632
- public static void KeepActing() { /* Actual operation scope owns the cue, with no trailing hold. */ }
732
+ public static void KeepActing() { /* Solid blue follows actual input; the separate scene hold survives it. */ }
633
733
 
634
734
  // Emergency stop must LOOK like one. The normal fade is a slow ease (~1.6 s) which,
635
735
  // during a panic, reads as "nothing happened" — so this snaps the overlay to zero on
@@ -637,7 +737,7 @@ namespace DshComputerUse.Worker
637
737
  static volatile bool _killNow = false;
638
738
  public static void Kill()
639
739
  {
640
- _acting = false; _preparing = false; _thinking = false; _flashStart = 0;
740
+ _sceneVisible = false; _preparing = false; _thinking = false; _flashStart = 0;
641
741
  _killNow = true;
642
742
  }
643
743
 
@@ -653,6 +753,7 @@ namespace DshComputerUse.Worker
653
753
  public static void Thinking(int holdMs)
654
754
  {
655
755
  if (!_enabled) return;
756
+ _sceneVisible = true;
656
757
  _thinking = true;
657
758
  EnsureStarted();
658
759
  }
@@ -689,7 +790,8 @@ namespace DshComputerUse.Worker
689
790
  public static int DeriveWant(bool flashOn, int committed)
690
791
  {
691
792
  if (Panic.Exited) return 0;
692
- return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (_preparing ? 7 : (Acting ? 1 : (flashOn ? 2 : committed)))));
793
+ bool sceneVisible = _enabled && _sceneVisible;
794
+ return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (_preparing ? 7 : (Acting && sceneVisible ? 1 : (Holding && sceneVisible ? 8 : (flashOn ? 2 : committed))))));
693
795
  }
694
796
  /// The title states the same fact as the light, derived the same way (an ended session must
695
797
  /// not advertise "已暂停 · Ctrl+Alt+R 继续" in the window list).
@@ -697,7 +799,7 @@ namespace DshComputerUse.Worker
697
799
  {
698
800
  if (Panic.Exited) return "AI 已停止操控 · 未监听";
699
801
  return _askMode ? "AI 正在提问 · 在 DSH 聊天里回答 · 无倒计时(Ctrl+Alt+R 撤销)"
700
- : (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : _preparing ? "即将开始操作,请暂停键鼠 · 1.2 秒准备" : label);
802
+ : (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : _preparing ? "即将开始操作,请暂停键鼠 · " + (PreparationMs / 1000.0).ToString("0.0") + " 秒准备" : Acting ? "AI 正在操作 · 请暂勿移动键鼠" : Holding ? "AI 正在检查 · 请保持页面与鼠标位置" : ThinkingActive ? "可自由使用键鼠 · AI 未占用现场" : label);
701
803
  }
702
804
  static volatile string _shownTitle = null;
703
805
  // What the indicator is ACTUALLY rendering right now, readable from outside via `probe`.
@@ -763,7 +865,7 @@ namespace DshComputerUse.Worker
763
865
 
764
866
  /// Force the overlay into existence. Used when a stop is ADOPTED from another process:
765
867
  /// the red border must be visible even though this process never actuated anything.
766
- public static void Reveal() { _enabled = true; EnsureStarted(); }
868
+ public static void Reveal() { _enabled = true; _sceneVisible = true; EnsureStarted(); }
767
869
 
768
870
  static void EnsureStarted()
769
871
  {
@@ -853,6 +955,7 @@ namespace DshComputerUse.Worker
853
955
  }
854
956
  finally
855
957
  {
958
+ AgentPointer.Dispose();
856
959
  _hwnd = IntPtr.Zero; _shownTitle = null;
857
960
  if (oldBmp != IntPtr.Zero && oldBmp != new IntPtr(-1)) SelectObject(memDc, oldBmp);
858
961
  if (dib != IntPtr.Zero) DeleteObject(dib);
@@ -880,9 +983,9 @@ namespace DshComputerUse.Worker
880
983
  _lastLoopTick = Environment.TickCount;
881
984
  _rendererStage = "rendering";
882
985
  Interlocked.Increment(ref _frames);
883
- // Ownership and deep blue use the same actual action scope. There is no
884
- // delayed onset or lingering tail during reading, thinking or preparation.
885
- bool acting = _enabled && Acting;
986
+ // Solid blue is actual input; breathing blue retains the scene between
987
+ // steps. Cyan is reserved for an explicitly free desktop.
988
+ bool acting = _enabled && _sceneVisible && Acting;
886
989
  bool thinking = ThinkingActive;
887
990
  bool flashOn = _flashStart != 0 && unchecked(Environment.TickCount - _flashStart) < _flashMs;
888
991
 
@@ -893,6 +996,7 @@ namespace DshComputerUse.Worker
893
996
  int preparationSerial = Volatile.Read(ref _preparationSerial);
894
997
  int want = DeriveWant(flashOn, committed);
895
998
  _want = want; // publish it: the state the light is showing, as a fact
999
+ AgentPointer.Update(want == 1 || want == 8, want == 8);
896
1000
 
897
1001
  // Keep the overlay's WINDOW TITLE in sync with the state, from the render loop.
898
1002
  // When the title was only written by Stopped(), a process that INHERITED a stop
@@ -912,6 +1016,7 @@ namespace DshComputerUse.Worker
912
1016
  if (_killNow)
913
1017
  {
914
1018
  _killNow = false;
1019
+ AgentPointer.Hide();
915
1020
  alpha = 0;
916
1021
  if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
917
1022
  curBitmap = 0; committed = 0; paintedFlash = 0;
@@ -921,7 +1026,7 @@ namespace DshComputerUse.Worker
921
1026
  }
922
1027
  // want: 0 idle | 1 acting (deep blue) | 3 thinking (pale blue) | 2 flash (yellow)
923
1028
  // 4 emergency-stopped (RED, steady — the session is dead until re-armed)
924
- int needBitmap = want == 7 ? 6 : want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : (want == 1 ? 3 : (want == 4 || want == 5 ? 4 : 1)));
1029
+ int needBitmap = want == 7 ? 6 : want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : ((want == 1 || want == 8) ? 3 : (want == 4 || want == 5 ? 4 : 1)));
925
1030
  // A REPAINT MUST REACH THE SCREEN EVEN WHEN ALPHA DOES NOT MOVE.
926
1031
  //
927
1032
  // The present below used to be gated on `diff != 0` alone, while the flash snaps
@@ -961,6 +1066,7 @@ namespace DshComputerUse.Worker
961
1066
  flashFrame = true;
962
1067
  }
963
1068
  else if (want == 1) { target = FULL; breathFrame = true; }
1069
+ else if (want == 8) { target = HoldingAlpha(Environment.TickCount); breathFrame = true; }
964
1070
  else if (want == 7) { target = 255; breathFrame = true; }
965
1071
  else if (want == 4) target = FULL; // steady red, no breathing
966
1072
  else if (want == 6)
@@ -1051,8 +1157,7 @@ namespace DshComputerUse.Worker
1051
1157
  }
1052
1158
  if (presented && want == 7 && !PreparationVisible)
1053
1159
  {
1054
- _preparationStart = Environment.TickCount;
1055
- Volatile.Write(ref _presentedPreparation, preparationSerial);
1160
+ PreparationPresented(preparationSerial);
1056
1161
  }
1057
1162
  if (presented && want == 7 && preparationElapsed >= PreparationMs)
1058
1163
  Volatile.Write(ref _completedPreparation, preparationSerial);
@@ -1154,6 +1259,9 @@ namespace DshComputerUse.Worker
1154
1259
  catch { /* display preference only; never changes control or capture behavior */ }
1155
1260
  }
1156
1261
 
1262
+ public static int HoldingAlpha(int tick)
1263
+ { return 120 + (int)Math.Round(42 * (1 - Math.Cos((unchecked((uint)tick) % 2500) * 2 * Math.PI / 2500.0))); }
1264
+
1157
1265
  static void PaintBlue(IntPtr bits, int w, int h, bool intense)
1158
1266
  {
1159
1267
  WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
@@ -1449,6 +1557,7 @@ namespace DshComputerUse.Worker
1449
1557
  CycleLit = false; // R2: the cycle is dead and must not be revived by anyone
1450
1558
  _controlsReleased = true;
1451
1559
  Disarm(); // ... and its monitors die WITH it, not at the next turn
1560
+ SceneControl.Reset();
1452
1561
  try { Glow.Kill(); } catch { }
1453
1562
  try { Glow.Ask(false); } catch { }
1454
1563
  try { Glow.Stopped(false); } catch { }
@@ -1479,7 +1588,8 @@ namespace DshComputerUse.Worker
1479
1588
  // is what made the first signed op hide a brake the human had already set.
1480
1589
  if (_engaged || _stopped)
1481
1590
  {
1482
- try { Glow.Kill(); } catch { }
1591
+ SceneControl.Reset();
1592
+ try { Glow.Kill(); } catch { }
1483
1593
  try { Glow.Stopped(true); } catch { }
1484
1594
  // START the overlay too: `Thinking()` was the only thing that ever did, so a
1485
1595
  // cycle whose FIRST call arrives with a brake already standing painted nothing at
@@ -1902,7 +2012,8 @@ namespace DshComputerUse.Worker
1902
2012
  bool showRed = !_exited && CycleLit;
1903
2013
  if (showRed)
1904
2014
  {
1905
- try { Glow.Kill(); } catch { }
2015
+ SceneControl.Reset();
2016
+ try { Glow.Kill(); } catch { }
1906
2017
  try { Glow.Stopped(true); } catch { }
1907
2018
  try { Glow.Reveal(); } catch { }
1908
2019
  }
@@ -2053,6 +2164,12 @@ namespace DshComputerUse.Worker
2053
2164
  if (m.dwExtraInfo != OwnMagic) Interlocked.Increment(ref _externalInput);
2054
2165
  bool physical = PhysicalEvent(m.dwExtraInfo, m.flags, 3);
2055
2166
  int message = wParam.ToInt32();
2167
+ if (!physical) {
2168
+ Interlocked.Exchange(ref _lastInjectedInputAt, HumanClock.ElapsedMilliseconds);
2169
+ int injectedKey = message == 0x201 || message == 0x202 ? 1 : message == 0x204 || message == 0x205 ? 2 :
2170
+ message == 0x207 || message == 0x208 ? 4 : message == 0x20B || message == 0x20C ? (((m.mouseData >> 16) & 0xFFFF) == 1 ? 5 : 6) : 0;
2171
+ if (injectedKey != 0) Handoff.NoteInjected(injectedKey);
2172
+ }
2056
2173
  // Significant idle motion invalidates observations, but does not yield.
2057
2174
  bool movement = message == 0x200 && PointerMotion.Record(m.pt.X, m.pt.Y, physical,
2058
2175
  true, Handoff.Active, HumanClock.ElapsedMilliseconds);
@@ -2094,6 +2211,10 @@ namespace DshComputerUse.Worker
2094
2211
  bool down = m == Native.WM_KEYDOWN_ || m == Native.WM_SYSKEYDOWN_;
2095
2212
  bool up = m == Native.WM_KEYUP_ || m == Native.WM_SYSKEYUP_;
2096
2213
  bool human = ClassifyAndTrackKey(k.vkCode, down, up, k.dwExtraInfo, k.flags);
2214
+ if (!human && (down || up)) {
2215
+ Interlocked.Exchange(ref _lastInjectedInputAt, HumanClock.ElapsedMilliseconds);
2216
+ Handoff.NoteInjected((int)k.vkCode);
2217
+ }
2097
2218
  if (human && (down || up))
2098
2219
  {
2099
2220
  RecordHumanInput((int)k.vkCode, down, up);
@@ -2283,6 +2404,7 @@ namespace DshComputerUse.Worker
2283
2404
  public static bool Armed { get { return _armed; } }
2284
2405
  public static bool Exited { get { return _exited; } }
2285
2406
  static readonly Stopwatch HumanClock = Stopwatch.StartNew();
2407
+ static long _lastInjectedInputAt;
2286
2408
  static readonly HumanInputState Handoff = new HumanInputState();
2287
2409
  static readonly object HandoffPublishLock = new object();
2288
2410
  static string _publishedHandoff = "idle";
@@ -2296,6 +2418,7 @@ namespace DshComputerUse.Worker
2296
2418
  {
2297
2419
  var state = (Dictionary<string, object>)Handoff.Snapshot(HumanClock.ElapsedMilliseconds);
2298
2420
  state["inputEpoch"] = InputEpoch; state["mouseTolerancePx"] = PointerHandoffTolerance.DistancePx;
2421
+ state["workerGeneration"] = Program.WorkerGeneration;
2299
2422
  return state;
2300
2423
  }
2301
2424
  static void ResetMonitorRun() { /* Manual recovery must not erase physical input history. */ }
@@ -2308,6 +2431,18 @@ namespace DshComputerUse.Worker
2308
2431
  if (desktop != IntPtr.Zero) { Native.CloseDesktop(desktop); return true; }
2309
2432
  return false;
2310
2433
  }
2434
+ static bool OnNormalInputDesktop()
2435
+ {
2436
+ // Async state may fail when the caller lacks desktop hook access.
2437
+ IntPtr desktop = Native.OpenInputDesktop(0, false, 0x0001 | 0x0008);
2438
+ if (desktop == IntPtr.Zero) return false;
2439
+ try {
2440
+ var current = new StringBuilder(256); var input = new StringBuilder(256); int needed;
2441
+ return Native.GetUserObjectInformation(Native.GetThreadDesktop(Native.GetCurrentThreadId()), 2, current, 512, out needed) &&
2442
+ Native.GetUserObjectInformation(desktop, 2, input, 512, out needed) &&
2443
+ String.Equals(current.ToString(), input.ToString(), StringComparison.OrdinalIgnoreCase);
2444
+ } finally { Native.CloseDesktop(desktop); }
2445
+ }
2311
2446
  /// Polled from the monitor thread. Audits and notifies TRANSITIONS only.
2312
2447
  static void PollSecureDesktop()
2313
2448
  {
@@ -2330,19 +2465,17 @@ namespace DshComputerUse.Worker
2330
2465
  throw error;
2331
2466
  }
2332
2467
  public static void Arm() { _armed = true; Handoff.Enable(CycleLit && !_exited, HumanClock.ElapsedMilliseconds); }
2333
- public static void Disarm() { _armed = false; Handoff.Enable(false, HumanClock.ElapsedMilliseconds); PublishHandoff(); }
2468
+ public static void Disarm() { _armed = false; SceneControl.Reset(); Handoff.Enable(false, HumanClock.ElapsedMilliseconds); PublishHandoff(); }
2334
2469
  public static void AgentActed() { _agentActAt = Environment.TickCount; }
2335
2470
  public static void BeginActuation()
2336
2471
  {
2337
2472
  POINT point; if (Native.GetCursorPos(out point)) PointerMotion.Reset(point.X, point.Y);
2338
- Glow.ActuationBegin();
2473
+ SceneControl.BeginActuation(); Glow.ActuationBegin();
2339
2474
  Handoff.SetActing(true, HumanClock.ElapsedMilliseconds);
2340
2475
  }
2341
- public static void EndActuation()
2342
- {
2343
- Handoff.SetActing(false, HumanClock.ElapsedMilliseconds);
2344
- Glow.ActuationEnd();
2345
- }
2476
+ public static void EndActuation() { SceneControl.EndActuation(); Glow.ActuationEnd(); }
2477
+ public static void UpdateInputProtection()
2478
+ { Handoff.SetActing(SceneControl.Acting || SceneControl.Holding, HumanClock.ElapsedMilliseconds); }
2346
2479
  static bool IsBareModifier(uint vk)
2347
2480
  {
2348
2481
  return vk == Native.VK_SHIFT || vk == Native.VK_CONTROL || vk == Native.VK_MENU ||
@@ -2375,7 +2508,12 @@ namespace DshComputerUse.Worker
2375
2508
  _publishedInputEpoch = epoch;
2376
2509
  if (!ControlListenersEnabled && !CycleLit && phase == "idle") return;
2377
2510
  if (phase != "idle") Glow.HumanYield();
2378
- if (changed) Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + HumanRevision + "; kind=" + Handoff.LastKind);
2511
+ if (changed) {
2512
+ var state = (Dictionary<string, object>)CooperationState();
2513
+ Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + state["revision"] +
2514
+ "; kind=" + state["lastKind"] + "; heldCount=" + state["heldCount"] +
2515
+ "; quietRemainingMs=" + state["quietRemainingMs"] + "; inputEpoch=" + state["inputEpoch"]);
2516
+ }
2379
2517
  Notify("handoff", phase);
2380
2518
  }
2381
2519
  }
@@ -2386,14 +2524,37 @@ namespace DshComputerUse.Worker
2386
2524
  PollSecureDesktop();
2387
2525
  long now = HumanClock.ElapsedMilliseconds;
2388
2526
  Handoff.Enable(_armed && CycleLit && !_exited, now);
2527
+ IntPtr sampleForeground = IntPtr.Zero;
2528
+ bool trusted = Handoff.HasHeld && CanReconcileHeld(_hooksReady, _thread != null && _thread.IsAlive, SecureDesktopActive,
2529
+ SceneControl.Acting, Glow.Injecting, now - Interlocked.Read(ref _lastInjectedInputAt), OnNormalInputDesktop()) &&
2530
+ Program.CanObserveAsyncKeys(out sampleForeground);
2531
+ bool swapped = Native.GetSystemMetrics(23) != 0; // SM_SWAPBUTTON
2532
+ int reconciled = Handoff.ReconcileReleased(delegate(int key) {
2533
+ bool down = HumanInputState.AsyncButtonMayBeDown(key, swapped,
2534
+ delegate(int physical) { return (Native.GetAsyncKeyState(physical) & 0x8000) != 0; });
2535
+ // A foreground/desktop switch during the sample makes zero unknown,
2536
+ // never evidence of release. The next tick checks the new token.
2537
+ return down || Native.GetForegroundWindow() != sampleForeground || !OnNormalInputDesktop();
2538
+ }, trusted, now);
2539
+ if (reconciled > 0) Program.AuditRecord("HUMAN-RELEASE-RECONCILED", "count=" + reconciled + "; source=three-normal-desktop-async-up-samples; quietMs=" + HumanInputState.IdleMs);
2389
2540
  Handoff.Tick(now);
2390
2541
  if (Handoff.Active) Program.ReleaseOwnedInputs();
2391
2542
  PublishHandoff();
2392
2543
  }
2544
+ static bool CanReconcileHeld(bool hooksReady, bool hookAlive, bool secure, bool acting, bool injecting, long sinceInjected, bool inputDesktop)
2545
+ { return hooksReady && hookAlive && !secure && !acting && !injecting && sinceInjected >= 300 && inputDesktop; }
2393
2546
 
2394
2547
  static RecoveryPermit _temporary;
2395
2548
  static long _externalInput;
2396
2549
  public static string TemporaryPauseId { get { lock (SyncLock) { return _temporary == null ? "" : _temporary.Id; } } }
2550
+ public static string EventPauseId(string kind)
2551
+ {
2552
+ // Handoff events are serialized under HandoffPublishLock. Lifecycle
2553
+ // transitions take SyncLock then publish, so their notification must
2554
+ // not take SyncLock in the reverse order. These two observation events
2555
+ // neither own nor consume temporary-pause credentials.
2556
+ return kind == "handoff" || kind == "secure" ? "" : TemporaryPauseId;
2557
+ }
2397
2558
  public static void ObserveTemporary(string id)
2398
2559
  {
2399
2560
  lock (SyncLock) { if (_temporary != null && _temporary.Id == id) _temporary.Observed = true; }
@@ -2469,6 +2630,7 @@ namespace DshComputerUse.Worker
2469
2630
  InputVersion = Interlocked.Read(ref _externalInput), InputTick = Native.LastInputTick() };
2470
2631
  WriteStopFile(why + "\nsource=agent-temporary\npauseId=" + _temporary.Id);
2471
2632
  }
2633
+ SceneControl.Reset();
2472
2634
  try { Glow.Kill(); } catch { }
2473
2635
  try { Glow.Stopped(true); } catch { } // red border + red badge: unmistakable
2474
2636
  // THE BRAKE IS A STATE CHANGE, SO IT GETS A LINE (2026-09-14). This is the record the
@@ -2531,6 +2693,7 @@ namespace DshComputerUse.Worker
2531
2693
  Program.AuditRecord("ENGAGE-ASK", "source=agent" +
2532
2694
  "; askPid=" + Process.GetCurrentProcess().Id +
2533
2695
  "; cycleLit=" + (CycleLit ? 1 : 0) + "; why=" + why);
2696
+ SceneControl.Reset();
2534
2697
  try { Glow.Kill(); } catch { }
2535
2698
  try { Glow.Ask(true); } catch { } // fast-breathing RED, not the steady stop
2536
2699
  announce = true;
@@ -2963,6 +3126,51 @@ namespace DshComputerUse.Worker
2963
3126
  }
2964
3127
  }
2965
3128
 
3129
+ // Only complete window-management chords may change their own foreground receiver.
3130
+ // Every down still passes the ordinary receiver guard. After the final accepted down,
3131
+ // only this chord's owned releases may bypass that guard; human/pause/cancel still win.
3132
+ public static class WindowManagementChord
3133
+ {
3134
+ public static bool IsExact(string canonical)
3135
+ {
3136
+ switch (canonical)
3137
+ {
3138
+ case "alt+tab": case "alt+shift+tab": case "alt+esc": case "alt+shift+esc":
3139
+ case "win+d": case "win+tab": case "win+left": case "win+right": case "win+up": case "win+down":
3140
+ case "rwin+d": case "rwin+tab": case "rwin+left": case "rwin+right": case "rwin+up": case "rwin+down":
3141
+ return true;
3142
+ default: return false;
3143
+ }
3144
+ }
3145
+
3146
+ public static void Run(IList<ushort> keys, Action<ushort, bool> send,
3147
+ Action<ushort> release, Func<ushort, bool> isDown, Action hold,
3148
+ Action checkContinuation, Action invalidateTarget)
3149
+ {
3150
+ int acceptedDowns = 0;
3151
+ try
3152
+ {
3153
+ KeyboardChord.Run(keys, delegate(ushort key, bool up) {
3154
+ if (up && acceptedDowns == keys.Count)
3155
+ {
3156
+ checkContinuation();
3157
+ release(key); // ledger-backed: never releases someone else's key
3158
+ }
3159
+ else
3160
+ {
3161
+ send(key, up);
3162
+ if (!up) acceptedDowns++;
3163
+ }
3164
+ }, release, isDown, hold);
3165
+ }
3166
+ finally
3167
+ {
3168
+ // Even a partial chord can open system UI. No stale sticky/refocus continuation.
3169
+ if (acceptedDowns > 0) invalidateTarget();
3170
+ }
3171
+ }
3172
+ }
3173
+
2966
3174
  public sealed class ClipboardBytes
2967
3175
  {
2968
3176
  public uint Format;
@@ -3163,6 +3371,16 @@ namespace DshComputerUse.Worker
3163
3371
 
3164
3372
  static class Program
3165
3373
  {
3374
+ public const int ProtocolVersion = 2;
3375
+ public static readonly string WorkerGeneration = Guid.NewGuid().ToString("N");
3376
+ static Dictionary<string, object> RuntimeIdentity()
3377
+ {
3378
+ return Dict("packageVersion", Environment.GetEnvironmentVariable("DSH_CU_PACKAGE_VERSION") ?? "unreported",
3379
+ "buildId", Environment.GetEnvironmentVariable("DSH_CU_BUILD_ID") ?? "unreported",
3380
+ "sourceHash", Environment.GetEnvironmentVariable("DSH_CU_SOURCE_HASH") ?? "unreported",
3381
+ "binaryHash", Environment.GetEnvironmentVariable("DSH_CU_BINARY_HASH") ?? "unreported",
3382
+ "protocolVersion", ProtocolVersion, "workerGeneration", WorkerGeneration);
3383
+ }
3166
3384
  static readonly JavaScriptSerializer Json = new JavaScriptSerializer();
3167
3385
  static int _reqId = 0;
3168
3386
  static readonly object OutLock = new object(); // responses AND unsolicited events share stdout
@@ -3225,6 +3443,33 @@ namespace DshComputerUse.Worker
3225
3443
  return Native.SendInput((uint)inputs.Length, inputs, InputCbSize);
3226
3444
  };
3227
3445
  internal static Func<IntPtr> PreparationForeground = delegate { return Native.GetForegroundWindow(); };
3446
+ internal static Func<IntPtr> InputForeground = delegate { return TopLevel(Native.GetForegroundWindow()); };
3447
+ internal static Func<IntPtr> InputPointReceiver = delegate {
3448
+ POINT p; return Native.GetCursorPos(out p) ? TopLevel(Native.WindowFromPoint(p)) : IntPtr.Zero;
3449
+ };
3450
+ internal static Func<IntPtr, bool> InputWindowExists = delegate(IntPtr h) { return Native.IsWindow(h); };
3451
+ internal static Func<IntPtr, uint> InputWindowPid = delegate(IntPtr h) { return PidOf(h); };
3452
+ [ThreadStatic] static Action _receiverCheck;
3453
+ sealed class ReceiverScope : IDisposable
3454
+ {
3455
+ readonly Action previous;
3456
+ public ReceiverScope(Action check) { previous = _receiverCheck; check(); _receiverCheck = check; }
3457
+ public void Dispose() { _receiverCheck = previous; }
3458
+ }
3459
+ static string InputReceiverProblem(long expected, uint expectedPid, bool exists, uint livePid, long actual)
3460
+ {
3461
+ if (expected == 0 || !exists || livePid != expectedPid) return "observed target disappeared or changed identity";
3462
+ if (actual != expected) return "input receiver changed from hwnd=" + expected + " to hwnd=" + actual;
3463
+ return null;
3464
+ }
3465
+ static IDisposable GuardReceiver(IntPtr expected, uint pid, bool keyboard)
3466
+ {
3467
+ return new ReceiverScope(delegate {
3468
+ IntPtr actual = keyboard ? InputForeground() : InputPointReceiver();
3469
+ string problem = InputReceiverProblem(expected.ToInt64(), pid, InputWindowExists(expected), InputWindowPid(expected), actual.ToInt64());
3470
+ if (problem != null) throw InputInterrupted("INPUT_TARGET_CHANGED", problem + ". Remaining input was refused; inspect any partial effect before retrying.");
3471
+ });
3472
+ }
3228
3473
 
3229
3474
  static Exception InputInterrupted(string code, string message)
3230
3475
  {
@@ -3321,11 +3566,31 @@ namespace DshComputerUse.Worker
3321
3566
  }
3322
3567
  public static bool CancelInput(int requestId)
3323
3568
  {
3324
- if (requestId <= 0 || requestId <= _lastCompletedRequestId) return false;
3325
- CancelledRequests[requestId] = true;
3326
- if (_activeRequestId == requestId)
3327
- { Interlocked.Increment(ref _cancelGeneration); ReleaseOwnedInputs(); }
3328
- return true;
3569
+ return GetB(CancelInputStatus(requestId), "cancelled", false);
3570
+ }
3571
+ static Dictionary<string, object> CancelInputStatus(int requestId)
3572
+ {
3573
+ bool valid = requestId > 0;
3574
+ bool completed = valid && requestId <= _lastCompletedRequestId;
3575
+ if (valid && !completed) CancelledRequests[requestId] = true;
3576
+ bool active = valid && _activeRequestId == requestId;
3577
+ if (active) Interlocked.Increment(ref _cancelGeneration);
3578
+ bool cleanup = false; int owned = -1; string state = valid ? "input-lock-busy" : "invalid-request";
3579
+ // The reader must remain responsive even if SendInput/provider work
3580
+ // owns InputLock. Failure to obtain it is uncertainty, not clean-up.
3581
+ if (valid && Monitor.TryEnter(InputLock, 200)) {
3582
+ try {
3583
+ // Never release a newer request's accepted input on behalf
3584
+ // of an older cancellation.
3585
+ if (_activeRequestId == requestId) ReleaseOwnedInputs();
3586
+ owned = OwnedUps.Count;
3587
+ cleanup = owned == 0;
3588
+ state = cleanup ? "no-owned-input" : "owned-input-remains";
3589
+ } finally { Monitor.Exit(InputLock); }
3590
+ }
3591
+ return Dict("requestId", requestId, "cancelled", valid && !completed,
3592
+ "cleanupConfirmed", cleanup, "ownedInputCount", owned, "cleanupState", state,
3593
+ "workerGeneration", WorkerGeneration);
3329
3594
  }
3330
3595
  static string InputKey(INPUT input, out bool up, out INPUT release, out int physical)
3331
3596
  {
@@ -3366,6 +3631,8 @@ namespace DshComputerUse.Worker
3366
3631
  string key = InputKey(inputs[0], out isUp, out release, out physical);
3367
3632
  if (isUp && key != null && !OwnedUps.ContainsKey(key)) return 1;
3368
3633
  }
3634
+ if (_receiverCheck != null) _receiverCheck();
3635
+ CheckHumanInput();
3369
3636
  uint sent = InputSender(inputs);
3370
3637
  if (sent > 0) _operationDispatched = true;
3371
3638
  for (int i = 0; i < Math.Min(sent, inputs.Length); i++)
@@ -3509,6 +3776,16 @@ namespace DshComputerUse.Worker
3509
3776
  [STAThread]
3510
3777
  static int Main(string[] args)
3511
3778
  {
3779
+ // The cursor guardian owns only the temporary visual-hide lease. It
3780
+ // must never install input hooks, open a control cycle or emit worker
3781
+ // protocol messages into the parent's inherited output stream.
3782
+ if (args.Length > 0 && args[0] == "--pointer-guardian")
3783
+ {
3784
+ int ownerPid; long ownerStartTicks;
3785
+ if (args.Length != 4 || !Int32.TryParse(args[2], out ownerPid) ||
3786
+ !Int64.TryParse(args[3], out ownerStartTicks)) return 2;
3787
+ return PointerVisibilityLease.GuardianMain(args[1], ownerPid, ownerStartTicks);
3788
+ }
3512
3789
  // DPI awareness first: it decides the coordinate space of everything below
3513
3790
  // (GetSystemMetrics, GetCursorPos, capture pixels, UIA rects AND the
3514
3791
  // interpretation of SendInput's absolute coordinates). Record which call won.
@@ -3537,8 +3814,9 @@ namespace DshComputerUse.Worker
3537
3814
  // lives; the consumer can read the fields and draw no conclusion the worker did
3538
3815
  // not record itself.
3539
3816
  string hint;
3540
- if (kind == "panic" && !string.IsNullOrEmpty(Panic.TemporaryPauseId))
3541
- hint = "Agent temporary diagnostic pause. Inspect the current target, then use checked recover only if the blocker is resolved. Credential: " + Panic.TemporaryPauseId;
3817
+ string pauseId = Panic.EventPauseId(kind);
3818
+ if (kind == "panic" && !string.IsNullOrEmpty(pauseId))
3819
+ hint = "Agent temporary diagnostic pause. Inspect the current target, then use checked recover only if the blocker is resolved. Credential: " + pauseId;
3542
3820
  else if (kind == "panic")
3543
3821
  hint = "STOPPED (brake engaged). The machine is stopped: nothing that drives it runs " +
3544
3822
  "until a human resumes with Ctrl+Alt+R. WRAP UP NOW: one short summary of what " +
@@ -3563,7 +3841,7 @@ namespace DshComputerUse.Worker
3563
3841
  WriteLine(Json.Serialize(Dict(
3564
3842
  "event", kind,
3565
3843
  "why", why,
3566
- "pauseId", Panic.TemporaryPauseId,
3844
+ "pauseId", pauseId,
3567
3845
  "cooperation", Panic.CooperationState(),
3568
3846
  "ts", NowMs(),
3569
3847
  "hint", hint)));
@@ -3600,6 +3878,8 @@ namespace DshComputerUse.Worker
3600
3878
  Dictionary<string, object> hello = Dict(
3601
3879
  "event", "ready",
3602
3880
  "pid", Process.GetCurrentProcess().Id,
3881
+ "protocolVersion", ProtocolVersion, "workerGeneration", WorkerGeneration,
3882
+ "runtime", RuntimeIdentity(),
3603
3883
  // COLD-START STATE, REPORTED AS A FACT (cold-start bug, 2026-09-13): this process has
3604
3884
  // already adopted any STOP/EXITED record that was on disk before it existed, so the
3605
3885
  // core can start from the machine's real state instead of "not stopped" — the two
@@ -3624,9 +3904,9 @@ namespace DshComputerUse.Worker
3624
3904
  {
3625
3905
  object argsValue;
3626
3906
  var cancelArgs = request.TryGetValue("args", out argsValue) ? argsValue as Dictionary<string, object> : null;
3627
- bool cancelled = cancelArgs != null && CancelInput(GetI(cancelArgs, "requestId", 0));
3907
+ var cancellation = CancelInputStatus(cancelArgs == null ? 0 : GetI(cancelArgs, "requestId", 0));
3628
3908
  WriteLine(parser.Serialize(Dict("id", GetI(request, "id", 0), "ok", true,
3629
- "data", Dict("cancelled", cancelled))));
3909
+ "data", cancellation)));
3630
3910
  continue;
3631
3911
  }
3632
3912
  } catch { /* Main loop reports malformed requests in the normal format. */ }
@@ -3893,7 +4173,7 @@ namespace DshComputerUse.Worker
3893
4173
  // Is a real injection sequence in flight RIGHT NOW? The ACTING light is driven
3894
4174
  // by the actual action scope, so both are reported as observable facts.
3895
4175
  "injecting", Glow.Injecting,
3896
- "acting", Glow.Acting, "preparing", Glow.Preparing, "preparationMs", Glow.PreparationMs,
4176
+ "acting", Glow.Acting, "holding", Glow.Holding, "preparing", Glow.Preparing, "preparationMs", Glow.PreparationMs,
3897
4177
  "title", Glow.ShownTitle == null ? "" : Glow.ShownTitle);
3898
4178
  // EVENTS THE OS ACTUALLY INSERTED (not the plan): 0 after a plain pause with
3899
4179
  // nothing held is the observable form of "no ghost right-click / Alt menu".
@@ -4090,6 +4370,7 @@ namespace DshComputerUse.Worker
4090
4370
  IntPtr old = Native.SelectObject(mem, dib);
4091
4371
  // SRCCOPY | CAPTUREBLT so layered windows are included
4092
4372
  Native.BitBlt(mem, 0, 0, w, h, screen, x, y, 0x00CC0020 | 0x40000000);
4373
+ long capturedAt = NowMs();
4093
4374
  bool cursorDrawn = false;
4094
4375
  int cursorFlags = 0, cursorAtX = 0, cursorAtY = 0;
4095
4376
  if (withCursor)
@@ -4152,13 +4433,16 @@ namespace DshComputerUse.Worker
4152
4433
  ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, q);
4153
4434
  outBmp.Save(ms, codec, ep);
4154
4435
  }
4436
+ int imageWidth = outBmp.Width, imageHeight = outBmp.Height;
4155
4437
  outBmp.Dispose();
4156
4438
  POINT cp2; Native.GetCursorPos(out cp2);
4157
4439
  Dictionary<string, object> res = Dict(
4158
4440
  "image", Convert.ToBase64String(ms.ToArray()),
4159
4441
  "mime", mime,
4160
4442
  "region", Dict("x", x, "y", y, "width", w, "height", h),
4161
- "scaledTo", maxWidth > 0 ? maxWidth : w,
4443
+ "scaledTo", imageWidth,
4444
+ "imageSize", new int[] { imageWidth, imageHeight },
4445
+ "capturedAt", capturedAt,
4162
4446
  "cursor", PtDict(cp2.X, cp2.Y),
4163
4447
  "ts", NowMs());
4164
4448
  if (withCursor)
@@ -4170,7 +4454,8 @@ namespace DshComputerUse.Worker
4170
4454
  res["cursorDrawn"] = cursorDrawn;
4171
4455
  res["cursorFlags"] = cursorFlags;
4172
4456
  res["cursorScreenPos"] = PtDict(cursorAtX, cursorAtY);
4173
- res["cursorInImage"] = PtDict(cursorAtX - x, cursorAtY - y);
4457
+ res["cursorInImage"] = Dict("x", (cursorAtX - x) * (double)imageWidth / w,
4458
+ "y", (cursorAtY - y) * (double)imageHeight / h);
4174
4459
  res["cursorNote"] = cursorDrawn
4175
4460
  ? "pointer burned into the image at cursorInImage"
4176
4461
  : ((cursorFlags & 1) == 0
@@ -4316,6 +4601,46 @@ namespace DshComputerUse.Worker
4316
4601
  SendInputChecked(new INPUT[] { inp });
4317
4602
  }
4318
4603
 
4604
+ internal static string StandardCursorShape(IntPtr cursor, Func<int, IntPtr> load)
4605
+ {
4606
+ if (cursor == IntPtr.Zero) return "unknown";
4607
+ int[] ids = { 32644, 32645, 32643, 32642, 32646, 32512, 32513, 32649, 32514, 32515, 32648, 32650, 32651 };
4608
+ string[] names = { "resize-ew", "resize-ns", "resize-nesw", "resize-nwse", "move", "arrow", "ibeam", "hand", "wait", "cross", "not-allowed", "app-starting", "help" };
4609
+ for (int i = 0; i < ids.Length; i++) if (load(ids[i]) == cursor) return names[i];
4610
+ return "unknown"; // Custom/browser-drawn cursors do not reveal a CSS value.
4611
+ }
4612
+
4613
+ static bool CursorMatches(string expected, string actual, bool visible)
4614
+ {
4615
+ if (String.IsNullOrEmpty(expected)) return true;
4616
+ if (!visible || actual == "unknown") return false;
4617
+ return expected == "resize" ? actual.StartsWith("resize-", StringComparison.Ordinal) : expected == actual;
4618
+ }
4619
+
4620
+ static Dictionary<string, object> CursorEvidence(bool available, bool visible, IntPtr cursor, Func<int, IntPtr> load)
4621
+ {
4622
+ visible = available && visible;
4623
+ string shape = visible ? StandardCursorShape(cursor, load) : "unknown";
4624
+ string classification = !available ? "unavailable" : !visible ? "not-visible" : shape == "unknown" ? "unrecognized" : "standard";
4625
+ var result = Dict("available", available, "visible", visible, "shape", shape,
4626
+ "classification", classification, "source", "win32-standard-cursor");
4627
+ if (classification == "unrecognized")
4628
+ result["note"] = "Visible cursor handle does not match a supported Windows standard cursor. Browser col-resize/row-resize and custom cursors may be unrecognized; this does not prove a hit or a miss on a draggable handle.";
4629
+ return result;
4630
+ }
4631
+
4632
+ static Dictionary<string, object> CursorSnapshot()
4633
+ {
4634
+ CURSORINFO ci = new CURSORINFO();
4635
+ ci.cbSize = (uint)Marshal.SizeOf(typeof(CURSORINFO));
4636
+ bool available = Native.GetCursorInfo(ref ci);
4637
+ bool visible = available && (ci.flags & Native.CURSOR_SHOWING) != 0;
4638
+ var result = CursorEvidence(available, visible, ci.hCursor,
4639
+ delegate(int id) { return Native.LoadCursor(IntPtr.Zero, new IntPtr(id)); });
4640
+ result["screenPos"] = available ? (object)PtDict(ci.ptScreenPos.X, ci.ptScreenPos.Y) : null;
4641
+ return result;
4642
+ }
4643
+
4319
4644
  static Dictionary<string, object> Move(Dictionary<string, object> a)
4320
4645
  {
4321
4646
  int x = GetI(a, "x", int.MinValue), y = GetI(a, "y", int.MinValue);
@@ -4337,11 +4662,75 @@ namespace DshComputerUse.Worker
4337
4662
  Native.GetCursorPos(out p);
4338
4663
  }
4339
4664
  CheckHumanInput();
4340
- return Dict("x", p.X, "y", p.Y, "positionCorrections", _positionCorrections);
4665
+ return Dict("x", p.X, "y", p.Y, "positionCorrections", _positionCorrections, "cursor", CursorSnapshot());
4666
+ }
4667
+
4668
+ static Rectangle ClickSampleRegion(int x, int y, Rectangle desktop)
4669
+ {
4670
+ int width = Math.Min(340, desktop.Width), height = Math.Min(340, desktop.Height);
4671
+ if (width <= 0 || height <= 0) return Rectangle.Empty;
4672
+ int left = (int)Math.Min(Math.Max((long)x - width / 2, desktop.Left), (long)desktop.Right - width);
4673
+ int top = (int)Math.Min(Math.Max((long)y - height / 2, desktop.Top), (long)desktop.Bottom - height);
4674
+ return new Rectangle(left, top, width, height);
4675
+ }
4676
+
4677
+ static int VerificationNumber(Dictionary<string, object> rectangle, string key)
4678
+ {
4679
+ object raw;
4680
+ if (!rectangle.TryGetValue(key, out raw) || !(raw is int || raw is long || raw is double || raw is float || raw is decimal))
4681
+ throw new ArgumentException("missing integer " + key);
4682
+ double n = Convert.ToDouble(raw);
4683
+ if (Double.IsNaN(n) || Double.IsInfinity(n) || n != Math.Truncate(n) || n <= Int32.MinValue || n > Int32.MaxValue)
4684
+ throw new ArgumentException("invalid integer " + key);
4685
+ return (int)n;
4686
+ }
4687
+ static Rectangle VerificationSampleRegion(Dictionary<string, object> a, int x, int y, Rectangle desktop)
4688
+ {
4689
+ if (!a.ContainsKey("verificationRegion")) return ClickSampleRegion(x, y, desktop);
4690
+ try {
4691
+ var box = a["verificationRegion"] as Dictionary<string, object>;
4692
+ if (box == null) throw new ArgumentException("expected rectangle object");
4693
+ int rx = VerificationNumber(box, "x"), ry = VerificationNumber(box, "y");
4694
+ int rw = VerificationNumber(box, "width"), rh = VerificationNumber(box, "height");
4695
+ if (rw <= 0 || rh <= 0 || (long)rw * rh > 16777216 ||
4696
+ (long)rx + rw > Int32.MaxValue || (long)ry + rh > Int32.MaxValue)
4697
+ throw new ArgumentException("positive size, 16M pixel maximum, non-overflowing edges required");
4698
+ Rectangle requested = new Rectangle(rx, ry, rw, rh);
4699
+ Rectangle visible = Rectangle.Intersect(requested, desktop);
4700
+ if (visible.Width <= 0 || visible.Height <= 0) throw new ArgumentException("region is outside the virtual desktop");
4701
+ return visible;
4702
+ } catch (Exception error) {
4703
+ var refusal = new ArgumentException("INVALID_VERIFICATION_REGION: " + error.Message + "; no input was dispatched.");
4704
+ refusal.Data["code"] = "INVALID_VERIFICATION_REGION"; refusal.Data["outcome"] = "not-dispatched";
4705
+ throw refusal;
4706
+ }
4707
+ }
4708
+ static void AddPixelVerification(Dictionary<string, object> result, Dictionary<string, object> args,
4709
+ Rectangle region, byte[] before, byte[] after)
4710
+ {
4711
+ result["effectVerified"] = false;
4712
+ result["verificationAvailable"] = before != null && after != null && before.Length > 0 && before.Length == after.Length;
4713
+ result["verificationSource"] = args.ContainsKey("verificationRegion") ? "requested-region" : "click-neighborhood";
4714
+ result["verificationRegion"] = Dict("x", region.X, "y", region.Y, "width", region.Width, "height", region.Height);
4715
+ if (args.ContainsKey("verificationRegion")) result["requestedVerificationRegion"] = args["verificationRegion"];
4716
+ if (!(bool)result["verificationAvailable"]) {
4717
+ result["verificationNote"] = "Pixel samples unavailable; input completion does not establish the intended effect.";
4718
+ return;
4719
+ }
4720
+ double changed = Math.Round(SigDiffPct(before, after, 8), 2);
4721
+ result["changedPct"] = changed;
4722
+ result["verificationNote"] = "Sampled pixel difference only, not semantic verification. Inspect the expected result before replaying input.";
4723
+ if (changed < 0.15 && !args.ContainsKey("verify"))
4724
+ result["warning"] = "No sampled change in the " + (args.ContainsKey("verificationRegion") ? "requested witness region" : "click neighborhood") +
4725
+ ". This is expected for an already-focused or already-satisfied control; an effect may also be delayed or elsewhere. " +
4726
+ "Check the intended state and focus/receiver before retrying.";
4341
4727
  }
4342
4728
 
4343
4729
  static Dictionary<string, object> Click(Dictionary<string, object> a)
4344
4730
  {
4731
+ Rectangle sampleRegion = VerificationSampleRegion(a, GetI(a, "x", 0), GetI(a, "y", 0), new Rectangle(
4732
+ Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN),
4733
+ Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN)));
4345
4734
  string btn = GetS(a, "button", "left");
4346
4735
  int clicks = Math.Max(1, Math.Min(3, GetI(a, "clicks", 1)));
4347
4736
  int gap = GetI(a, "intervalMs", 60);
@@ -4471,41 +4860,33 @@ namespace DshComputerUse.Worker
4471
4860
  // The window under the click point becomes the sticky target for later typing.
4472
4861
  try { SetSticky(under); } catch { }
4473
4862
  }
4474
- // POST-condition, general: did ANYTHING change where we clicked? A click that changes
4475
- // nothing within a quarter second did not reach its target (dead UI, disabled control,
4476
- // missed pixel, wrong element) — and "no change" is exactly the silent failure that
4477
- // used to be indistinguishable from success. Region sampling is ~1700 pixels, cheaper
4478
- // than a screenshot by three orders of magnitude.
4479
- int vsW = Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN);
4480
- int vsH = Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN);
4863
+ // A cheap pixel witness, not a business-success condition. The caller may
4864
+ // identify an effect elsewhere (e.g. a player bar), independently of the click.
4481
4865
  bool verify = (tx != int.MinValue) && GetB(a, "verify", true);
4482
- int half = 170, margin = 90;
4483
- int bx = Math.Min(Math.Max(tx - half, margin), Math.Max(margin, vsW - margin - 2 * half));
4484
- int by = Math.Min(Math.Max(ty - half, margin), Math.Max(margin, vsH - margin - 2 * half));
4485
- int bw = Math.Min(2 * half, Math.Max(16, vsW - 2 * margin));
4486
- int bh = Math.Min(2 * half, Math.Max(16, vsH - 2 * margin));
4866
+ int bx = sampleRegion.X, by = sampleRegion.Y, bw = sampleRegion.Width, bh = sampleRegion.Height;
4487
4867
  byte[] before = verify ? FrameSignatureRect(bx, by, bw, bh, 8) : null;
4488
4868
 
4489
4869
  // Re-ask at the moment of injection: the pointer has travelled since the precondition
4490
4870
  // above, and the only promise that matters is about the pixel the button is really
4491
4871
  // pressed on. Cheap (one WindowFromPoint) and it closes the window between check and use.
4492
4872
  if (tx != int.MinValue) HostGuardPoint(tx, ty, "click");
4873
+ if (under == IntPtr.Zero) under = InputPointReceiver();
4493
4874
 
4494
4875
  uint down = Native.MOUSEEVENTF_LEFTDOWN, up = Native.MOUSEEVENTF_LEFTUP;
4495
4876
  if (btn == "right") { down = Native.MOUSEEVENTF_RIGHTDOWN; up = Native.MOUSEEVENTF_RIGHTUP; }
4496
4877
  else if (btn == "middle") { down = Native.MOUSEEVENTF_MIDDLEDOWN; up = Native.MOUSEEVENTF_MIDDLEUP; }
4497
- for (int i = 0; i < clicks; i++)
4498
- {
4499
- SendMouse(down, 0, 0, 0); Thread.Sleep(30); SendMouse(up, 0, 0, 0);
4500
- if (i < clicks - 1) Thread.Sleep(gap);
4878
+ using (GuardReceiver(under, InputWindowPid(under), false)) {
4879
+ for (int i = 0; i < clicks; i++) {
4880
+ SendMouse(down, 0, 0, 0); Thread.Sleep(30); SendMouse(up, 0, 0, 0);
4881
+ if (i < clicks - 1) Thread.Sleep(gap);
4882
+ }
4501
4883
  }
4502
4884
  EndActuationForReadback();
4503
- double changedPct = -1;
4885
+ byte[] after = null;
4504
4886
  if (verify && before != null)
4505
4887
  {
4506
4888
  Thread.Sleep(GetI(a, "verifyDelayMs", 240));
4507
- byte[] after = FrameSignatureRect(bx, by, bw, bh, 8);
4508
- if (after != null) changedPct = Math.Round(SigDiffPct(before, after, 8), 2);
4889
+ after = FrameSignatureRect(bx, by, bw, bh, 8);
4509
4890
  }
4510
4891
  POINT fp; Native.GetCursorPos(out fp);
4511
4892
  Dictionary<string, object> clickFocus = FocusReport(a, false);
@@ -4515,18 +4896,58 @@ namespace DshComputerUse.Worker
4515
4896
  if (coveredBy != null) clickOut["coveredBy"] = coveredBy;
4516
4897
  if (under != IntPtr.Zero)
4517
4898
  clickOut["receiver"] = Short(WinTitleOf(under), 40) + " (pid " + PidOf(under) + ")";
4518
- if (changedPct >= 0)
4519
- {
4520
- clickOut["changedPct"] = changedPct;
4521
- if (changedPct < 0.15 && !a.ContainsKey("verify"))
4522
- clickOut["warning"] = "nothing changed near the click point within ~240ms. This is EXPECTED when the " +
4523
- "click lands on an already-focused editor or a control already in the desired state (a caret blink " +
4524
- "alone sits below the threshold — test report 2026-11-04); it can also mean a dead/disabled control " +
4525
- "or a wrong pixel. Read focus/receiver above and only retry if they disagree with the intent.";
4526
- }
4899
+ if (verify) AddPixelVerification(clickOut, a, sampleRegion, before, after);
4527
4900
  return clickOut;
4528
4901
  }
4529
4902
 
4903
+ static Exception DragPrecondition(string code, string detail)
4904
+ {
4905
+ var error = InputInterrupted(code, detail + " No drag button was pressed; earlier focus or pointer movement may have occurred.");
4906
+ error.Data["dragStarted"] = false;
4907
+ return error;
4908
+ }
4909
+
4910
+ static Dictionary<string, object> CheckDragCursor(string expected, Dictionary<string, object> actual)
4911
+ {
4912
+ expected = (expected ?? "").Trim().ToLowerInvariant();
4913
+ string shape = (string)actual["shape"];
4914
+ string classification = (string)actual["classification"];
4915
+ string status = expected.Length == 0 ? "not-requested" : CursorMatches(expected, shape, (bool)actual["visible"])
4916
+ ? "matched" : classification == "standard" ? "mismatched" : "unverifiable";
4917
+ var check = Dict("status", status, "expected", expected.Length == 0 ? null : (object)expected, "observed", shape);
4918
+ if (status == "mismatched" || status == "unverifiable") {
4919
+ string detail = status == "mismatched"
4920
+ ? "Expected " + expected + ", observed standard cursor " + shape + ". Re-observe the intended handle before retrying."
4921
+ : "Cannot verify expected cursor " + expected + ": classification=" + classification + ". An unknown/unavailable cursor is not proof of an incorrect target. Browser col-resize/row-resize may use nonstandard cursors. If fresh independent geometry and hit-testing confirm the handle, the optional expectCursor may be omitted; verify the application result afterward.";
4922
+ // Keep the existing error code for callers; cursorCheck distinguishes a known
4923
+ // shape disagreement from missing evidence. Neither is converted to a match.
4924
+ var error = DragPrecondition("CURSOR_MISMATCH", detail);
4925
+ error.Data["expectedCursor"] = expected;
4926
+ error.Data["actualCursor"] = actual;
4927
+ error.Data["cursorCheck"] = check;
4928
+ throw error;
4929
+ }
4930
+ return check;
4931
+ }
4932
+
4933
+ static IntPtr CheckDragStart(Dictionary<string, object> a, int x, int y)
4934
+ {
4935
+ HostGuardPoint(x, y, "drag");
4936
+ POINT point; point.X = x; point.Y = y;
4937
+ IntPtr under = TopLevel(Native.WindowFromPoint(point));
4938
+ IntPtr want = a.ContainsKey("expectHwnd") ? new IntPtr(GetI(a, "expectHwnd", 0)) : _stickyHwnd;
4939
+ if (under == IntPtr.Zero) throw DragPrecondition("DRAG_START_UNREACHABLE", "No window owns the drag start pixel.");
4940
+ if (want != IntPtr.Zero) {
4941
+ if (!Native.IsWindow(want) || Native.IsIconic(want) || IsOffScreen(want))
4942
+ throw DragPrecondition("DRAG_TARGET_UNAVAILABLE", "The expected drag window is gone, minimized or off-screen.");
4943
+ if (under != want && PidOf(under) != PidOf(want))
4944
+ throw DragPrecondition("DRAG_START_COVERED", "The start pixel belongs to hwnd=" + under.ToInt64() + ", not expected hwnd=" + want.ToInt64() + ". Re-observe the target.");
4945
+ }
4946
+ if (ProcessElevated(PidOf(under)) && !SelfElevated())
4947
+ throw DragPrecondition("DRAG_START_ELEVATED", "The drag receiver is elevated and this worker cannot inject into it.");
4948
+ return under;
4949
+ }
4950
+
4530
4951
  static Dictionary<string, object> Drag(Dictionary<string, object> a)
4531
4952
  {
4532
4953
  int fx = GetI(a, "fromX", int.MinValue), fy = GetI(a, "fromY", int.MinValue);
@@ -4541,9 +4962,22 @@ namespace DshComputerUse.Worker
4541
4962
  int durMs = Math.Max(50, GetI(a, "durationMs", 350));
4542
4963
  AgentActing(a, "drag");
4543
4964
  CheckHumanInput();
4965
+ if (a.ContainsKey("expectHwnd")) {
4966
+ HostGuardNamed(a, "drag");
4967
+ FocusReport(a, true);
4968
+ }
4969
+ CheckDragStart(a, fx, fy);
4544
4970
  AnimatedMove(fx, fy, GetI(a, "moveDurationMs", 250));
4545
4971
  Thread.Sleep(80);
4546
- SendMouse(Native.MOUSEEVENTF_LEFTDOWN, 0, 0, 0);
4972
+ CheckHumanInput();
4973
+ POINT startPoint;
4974
+ if (!Native.GetCursorPos(out startPoint) || Dist(startPoint, fx, fy) > 2)
4975
+ throw DragPrecondition("DRAG_START_MISSED", "The pointer did not reach the measured drag start within two screen pixels.");
4976
+ IntPtr startWindow = CheckDragStart(a, startPoint.X, startPoint.Y);
4977
+ Dictionary<string, object> startCursor = CursorSnapshot();
4978
+ Dictionary<string, object> cursorCheck = CheckDragCursor(GetS(a, "expectCursor", ""), startCursor);
4979
+ using (GuardReceiver(startWindow, InputWindowPid(startWindow), false))
4980
+ SendMouse(Native.MOUSEEVENTF_LEFTDOWN, 0, 0, 0);
4547
4981
  Thread.Sleep(80);
4548
4982
  POINT probe0; Native.GetCursorPos(out probe0);
4549
4983
  if (Dist(probe0, fx, fy) > 24)
@@ -4572,7 +5006,9 @@ namespace DshComputerUse.Worker
4572
5006
  SendMouse(Native.MOUSEEVENTF_LEFTUP, 0, 0, 0);
4573
5007
  POINT p; Native.GetCursorPos(out p);
4574
5008
  CheckHumanInput();
4575
- return Dict("x", p.X, "y", p.Y, "positionCorrections", _positionCorrections);
5009
+ return Dict("x", p.X, "y", p.Y, "positionCorrections", _positionCorrections,
5010
+ "dragStarted", true, "startCursor", startCursor, "cursorCheck", cursorCheck, "startHwnd", startWindow.ToInt64(),
5011
+ "effectVerified", false, "note", "Input sequence completed. Cursor/window checks do not prove that a handle moved; verify the intended UI change.");
4576
5012
  }
4577
5013
 
4578
5014
  static Dictionary<string, object> Scroll(Dictionary<string, object> a)
@@ -4587,10 +5023,14 @@ namespace DshComputerUse.Worker
4587
5023
  else HostGuardCursor("scroll");
4588
5024
  AgentActing(a, "scroll");
4589
5025
  if (a.ContainsKey("x") && a.ContainsKey("y")) Move(a);
4590
- for (int i = 0; i < lines; i++)
4591
- {
4592
- SendMouse(flag, 0, 0, unchecked((uint)(sign * (int)Native.WHEEL_DELTA)));
4593
- Thread.Sleep(25);
5026
+ IntPtr scrollTarget = InputPointReceiver();
5027
+ if (_stickyHwnd != IntPtr.Zero && scrollTarget != _stickyHwnd && InputWindowPid(scrollTarget) != InputWindowPid(_stickyHwnd))
5028
+ throw InputInterrupted("INPUT_TARGET_CHANGED", "Scroll receiver differs from the observed target. No wheel input was sent.");
5029
+ using (GuardReceiver(scrollTarget, InputWindowPid(scrollTarget), false)) {
5030
+ for (int i = 0; i < lines; i++) {
5031
+ SendMouse(flag, 0, 0, unchecked((uint)(sign * (int)Native.WHEEL_DELTA)));
5032
+ Thread.Sleep(25);
5033
+ }
4594
5034
  }
4595
5035
  POINT p; Native.GetCursorPos(out p);
4596
5036
  return PtDict(p.X, p.Y);
@@ -4634,6 +5074,14 @@ namespace DshComputerUse.Worker
4634
5074
 
4635
5075
  static Dictionary<string, object> Indicator(Dictionary<string, object> a)
4636
5076
  {
5077
+ if (a.ContainsKey("hold"))
5078
+ {
5079
+ SceneControl.SetHolding(GetB(a, "hold", false));
5080
+ if (SceneControl.Holding) Glow.Reveal();
5081
+ if (Panic.CycleLit && !Panic.Exited) Glow.Thinking(0);
5082
+ return Dict("holding", Glow.Holding, "cycleActive", Panic.CycleLit && !Panic.Exited,
5083
+ "indicator", Glow.Holding ? "holding" : "free");
5084
+ }
4637
5085
  bool on = GetB(a, "on", true);
4638
5086
  bool soft = GetB(a, "soft", false);
4639
5087
  if (on) { Glow.Enabled = true; Glow.Touch(GetI(a, "ms", 30000)); }
@@ -4717,20 +5165,38 @@ namespace DshComputerUse.Worker
4717
5165
  {
4718
5166
  int x = GetI(a, "x", int.MinValue), y = GetI(a, "y", int.MinValue);
4719
5167
  if (x == int.MinValue || y == int.MinValue) throw new Exception("x,y required");
5168
+ Rectangle witness = VerificationSampleRegion(a, x, y, new Rectangle(
5169
+ Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN),
5170
+ Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN)));
4720
5171
  HostGuardPoint(x, y, "shiftClick");
5172
+ POINT targetPoint; targetPoint.X = x; targetPoint.Y = y;
5173
+ IntPtr targetWindow = TopLevel(Native.WindowFromPoint(targetPoint));
5174
+ if (targetWindow == IntPtr.Zero) throw InputInterrupted("INPUT_TARGET_CHANGED", "Shift-click target is unavailable; no input sent.");
5175
+ uint targetPid = InputWindowPid(targetWindow);
4721
5176
  int clicks = Math.Max(1, Math.Min(3, GetI(a, "clicks", 1)));
4722
5177
  AgentActing(a, "shiftClick");
4723
5178
  CheckHumanInput();
4724
5179
  AnimatedMove(x, y, Math.Max(60, GetI(a, "moveDurationMs", 200)));
4725
5180
  Thread.Sleep(GetI(a, "settleMs", 90));
4726
- SendKey(0x10, false); // VK_SHIFT down
4727
- Thread.Sleep(40);
4728
- ClickTimes(clicks);
4729
- Thread.Sleep(40);
4730
- SendKey(0x10, true); // VK_SHIFT up
5181
+ byte[] before = a.ContainsKey("verificationRegion") ? FrameSignatureRect(witness.X, witness.Y, witness.Width, witness.Height, 8) : null;
5182
+ using (GuardReceiver(targetWindow, targetPid, false)) {
5183
+ SendKey(0x10, false); // VK_SHIFT down
5184
+ Thread.Sleep(40);
5185
+ ClickTimes(clicks);
5186
+ Thread.Sleep(40);
5187
+ SendKey(0x10, true); // VK_SHIFT up
5188
+ }
4731
5189
  Thread.Sleep(GetI(a, "settleMs", 90));
4732
5190
  CheckHumanInput();
4733
- return Dict("x", x, "y", y, "clicks", clicks);
5191
+ var result = Dict("x", x, "y", y, "clicks", clicks);
5192
+ if (a.ContainsKey("verificationRegion")) {
5193
+ EndActuationForReadback();
5194
+ Thread.Sleep(240);
5195
+ byte[] after = FrameSignatureRect(witness.X, witness.Y, witness.Width, witness.Height, 8);
5196
+ CheckHumanInput();
5197
+ AddPixelVerification(result, a, witness, before, after);
5198
+ }
5199
+ return result;
4734
5200
  }
4735
5201
 
4736
5202
  static void ClickTimes(int n)
@@ -4819,7 +5285,11 @@ namespace DshComputerUse.Worker
4819
5285
  IntPtr dib = Native.CreateDIBSection(screen, ref bmi, 0, out bits, IntPtr.Zero, 0);
4820
5286
  if (dib == IntPtr.Zero) { Native.DeleteDC(mem); Native.ReleaseDC(IntPtr.Zero, screen); return null; }
4821
5287
  IntPtr old = Native.SelectObject(mem, dib);
4822
- Native.BitBlt(mem, 0, 0, rw, rh, screen, rx, ry, 0x00CC0020 | 0x40000000);
5288
+ if (!Native.BitBlt(mem, 0, 0, rw, rh, screen, rx, ry, 0x00CC0020 | 0x40000000)) {
5289
+ Native.SelectObject(mem, old);
5290
+ Native.DeleteObject(dib); Native.DeleteDC(mem); Native.ReleaseDC(IntPtr.Zero, screen);
5291
+ return null;
5292
+ }
4823
5293
  int n = ((rw - 1) / step + 1) * ((rh - 1) / step + 1);
4824
5294
  byte[] sig = new byte[n];
4825
5295
  int k = 0;
@@ -5242,6 +5712,35 @@ namespace DshComputerUse.Worker
5242
5712
  }
5243
5713
  throw new Exception("unknown key: " + k);
5244
5714
  }
5715
+ static string NormalizeKeyCombo(string combo)
5716
+ {
5717
+ if (String.IsNullOrWhiteSpace(combo)) throw new Exception("key combo required");
5718
+ var ordered = new List<string>(); var seen = new HashSet<string>(StringComparer.Ordinal);
5719
+ foreach (string raw in combo.Split('+')) {
5720
+ string key = raw.Trim().ToLowerInvariant();
5721
+ if (key.Length == 0) throw new Exception("key combo contains an empty token; use plus for the + key");
5722
+ switch (key) {
5723
+ case "control": key = "ctrl"; break;
5724
+ case "lwin": case "windows": case "meta": key = "win"; break;
5725
+ case "return": key = "enter"; break;
5726
+ case "escape": key = "esc"; break;
5727
+ case "bksp": key = "backspace"; break;
5728
+ case "del": key = "delete"; break;
5729
+ case "ins": key = "insert"; break;
5730
+ case "pageup": key = "pgup"; break;
5731
+ case "pagedown": key = "pgdn"; break;
5732
+ case "capslock": key = "caps"; break;
5733
+ case "printscreen": key = "prtsc"; break;
5734
+ }
5735
+ VkOf(key); // parser and native dispatch accept the same key set
5736
+ if (seen.Add(key)) ordered.Add(key);
5737
+ }
5738
+ var result = new List<string>();
5739
+ foreach (string modifier in new string[] { "ctrl", "alt", "shift", "win", "rwin" })
5740
+ if (seen.Contains(modifier)) { result.Add(modifier); ordered.Remove(modifier); }
5741
+ result.AddRange(ordered);
5742
+ return String.Join("+", result);
5743
+ }
5245
5744
 
5246
5745
  static void SendKey(ushort vk, bool up)
5247
5746
  {
@@ -5281,11 +5780,11 @@ namespace DshComputerUse.Worker
5281
5780
 
5282
5781
  static Dictionary<string, object> Key(Dictionary<string, object> a)
5283
5782
  {
5783
+ if (_receiverCheck != null) _receiverCheck(); // nested paste may not silently repair a stolen receiver
5284
5784
  string combo = GetS(a, "combo", null);
5285
5785
  if (combo == null) throw new Exception("combo required (e.g. 'ctrl+shift+t', 'enter')");
5286
- List<string> keys = new List<string>();
5287
- foreach (string s in combo.Split('+')) { string t = s.Trim().ToLowerInvariant(); if (t.Length > 0) keys.Add(t); }
5288
- if (keys.Count == 0) throw new Exception("empty combo");
5786
+ combo = NormalizeKeyCombo(combo);
5787
+ List<string> keys = new List<string>(combo.Split('+'));
5289
5788
  AgentActing(a, "key");
5290
5789
  // Keyboard input goes to the FOREGROUND window — re-assert the window the agent is
5291
5790
  // working in, and report which window actually received the keys.
@@ -5300,18 +5799,38 @@ namespace DshComputerUse.Worker
5300
5799
  HostGuardForeground("key");
5301
5800
  List<ushort> vks = new List<ushort>();
5302
5801
  foreach (string k in keys) { ushort vk = VkOf(k); if (!vks.Contains(vk)) vks.Add(vk); }
5303
- KeyboardChord.Run(vks, SendKey, ReleaseKeyForCleanup,
5304
- delegate(ushort vk) { return (Native.GetAsyncKeyState(vk) & 0x8000) != 0; },
5305
- delegate { WaitForKeyHold(Math.Max(20, GetI(a, "holdMs", 60))); });
5802
+ bool windowManagement = WindowManagementChord.IsExact(combo);
5803
+ using (GuardReceiver(new IntPtr(Convert.ToInt64(focus["hwnd"])), Convert.ToUInt32(focus["pid"]), true))
5804
+ {
5805
+ if (windowManagement)
5806
+ WindowManagementChord.Run(vks, SendKey, ReleaseKeyForCleanup,
5807
+ delegate(ushort vk) { return (Native.GetAsyncKeyState(vk) & 0x8000) != 0; },
5808
+ delegate { WaitForKeyHold(Math.Max(20, GetI(a, "holdMs", 60))); },
5809
+ delegate { WaitForKeyHold(0); }, InvalidateKeyboardTarget);
5810
+ else
5811
+ KeyboardChord.Run(vks, SendKey, ReleaseKeyForCleanup,
5812
+ delegate(ushort vk) { return (Native.GetAsyncKeyState(vk) & 0x8000) != 0; },
5813
+ delegate { WaitForKeyHold(Math.Max(20, GetI(a, "holdMs", 60))); });
5814
+ }
5306
5815
  EndActuationForReadback();
5307
5816
  Thread.Sleep(30);
5308
- Dictionary<string, object> keyOut = Dict("combo", combo, "focus", focus);
5817
+ Dictionary<string, object> keyOut = AddFocusReadback(Dict("combo", combo), a, focus);
5818
+ if (windowManagement)
5819
+ {
5820
+ var after = (Dictionary<string, object>)keyOut["focusAfter"];
5821
+ keyOut["receiverChanged"] = Convert.ToInt64(focus["hwnd"]) != Convert.ToInt64(after["hwnd"]) ||
5822
+ Convert.ToUInt32(focus["pid"]) != Convert.ToUInt32(after["pid"]);
5823
+ keyOut["reobserveRequired"] = true;
5824
+ keyOut["effectVerified"] = false;
5825
+ keyOut["hint"] = KeyboardReobserveHint;
5826
+ }
5309
5827
  if (_lastRefocus != null) keyOut["refocused"] = _lastRefocus;
5310
5828
  return keyOut;
5311
5829
  }
5312
5830
 
5313
5831
  static Dictionary<string, object> Type(Dictionary<string, object> a)
5314
5832
  {
5833
+ if (_receiverCheck != null) _receiverCheck();
5315
5834
  string text = GetS(a, "text", null);
5316
5835
  if (text == null) throw new Exception("text required");
5317
5836
  AgentActing(a, "type");
@@ -5320,6 +5839,7 @@ namespace DshComputerUse.Worker
5320
5839
  HostGuardNamed(a, "type");
5321
5840
  Dictionary<string, object> focus = FocusReport(a, true);
5322
5841
  HostGuardForeground("type"); // same rule as `key`: the foreground window is the victim
5842
+ using (GuardReceiver(new IntPtr(Convert.ToInt64(focus["hwnd"])), Convert.ToUInt32(focus["pid"]), true)) {
5323
5843
  // ...and whether whatever owns the focus can hold text at all. Typing into a control
5324
5844
  // with no editable pattern is silently discarded, or interpreted as shortcuts.
5325
5845
  string typeWarn = null;
@@ -5372,7 +5892,7 @@ namespace DshComputerUse.Worker
5372
5892
  downgraded["clipboardWarning"] = "The previous clipboard could not be preserved (private or oversized formats) and was overwritten by this paste. Restore it manually if it mattered.";
5373
5893
  if (typeWarn != null) downgraded["warning"] = typeWarn;
5374
5894
  if (_lastRefocus != null) downgraded["refocused"] = _lastRefocus;
5375
- return downgraded;
5895
+ return AddFocusReadback(downgraded, a, focus);
5376
5896
  }
5377
5897
  Exception inputError = null;
5378
5898
  try
@@ -5406,7 +5926,7 @@ namespace DshComputerUse.Worker
5406
5926
  pr["clipboardWarning"] = "Clipboard changed or could not be restored. Verify the target value before repeating input.";
5407
5927
  if (typeWarn != null) pr["warning"] = typeWarn;
5408
5928
  if (_lastRefocus != null) pr["refocused"] = _lastRefocus;
5409
- return pr;
5929
+ return AddFocusReadback(pr, a, focus);
5410
5930
  }
5411
5931
  int charDelay = GetI(a, "charDelayMs", 8);
5412
5932
  foreach (char ch in text)
@@ -5430,10 +5950,17 @@ namespace DshComputerUse.Worker
5430
5950
  SendInputChecked(new INPUT[] { down, up });
5431
5951
  Thread.Sleep(charDelay);
5432
5952
  }
5433
- Dictionary<string, object> tr = Dict("mode", "unicode", "chars", text.Length, "focus", focus);
5953
+ Dictionary<string, object> tr = Dict("mode", "unicode", "chars", text.Length);
5434
5954
  if (typeWarn != null) tr["warning"] = typeWarn;
5435
5955
  if (_lastRefocus != null) tr["refocused"] = _lastRefocus;
5436
- return tr;
5956
+ return AddFocusReadback(tr, a, focus);
5957
+ }
5958
+ }
5959
+ static Dictionary<string, object> AddFocusReadback(Dictionary<string, object> result, Dictionary<string, object> args, Dictionary<string, object> before)
5960
+ {
5961
+ var after = FocusReport(args, false);
5962
+ result["focusBefore"] = before; result["focusAfter"] = after; result["focus"] = after;
5963
+ return result;
5437
5964
  }
5438
5965
 
5439
5966
  // ---------- clipboard ----------
@@ -5492,7 +6019,34 @@ namespace DshComputerUse.Worker
5492
6019
  list.Add(WinDict(h));
5493
6020
  return true;
5494
6021
  }, IntPtr.Zero);
5495
- return Dict("windows", list);
6022
+ return Dict("windows", list, "virtualScreen", VirtualScreenDict(),
6023
+ "cooperation", Panic.CooperationState(), "indicator", IndicatorSnapshot());
6024
+ }
6025
+
6026
+ static string IndicatorStateName(int want)
6027
+ {
6028
+ switch (want) {
6029
+ case 0: return "idle";
6030
+ case 1: return "acting";
6031
+ case 2: return "capture";
6032
+ case 3: return "thinking";
6033
+ case 4: return "paused";
6034
+ case 5: return "asking";
6035
+ case 6: return "human-handoff";
6036
+ case 7: return "preparing";
6037
+ case 8: return "reserved";
6038
+ default: return "unknown";
6039
+ }
6040
+ }
6041
+
6042
+ static Dictionary<string, object> IndicatorSnapshot()
6043
+ {
6044
+ // Want/title describe the last rendered frame, not a prediction that
6045
+ // an input will run. The cooperation record supplies the handoff cause.
6046
+ int want = Glow.Want;
6047
+ return Dict("state", IndicatorStateName(want), "want", want,
6048
+ "title", Glow.ShownTitle ?? "", "injecting", Glow.Injecting,
6049
+ "acting", Glow.Acting, "preparing", Glow.Preparing, "holding", Glow.Holding);
5496
6050
  }
5497
6051
 
5498
6052
  static IntPtr FindHwnd(Dictionary<string, object> a)
@@ -5557,6 +6111,7 @@ namespace DshComputerUse.Worker
5557
6111
  _kickoffFocus.Confirm(h.ToInt64(), confirmedPid);
5558
6112
  }
5559
6113
  CheckHumanInput();
6114
+ if (how != "failed") _keyboardReobserveRequired = false;
5560
6115
  return Dict("activated", how != "failed", "activatedVia", how,
5561
6116
  "hwnd", h.ToInt64(), "pid", confirmedPid == 0 ? null : (object)confirmedPid,
5562
6117
  "hint", how == "failed"
@@ -5578,6 +6133,22 @@ namespace DshComputerUse.Worker
5578
6133
  // 2. if that window belongs to an ELEVATED process while we are not, UIPI silently
5579
6134
  // drops every event — say so loudly instead of reporting a successful no-op.
5580
6135
  static IntPtr _stickyHwnd = IntPtr.Zero;
6136
+ static volatile bool _keyboardReobserveRequired;
6137
+ const string KeyboardReobserveHint = "A window-management chord was dispatched; this is not proof that a switch succeeded. " +
6138
+ "Inspect computer_state {windows:true}, then use computer_window {hwnd:...,op:'activate'} and require activated:true before further keyboard input.";
6139
+ static void InvalidateKeyboardTarget()
6140
+ {
6141
+ _stickyHwnd = IntPtr.Zero;
6142
+ ResetKickoffFocus();
6143
+ _keyboardReobserveRequired = true;
6144
+ }
6145
+ static void CheckKeyboardReobserve()
6146
+ {
6147
+ if (!_keyboardReobserveRequired) return;
6148
+ var error = NotDispatched("INPUT_REOBSERVE_REQUIRED", "INPUT_REOBSERVE_REQUIRED: " + KeyboardReobserveHint);
6149
+ error.Data["reobserveRequired"] = true;
6150
+ throw error;
6151
+ }
5581
6152
  static readonly KickoffFocus _kickoffFocus = new KickoffFocus();
5582
6153
  public static void ResetKickoffFocus() { _kickoffFocus.Reset(); }
5583
6154
  public static long RecoveryTarget(out uint pid)
@@ -6170,6 +6741,45 @@ namespace DshComputerUse.Worker
6170
6741
  catch { return false; }
6171
6742
  }
6172
6743
 
6744
+ static int ProcessIntegrityLevel(uint pid)
6745
+ {
6746
+ if (pid == 0) return -1;
6747
+ IntPtr proc = IntPtr.Zero, token = IntPtr.Zero, buffer = IntPtr.Zero;
6748
+ try {
6749
+ proc = Native.OpenProcess(Native.PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
6750
+ if (proc == IntPtr.Zero || !Native.OpenProcessToken(proc, Native.TOKEN_QUERY, out token)) return -1;
6751
+ uint needed;
6752
+ Native.GetTokenInformationBuffer(token, 25, IntPtr.Zero, 0, out needed); // TokenIntegrityLevel
6753
+ if (needed < IntPtr.Size || needed > 4096) return -1;
6754
+ buffer = Marshal.AllocHGlobal((int)needed);
6755
+ if (!Native.GetTokenInformationBuffer(token, 25, buffer, needed, out needed)) return -1;
6756
+ IntPtr sid = Marshal.ReadIntPtr(buffer);
6757
+ if (sid == IntPtr.Zero) return -1;
6758
+ IntPtr countPointer = Native.GetSidSubAuthorityCount(sid);
6759
+ if (countPointer == IntPtr.Zero) return -1;
6760
+ byte count = Marshal.ReadByte(countPointer);
6761
+ if (count == 0 || count > 15) return -1;
6762
+ IntPtr rid = Native.GetSidSubAuthority(sid, (uint)(count - 1));
6763
+ return rid == IntPtr.Zero ? -1 : Marshal.ReadInt32(rid);
6764
+ } catch { return -1; }
6765
+ finally {
6766
+ if (buffer != IntPtr.Zero) Marshal.FreeHGlobal(buffer);
6767
+ if (token != IntPtr.Zero) Native.CloseHandle(token);
6768
+ if (proc != IntPtr.Zero) Native.CloseHandle(proc);
6769
+ }
6770
+ }
6771
+ static bool AsyncKeyIntegrityAllows(int self, int foreground)
6772
+ { return self >= 0 && foreground >= 0 && foreground <= self; }
6773
+ internal static bool CanObserveAsyncKeys(out IntPtr foreground)
6774
+ {
6775
+ foreground = Native.GetForegroundWindow();
6776
+ if (foreground == IntPtr.Zero) return false;
6777
+ uint pid = PidOf(foreground);
6778
+ // Unknown is NOT unelevated. GetAsyncKeyState reports zero for UIPI
6779
+ // failures too, so only known same/lower integrity is usable evidence.
6780
+ return AsyncKeyIntegrityAllows(ProcessIntegrityLevel((uint)Process.GetCurrentProcess().Id), ProcessIntegrityLevel(pid)) &&
6781
+ Native.GetForegroundWindow() == foreground && PidOf(foreground) == pid;
6782
+ }
6173
6783
  static bool ProcessElevated(uint pid)
6174
6784
  {
6175
6785
  if (pid == 0) return false;
@@ -6309,6 +6919,7 @@ namespace DshComputerUse.Worker
6309
6919
  /// reach because of integrity levels.
6310
6920
  static Dictionary<string, object> FocusReport(Dictionary<string, object> a, bool refocus)
6311
6921
  {
6922
+ if (refocus) CheckKeyboardReobserve(); // explicit old expectHwnd is not fresh authorization
6312
6923
  IntPtr want = IntPtr.Zero;
6313
6924
  if (a != null && a.ContainsKey("expectHwnd")) want = new IntPtr(GetI(a, "expectHwnd", 0));
6314
6925
  else if (a != null && a.ContainsKey("hwnd")) want = new IntPtr(GetI(a, "hwnd", 0));
@@ -6386,6 +6997,43 @@ namespace DshComputerUse.Worker
6386
6997
  }
6387
6998
 
6388
6999
  // ---------- UIA ----------
7000
+ static void AddUiGeometry(Dictionary<string, object> d, System.Windows.Rect? bounds,
7001
+ bool? offscreen, Rectangle virtualScreen)
7002
+ {
7003
+ bool valid = false;
7004
+ if (bounds.HasValue) {
7005
+ System.Windows.Rect r = bounds.Value;
7006
+ // Empty UIA rectangles contain infinities. Casting these to int
7007
+ // manufactured INT32_MIN coordinates that looked like real layout.
7008
+ valid = !r.IsEmpty && r.Width > 0 && r.Height > 0 &&
7009
+ !Double.IsNaN(r.X) && !Double.IsNaN(r.Y) &&
7010
+ !Double.IsNaN(r.Width) && !Double.IsNaN(r.Height) &&
7011
+ r.X > Int32.MinValue && r.Y > Int32.MinValue &&
7012
+ r.Right < Int32.MaxValue && r.Bottom < Int32.MaxValue &&
7013
+ r.Width < Int32.MaxValue && r.Height < Int32.MaxValue;
7014
+ if (valid) {
7015
+ long left = (long)Math.Floor(r.Left), top = (long)Math.Floor(r.Top);
7016
+ long right = (long)Math.Ceiling(r.Right), bottom = (long)Math.Ceiling(r.Bottom);
7017
+ valid = left > Int32.MinValue && top > Int32.MinValue &&
7018
+ right - left <= Int32.MaxValue && bottom - top <= Int32.MaxValue;
7019
+ if (valid) {
7020
+ d["rect"] = Dict("x", (int)left, "y", (int)top, "width", (int)(right - left), "height", (int)(bottom - top));
7021
+ d["intersectsVirtualScreen"] = r.Right > virtualScreen.Left && r.Left < virtualScreen.Right &&
7022
+ r.Bottom > virtualScreen.Top && r.Top < virtualScreen.Bottom;
7023
+ }
7024
+ }
7025
+ }
7026
+ d["rectValid"] = valid;
7027
+ d["isOffscreen"] = offscreen.HasValue ? (object)offscreen.Value : null;
7028
+ if (!valid) {
7029
+ d["rect"] = null;
7030
+ d["intersectsVirtualScreen"] = null;
7031
+ d["rectInvalidReason"] = bounds.HasValue ? "empty-or-invalid-provider-bounds" : "provider-bounds-unavailable";
7032
+ }
7033
+ // Neither a valid rectangle nor IsOffscreen=false proves that this
7034
+ // element owns a pixel; use the point probe for occlusion/hit testing.
7035
+ }
7036
+
6389
7037
  static Dictionary<string, object> ElemDict(AutomationElement e)
6390
7038
  {
6391
7039
  Dictionary<string, object> d = new Dictionary<string, object>();
@@ -6395,10 +7043,15 @@ namespace DshComputerUse.Worker
6395
7043
  string ctype = "";
6396
7044
  try { if (e.Current.ControlType != null) ctype = e.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""); }
6397
7045
  catch { }
6398
- System.Windows.Rect r = e.Current.BoundingRectangle;
6399
7046
  d["name"] = name;
6400
7047
  d["role"] = ctype;
6401
- d["rect"] = Dict("x", (int)r.X, "y", (int)r.Y, "width", (int)r.Width, "height", (int)r.Height);
7048
+ System.Windows.Rect? bounds = null;
7049
+ bool? offscreen = null;
7050
+ try { bounds = e.Current.BoundingRectangle; } catch { }
7051
+ try { offscreen = e.Current.IsOffscreen; } catch { }
7052
+ AddUiGeometry(d, bounds, offscreen, new Rectangle(
7053
+ Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN),
7054
+ Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN)));
6402
7055
  try { d["automationId"] = e.Current.AutomationId; } catch { }
6403
7056
  try { d["className"] = e.Current.ClassName; } catch { }
6404
7057
  try { d["runtimeId"] = String.Join(".", e.GetRuntimeId()); d["pid"] = e.Current.ProcessId; } catch { }
@@ -6424,61 +7077,205 @@ namespace DshComputerUse.Worker
6424
7077
  return d;
6425
7078
  }
6426
7079
 
7080
+ // >>>UIA-QUERY-BUDGET<<<
7081
+ // Cooperative limits between provider calls. A synchronous UIA/COM call
7082
+ // already in progress is NOT interruptible by this traversal budget.
7083
+ internal sealed class UiBudgetStop : Exception { }
7084
+ internal sealed class UiReadBudget
7085
+ {
7086
+ readonly Func<long> clock;
7087
+ readonly long started;
7088
+ readonly List<string> reasons = new List<string>();
7089
+ public readonly int BudgetMs;
7090
+ public int Calls, Scanned;
7091
+ public long MaxCallMs;
7092
+ public UiReadBudget(int budgetMs, Func<long> now)
7093
+ { BudgetMs = Math.Max(100, Math.Min(5000, budgetMs)); clock = now; started = now(); }
7094
+ public long ElapsedMs { get { return Math.Max(0, clock() - started); } }
7095
+ public bool Truncated { get { return reasons.Count > 0; } }
7096
+ public void Mark(string reason) { if (!reasons.Contains(reason)) reasons.Add(reason); }
7097
+ public bool Continue()
7098
+ { if (ElapsedMs < BudgetMs) return true; Mark("deadline"); return false; }
7099
+ public T Read<T>(Func<T> read)
7100
+ {
7101
+ if (!Continue()) throw new UiBudgetStop();
7102
+ long before = clock(); Calls++;
7103
+ try {
7104
+ T value = read();
7105
+ if (!Continue()) throw new UiBudgetStop();
7106
+ return value;
7107
+ } finally { MaxCallMs = Math.Max(MaxCallMs, Math.Max(0, clock() - before)); }
7108
+ }
7109
+ public Dictionary<string, object> Report(int maxNodes, int depth)
7110
+ {
7111
+ return Dict("budgetMs", BudgetMs, "elapsedMs", ElapsedMs, "maxNodes", maxNodes, "depth", depth,
7112
+ "scanned", Scanned, "providerCalls", Calls, "maxCallMs", MaxCallMs,
7113
+ "truncatedReasons", reasons.ToArray(), "singleCallInterruptible", false,
7114
+ "note", "Soft traversal budget checked between provider calls; one synchronous UIA/COM call can exceed it. A partial result cannot prove absence or uniqueness.");
7115
+ }
7116
+ }
7117
+ static long UiClockMs()
7118
+ { return (long)(Stopwatch.GetTimestamp() * (1000.0 / Stopwatch.Frequency)); }
7119
+ static UiReadBudget NewUiBudget(Dictionary<string, object> a)
7120
+ { return new UiReadBudget(GetI(a, "budgetMs", 1200), UiClockMs); }
7121
+
7122
+ internal static void ValidateUiQueryScope(bool scoped, bool filtered, bool anchored, int depth, int maxNodes)
7123
+ {
7124
+ if (filtered && !scoped)
7125
+ throw new Exception("UIA_SCOPE_REQUIRED: filtered UIA queries require hwnd or an unambiguous window title; use computer_state to identify the window, or computer_uia at for a point probe.");
7126
+ if (!anchored && (depth > 6 || maxNodes > 300))
7127
+ throw new Exception("UIA_ANCHOR_REQUIRED: broad UIA traversal requires an observed anchor point in the requested window; use computer_uia at first, then anchor with depth/maxNodes limits. An exact id does not avoid traversal.");
7128
+ }
7129
+ internal static bool UiAnchorOwned(long requestedTop, uint requestedPid, long actualTop, uint actualPid)
7130
+ { return requestedTop != 0 && requestedPid != 0 && requestedTop == actualTop && requestedPid == actualPid; }
7131
+ static AutomationElement UiQueryRoot(Dictionary<string, object> a, IntPtr root, UiReadBudget budget)
7132
+ {
7133
+ string anchor = GetS(a, "anchor", null);
7134
+ if (String.IsNullOrWhiteSpace(anchor)) return budget.Read(delegate { return AutomationElement.FromHandle(root); });
7135
+ string[] xy = anchor.Split(','); int x, y;
7136
+ if (xy.Length != 2 || !Int32.TryParse(xy[0].Trim(), out x) || !Int32.TryParse(xy[1].Trim(), out y))
7137
+ throw new Exception("UIA_ANCHOR_INVALID: anchor must be physical screen x,y");
7138
+ POINT point; point.X = x; point.Y = y;
7139
+ uint pid = PidOf(root); IntPtr expectedTop = TopLevel(root), hit = Native.WindowFromPoint(point);
7140
+ if (!Native.IsWindow(root) || !UiAnchorOwned(expectedTop.ToInt64(), pid, TopLevel(hit).ToInt64(), PidOf(hit)))
7141
+ throw new Exception("UIA_ANCHOR_MISMATCH: anchor is not currently owned by the requested window and process; no provider query was started");
7142
+ AutomationElement element = budget.Read(delegate { return AutomationElement.FromPoint(new System.Windows.Point(x, y)); });
7143
+ IntPtr afterHit = Native.WindowFromPoint(point);
7144
+ if (element == null || budget.Read(delegate { return element.Current.ProcessId; }) != (int)pid ||
7145
+ PidOf(root) != pid || !UiAnchorOwned(expectedTop.ToInt64(), pid, TopLevel(afterHit).ToInt64(), PidOf(afterHit)))
7146
+ throw new Exception("UIA_ANCHOR_MISMATCH: provider hit or window changed during the point probe; repeat the observation");
7147
+ return element;
7148
+ }
7149
+
7150
+ // Generic only so deterministic fake trees exercise the identical walk.
7151
+ // Child/sibling navigation is incremental: no FindAll collection can be
7152
+ // built before the client-side node/depth/deadline checks run.
7153
+ internal static List<T> UiWalkBounded<T>(T start, int depth, int maxNodes, int limit, UiReadBudget budget,
7154
+ Func<T, bool> matches, Func<T, T> firstChild, Func<T, T> nextSibling) where T : class
7155
+ {
7156
+ var found = new List<T>(); var queue = new Queue<KeyValuePair<T, int>>();
7157
+ if (start == null) return found;
7158
+ // A queue entry is a parent whose direct children form the next BFS
7159
+ // segment. Visit each child before asking for its next sibling: a
7160
+ // wide provider can no longer consume the budget before any match
7161
+ // in that sibling list has been inspected/materialized.
7162
+ queue.Enqueue(new KeyValuePair<T, int>(start, -1));
7163
+ while (queue.Count > 0 && budget.Scanned < maxNodes && budget.Continue())
7164
+ {
7165
+ var segment = queue.Dequeue(); int level = segment.Value + 1;
7166
+ try
7167
+ {
7168
+ T node = segment.Value < 0 ? segment.Key : budget.Read(delegate { return firstChild(segment.Key); });
7169
+ while (node != null && budget.Continue())
7170
+ {
7171
+ if (budget.Scanned >= maxNodes) { budget.Mark("max-nodes"); return found; }
7172
+ budget.Scanned++;
7173
+ try {
7174
+ if (matches(node)) {
7175
+ found.Add(node);
7176
+ if (found.Count >= limit) { budget.Mark("match-limit"); return found; }
7177
+ }
7178
+ if (!budget.Continue()) return found;
7179
+ if (level < depth) queue.Enqueue(new KeyValuePair<T, int>(node, level));
7180
+ else if (budget.Read(delegate { return firstChild(node); }) != null) budget.Mark("depth");
7181
+ } catch (UiBudgetStop) { return found; }
7182
+ catch { budget.Mark("provider-error"); }
7183
+ if (segment.Value < 0) break;
7184
+ if (budget.Scanned >= maxNodes) { budget.Mark("max-nodes"); return found; }
7185
+ T previous = node;
7186
+ node = budget.Read(delegate { return nextSibling(previous); });
7187
+ }
7188
+ }
7189
+ catch (UiBudgetStop) { break; }
7190
+ catch { budget.Mark("provider-error"); }
7191
+ }
7192
+ if (queue.Count > 0) budget.Mark(budget.Scanned >= maxNodes ? "max-nodes" : "pending-nodes");
7193
+ return found;
7194
+ }
7195
+ // <<<UIA-QUERY-BUDGET<<<
7196
+
7197
+ static Dictionary<string, object> ElemDictBudgeted(AutomationElement e, UiReadBudget budget)
7198
+ {
7199
+ var d = new Dictionary<string, object>();
7200
+ string name = "", role = ""; System.Windows.Rect? bounds = null; bool? offscreen = null;
7201
+ try { name = budget.Read(delegate { return e.Current.Name; }) ?? ""; } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7202
+ try { ControlType ct = budget.Read(delegate { return e.Current.ControlType; }); if (ct != null) role = ct.ProgrammaticName.Replace("ControlType.", ""); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7203
+ d["name"] = name.Length > 160 ? name.Substring(0, 160) : name; d["role"] = role;
7204
+ try { bounds = budget.Read(delegate { return e.Current.BoundingRectangle; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7205
+ try { offscreen = budget.Read(delegate { return e.Current.IsOffscreen; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7206
+ AddUiGeometry(d, bounds, offscreen, new Rectangle(Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN)));
7207
+ try { d["automationId"] = budget.Read(delegate { return e.Current.AutomationId; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7208
+ try { d["className"] = budget.Read(delegate { return e.Current.ClassName; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7209
+ try { d["runtimeId"] = String.Join(".", budget.Read(delegate { return e.GetRuntimeId(); })); d["pid"] = budget.Read(delegate { return e.Current.ProcessId; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7210
+ try { d["isPassword"] = budget.Read(delegate { return e.Current.IsPassword; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7211
+ try {
7212
+ var patterns = new List<string>();
7213
+ foreach (AutomationPattern pattern in budget.Read(delegate { return e.GetSupportedPatterns(); })) {
7214
+ string text = pattern.ProgrammaticName.Replace("PatternIdentifiers", "").Replace("Pattern", "").TrimEnd('.', ' ');
7215
+ if (text.Length > 0) patterns.Add(text);
7216
+ }
7217
+ d["patterns"] = patterns;
7218
+ } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7219
+ try {
7220
+ object value = budget.Read(delegate { object p; return e.TryGetCurrentPattern(ValuePattern.Pattern, out p) ? p : null; });
7221
+ if (value != null) d["value"] = budget.Read(delegate { return ((ValuePattern)value).Current.Value; });
7222
+ } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
7223
+ return d;
7224
+ }
7225
+
6427
7226
  static Dictionary<string, object> Uia(Dictionary<string, object> a)
6428
7227
  {
6429
7228
  IntPtr root = FindHwnd(a);
6430
- AutomationElement rootEl = root != IntPtr.Zero ? AutomationElement.FromHandle(root) : AutomationElement.RootElement;
6431
7229
  int maxDepth = Math.Max(1, Math.Min(24, GetI(a, "depth", 6)));
6432
- int maxNodes = Math.Max(10, Math.Min(1200, GetI(a, "maxNodes", 300)));
6433
- if (GetB(a, "query", false))
7230
+ int maxNodes = Math.Max(10, Math.Min(1200, GetI(a, "maxNodes", 120)));
7231
+ bool query = GetB(a, "query", false), anchored = !String.IsNullOrWhiteSpace(GetS(a, "anchor", null));
7232
+ ValidateUiQueryScope(root != IntPtr.Zero, query, anchored, maxDepth, maxNodes);
7233
+ if (root == IntPtr.Zero) root = Native.GetForegroundWindow();
7234
+ if (root == IntPtr.Zero) throw new Exception("UIA_SCOPE_REQUIRED: no foreground window; specify hwnd");
7235
+ UiReadBudget budget = NewUiBudget(a);
7236
+ AutomationElement rootEl;
7237
+ try { rootEl = UiQueryRoot(a, root, budget); }
7238
+ catch (UiBudgetStop) { return Dict("flat", new List<object>(), "count", 0, "scanned", 0, "truncated", true, "budget", budget.Report(maxNodes, maxDepth), "hwnd", root.ToInt64(), "pid", PidOf(root)); }
7239
+ if (query)
6434
7240
  {
6435
- int scanned; bool truncated;
6436
- List<AutomationElement> matches = FindUiMatches(rootEl, GetS(a, "nameContains", null),
6437
- GetS(a, "automationId", null), GetS(a, "role", null), maxDepth, maxNodes,
6438
- Math.Max(1, Math.Min(80, GetI(a, "limit", 80))), out scanned, out truncated);
6439
7241
  List<object> selected = new List<object>();
6440
7242
  string witness = root != IntPtr.Zero && GetB(a, "targets", false) ? UiWindowWitness(root) : null;
6441
7243
  var ancestorCache = new Dictionary<string, List<object>>();
6442
- foreach (AutomationElement match in matches)
6443
- {
6444
- try {
6445
- var item = ElemDict(match);
7244
+ AutomationElement evidenceRoot = rootEl;
7245
+ if (witness != null && anchored) {
7246
+ try { evidenceRoot = budget.Read(delegate { return AutomationElement.FromHandle(root); }); }
7247
+ catch (UiBudgetStop) { evidenceRoot = null; }
7248
+ }
7249
+ string nameFilter = GetS(a, "nameContains", null), idFilter = GetS(a, "automationId", null), roleFilter = GetS(a, "role", null);
7250
+ CacheRequest selectorCache = UiSelectorCache();
7251
+ UiWalkBounded(rootEl, maxDepth, maxNodes, Math.Max(1, Math.Min(80, GetI(a, "limit", 80))), budget,
7252
+ delegate(AutomationElement match) {
7253
+ if (!ElementMatchesBudgeted(match, nameFilter, idFilter, roleFilter, budget)) return false;
7254
+ var item = ElemDictBudgeted(match, budget);
6446
7255
  if (witness != null) {
6447
- try { item["target"] = RememberUiTarget(match, root, witness, rootEl, ancestorCache, item); }
7256
+ try {
7257
+ if (evidenceRoot == null) throw new UiBudgetStop();
7258
+ item["target"] = RememberUiTarget(match, root, witness, evidenceRoot, ancestorCache, item, budget);
7259
+ }
7260
+ catch (UiBudgetStop) { item["targetUnavailable"] = true; }
6448
7261
  catch { item["targetUnavailable"] = true; }
6449
7262
  }
6450
7263
  selected.Add(item);
6451
- } catch { truncated = true; }
6452
- }
7264
+ return true;
7265
+ },
7266
+ delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetFirstChild(element, selectorCache); },
7267
+ delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetNextSibling(element, selectorCache); });
6453
7268
  if (witness != null && witness != UiWindowWitness(root)) throw new Exception("TARGET_STALE: window changed during observation; retry the query");
6454
- return Dict("flat", selected, "count", selected.Count, "scanned", scanned, "truncated", truncated,
6455
- "hwnd", root.ToInt64(), "pid", root == IntPtr.Zero ? 0 : PidOf(root));
7269
+ return Dict("flat", selected, "count", selected.Count, "scanned", budget.Scanned, "truncated", budget.Truncated,
7270
+ "hwnd", root.ToInt64(), "pid", PidOf(root), "budget", budget.Report(maxNodes, maxDepth));
6456
7271
  }
6457
7272
  List<object> flat = new List<object>();
6458
- bool treeTruncated = false;
6459
- // breadth-first so the first `maxNodes` elements cover the shallow UI evenly
6460
- Queue<KeyValuePair<AutomationElement, int>> q = new Queue<KeyValuePair<AutomationElement, int>>();
6461
- q.Enqueue(new KeyValuePair<AutomationElement, int>(rootEl, 0));
6462
- while (q.Count > 0 && flat.Count < maxNodes)
6463
- {
6464
- KeyValuePair<AutomationElement, int> kv = q.Dequeue();
6465
- try { flat.Add(ElemDict(kv.Key)); }
6466
- catch { treeTruncated = true; continue; }
6467
- if (kv.Value < maxDepth)
6468
- {
6469
- try
6470
- {
6471
- foreach (AutomationElement c in kv.Key.FindAll(TreeScope.Children, Condition.TrueCondition))
6472
- {
6473
- if (flat.Count + q.Count >= maxNodes) { treeTruncated = true; break; }
6474
- q.Enqueue(new KeyValuePair<AutomationElement, int>(c, kv.Value + 1));
6475
- }
6476
- }
6477
- catch { treeTruncated = true; }
6478
- }
6479
- else try { if (kv.Key.FindFirst(TreeScope.Children, Condition.TrueCondition) != null) treeTruncated = true; } catch { treeTruncated = true; }
6480
- }
6481
- return Dict("root", ElemDict(rootEl), "flat", flat, "count", flat.Count, "truncated", treeTruncated || q.Count > 0);
7273
+ UiWalkBounded(rootEl, maxDepth, maxNodes, maxNodes + 1, budget,
7274
+ delegate(AutomationElement element) { flat.Add(ElemDictBudgeted(element, budget)); return false; },
7275
+ delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetFirstChild(element); },
7276
+ delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetNextSibling(element); });
7277
+ return Dict("root", flat.Count > 0 ? flat[0] : null, "flat", flat, "count", flat.Count,
7278
+ "truncated", budget.Truncated, "budget", budget.Report(maxNodes, maxDepth), "hwnd", root.ToInt64(), "pid", PidOf(root));
6482
7279
  }
6483
7280
 
6484
7281
  // Observed native identities are private to this worker, bounded, and never reconstructed
@@ -6502,31 +7299,35 @@ namespace DshComputerUse.Worker
6502
7299
  return Json.Serialize(new object[] { hwnd.ToInt64(), pid, started, WinTitleOf(hwnd), ClassOf(hwnd), rect.Left, rect.Top, rect.Right, rect.Bottom });
6503
7300
  }
6504
7301
  static string UiText(string text) { return (text ?? "").Normalize(NormalizationForm.FormKC).Trim(); }
6505
- static object UiSemanticPart(AutomationElement element)
7302
+ static T UiReadMaybe<T>(UiReadBudget budget, Func<T> read)
7303
+ { return budget == null ? read() : budget.Read(read); }
7304
+ static object UiSemanticPart(AutomationElement element, UiReadBudget budget = null)
6506
7305
  {
6507
- return new object[] { element.Current.ControlType.ProgrammaticName, UiText(element.Current.Name), element.Current.AutomationId, element.Current.ClassName };
7306
+ return new object[] { UiReadMaybe(budget, delegate { return element.Current.ControlType; }).ProgrammaticName,
7307
+ UiText(UiReadMaybe(budget, delegate { return element.Current.Name; })),
7308
+ UiReadMaybe(budget, delegate { return element.Current.AutomationId; }), UiReadMaybe(budget, delegate { return element.Current.ClassName; }) };
6508
7309
  }
6509
- static string UiTargetEvidence(AutomationElement element, IntPtr hwnd)
7310
+ static string UiTargetEvidence(AutomationElement element, IntPtr hwnd, UiReadBudget budget = null)
6510
7311
  {
6511
- return UiTargetEvidence(element, AutomationElement.FromHandle(hwnd), new Dictionary<string, List<object>>(), null);
7312
+ return UiTargetEvidence(element, UiReadMaybe(budget, delegate { return AutomationElement.FromHandle(hwnd); }), new Dictionary<string, List<object>>(), null, budget);
6512
7313
  }
6513
- static string UiTargetEvidence(AutomationElement element, AutomationElement root, Dictionary<string, List<object>> cache, Dictionary<string, object> item)
7314
+ static string UiTargetEvidence(AutomationElement element, AutomationElement root, Dictionary<string, List<object>> cache, Dictionary<string, object> item, UiReadBudget budget = null)
6514
7315
  {
6515
7316
  var ancestors = new List<object>();
6516
- AutomationElement current = TreeWalker.ControlViewWalker.GetParent(element);
6517
- string parentId = current == null ? "" : String.Join(".", current.GetRuntimeId());
7317
+ AutomationElement current = UiReadMaybe(budget, delegate { return TreeWalker.ControlViewWalker.GetParent(element); });
7318
+ string parentId = current == null ? "" : String.Join(".", UiReadMaybe(budget, delegate { return current.GetRuntimeId(); }));
6518
7319
  if (parentId.Length == 0 || !cache.TryGetValue(parentId, out ancestors)) {
6519
7320
  ancestors = new List<object>();
6520
- bool rooted = Automation.Compare(element, root);
7321
+ bool rooted = UiReadMaybe(budget, delegate { return Automation.Compare(element, root); });
6521
7322
  for (int depth = 0; current != null && depth < 24; depth++) {
6522
- ancestors.Add(UiSemanticPart(current));
6523
- if (Automation.Compare(current, root)) { rooted = true; break; }
6524
- current = TreeWalker.ControlViewWalker.GetParent(current);
7323
+ ancestors.Add(UiSemanticPart(current, budget));
7324
+ if (UiReadMaybe(budget, delegate { return Automation.Compare(current, root); })) { rooted = true; break; }
7325
+ current = UiReadMaybe(budget, delegate { return TreeWalker.ControlViewWalker.GetParent(current); });
6525
7326
  }
6526
7327
  if (!rooted) throw new Exception("TARGET_STALE: control is outside the observed window or ancestry is incomplete");
6527
7328
  if (parentId.Length > 0) cache[parentId] = ancestors;
6528
7329
  }
6529
- if (item == null) item = ElemDict(element);
7330
+ if (item == null) item = budget == null ? ElemDict(element) : ElemDictBudgeted(element, budget);
6530
7331
  var patterns = new List<string>();
6531
7332
  object raw;
6532
7333
  if (!item.TryGetValue("patterns", out raw)) throw new Exception("TARGET_STALE: native patterns unavailable");
@@ -6534,14 +7335,14 @@ namespace DshComputerUse.Worker
6534
7335
  patterns.Sort(StringComparer.Ordinal);
6535
7336
  // AutomationId is a separate identity channel, so a unique semantic replacement can
6536
7337
  // still be recognized if a provider regenerates IDs. Ancestor identities remain strict.
6537
- return Json.Serialize(new object[] { item["role"], UiText(element.Current.Name),
7338
+ return Json.Serialize(new object[] { item["role"], UiText(UiReadMaybe(budget, delegate { return element.Current.Name; })),
6538
7339
  item["className"], item["isPassword"], patterns, ancestors });
6539
7340
  }
6540
- static string RememberUiTarget(AutomationElement element, IntPtr hwnd, string witness, AutomationElement root, Dictionary<string, List<object>> cache, Dictionary<string, object> item)
7341
+ static string RememberUiTarget(AutomationElement element, IntPtr hwnd, string witness, AutomationElement root, Dictionary<string, List<object>> cache, Dictionary<string, object> item, UiReadBudget budget = null)
6541
7342
  {
6542
7343
  string runtimeId = GetS(item, "runtimeId", "");
6543
7344
  if (runtimeId.Length == 0) throw new Exception("native control has no runtime identity");
6544
- string evidence = UiTargetEvidence(element, root, cache, item);
7345
+ string evidence = UiTargetEvidence(element, root, cache, item, budget);
6545
7346
  string autoId = GetS(item, "automationId", "");
6546
7347
  string key = Json.Serialize(new object[] { witness, runtimeId, evidence, autoId });
6547
7348
  string existing; ObservedUiTarget old;
@@ -6550,7 +7351,7 @@ namespace DshComputerUse.Worker
6550
7351
  }
6551
7352
  string token = "T" + Guid.NewGuid().ToString("N");
6552
7353
  var target = new ObservedUiTarget { Token = token, Key = key, Hwnd = hwnd, Witness = witness, RuntimeId = runtimeId,
6553
- Evidence = evidence, AutoId = autoId, Name = element.Current.Name,
7354
+ Evidence = evidence, AutoId = autoId, Name = UiReadMaybe(budget, delegate { return element.Current.Name; }),
6554
7355
  Role = GetS(item, "role", ""), Element = element, At = NowMs() };
6555
7356
  UiTargets[token] = target; UiTargetKeys[key] = token; UiTargetOrder.Enqueue(token);
6556
7357
  while (UiTargets.Count > 512) {
@@ -6568,36 +7369,38 @@ namespace DshComputerUse.Worker
6568
7369
  throw NotDispatched("TARGET_STALE", "TARGET_STALE: window identity, title or bounds changed; query again; no action sent");
6569
7370
  return target;
6570
7371
  }
6571
- static AutomationElement ResolveUiTarget(ObservedUiTarget target, bool rebind, int depth, int budget, out string mode, out int seen)
7372
+ static AutomationElement ResolveUiTarget(ObservedUiTarget target, bool rebind, int depth, int budget, out string mode, out int seen,
7373
+ UiReadBudget readBudget = null, Dictionary<string, object> scope = null)
6572
7374
  {
7375
+ if (readBudget == null) readBudget = new UiReadBudget(1200, UiClockMs);
6573
7376
  mode = "runtime-id"; seen = 0;
6574
7377
  try {
6575
- if (String.Join(".", target.Element.GetRuntimeId()) == target.RuntimeId &&
6576
- target.Element.Current.AutomationId == target.AutoId && UiTargetEvidence(target.Element, target.Hwnd) == target.Evidence)
7378
+ if (String.Join(".", readBudget.Read(delegate { return target.Element.GetRuntimeId(); })) == target.RuntimeId &&
7379
+ readBudget.Read(delegate { return target.Element.Current.AutomationId; }) == target.AutoId && UiTargetEvidence(target.Element, target.Hwnd, readBudget) == target.Evidence)
6577
7380
  return target.Element;
6578
- } catch { }
7381
+ } catch (UiBudgetStop) { throw; } catch { }
6579
7382
  if (!rebind) throw new Exception("TARGET_STALE: observed control was replaced or changed; query again or explicitly request rebind; no action sent");
6580
- var start = AutomationElement.FromHandle(target.Hwnd);
7383
+ var start = scope == null ? readBudget.Read(delegate { return AutomationElement.FromHandle(target.Hwnd); }) : UiQueryRoot(scope, target.Hwnd, readBudget);
6581
7384
  bool truncated; int visited;
6582
7385
  List<AutomationElement> candidates = new List<AutomationElement>();
6583
7386
  if (!String.IsNullOrEmpty(target.AutoId)) {
6584
- candidates = FindUiMatches(start, null, target.AutoId, null, depth, budget, budget, out visited, out truncated);
7387
+ candidates = FindUiMatchesBudgeted(start, null, target.AutoId, null, depth, budget, budget, readBudget, out visited, out truncated);
6585
7388
  seen += visited;
6586
7389
  if (truncated) throw new Exception("INCOMPLETE_UIA_SEARCH: cannot prove unique target recovery; no action sent");
6587
7390
  if (candidates.Count > 1) throw new Exception("AMBIGUOUS_UIA_TARGET: native identity matches multiple controls; no action sent");
6588
7391
  if (candidates.Count == 1) {
6589
- if (UiTargetEvidence(candidates[0], target.Hwnd) != target.Evidence)
7392
+ if (UiTargetEvidence(candidates[0], target.Hwnd, readBudget) != target.Evidence)
6590
7393
  throw new Exception("TARGET_SEMANTICS_CHANGED: native ID now names a different control/context; no action sent");
6591
7394
  mode = "native-identifier";
6592
7395
  }
6593
7396
  }
6594
7397
  if (candidates.Count == 0) {
6595
7398
  if (String.IsNullOrWhiteSpace(target.Name)) throw new Exception("TARGET_STALE: target has no usable semantic name; no action sent");
6596
- var matches = FindUiMatches(start, target.Name, null, target.Role, depth, budget, budget, out visited, out truncated);
7399
+ var matches = FindUiMatchesBudgeted(start, target.Name, null, target.Role, depth, budget, budget, readBudget, out visited, out truncated);
6597
7400
  seen += visited;
6598
7401
  if (truncated) throw new Exception("INCOMPLETE_UIA_SEARCH: cannot prove unique semantic recovery; no action sent");
6599
7402
  foreach (AutomationElement element in matches)
6600
- if (UiTargetEvidence(element, target.Hwnd) == target.Evidence) candidates.Add(element);
7403
+ if (UiTargetEvidence(element, target.Hwnd, readBudget) == target.Evidence) candidates.Add(element);
6601
7404
  if (candidates.Count > 1) throw new Exception("AMBIGUOUS_UIA_TARGET: semantic recovery matches multiple controls; no action sent");
6602
7405
  if (candidates.Count == 0) throw new Exception("TARGET_STALE: no control retains the observed identity/context; no action sent");
6603
7406
  mode = "semantic";
@@ -6615,9 +7418,28 @@ namespace DshComputerUse.Worker
6615
7418
  // button, expanding a dropdown, filling a field.
6616
7419
  static bool ElementMatches(AutomationElement e, string nameFilter, string autoIdFilter, string roleFilter)
6617
7420
  {
6618
- if (autoIdFilter != null && !String.Equals(e.Current.AutomationId, autoIdFilter, StringComparison.Ordinal)) return false;
6619
- if (roleFilter != null && !String.Equals(e.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""), roleFilter, StringComparison.OrdinalIgnoreCase)) return false;
6620
- if (nameFilter != null && (e.Current.Name ?? "").IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) < 0) return false;
7421
+ return ElementMatchesBudgeted(e, nameFilter, autoIdFilter, roleFilter, null);
7422
+ }
7423
+ static CacheRequest UiSelectorCache()
7424
+ {
7425
+ // Only the current navigated element, never a cached subtree. This
7426
+ // batches the three selector properties into the navigation call.
7427
+ var cache = new CacheRequest(); cache.TreeScope = TreeScope.Element;
7428
+ cache.AutomationElementMode = AutomationElementMode.Full;
7429
+ cache.Add(AutomationElement.NameProperty); cache.Add(AutomationElement.AutomationIdProperty);
7430
+ cache.Add(AutomationElement.ControlTypeProperty); return cache;
7431
+ }
7432
+ static T UiSelectorRead<T>(UiReadBudget budget, Func<T> cached, Func<T> current)
7433
+ {
7434
+ if (budget != null && !budget.Continue()) throw new UiBudgetStop();
7435
+ try { return cached(); }
7436
+ catch (InvalidOperationException) { return UiReadMaybe(budget, current); }
7437
+ }
7438
+ static bool ElementMatchesBudgeted(AutomationElement e, string nameFilter, string autoIdFilter, string roleFilter, UiReadBudget budget)
7439
+ {
7440
+ if (autoIdFilter != null && !String.Equals(UiSelectorRead(budget, delegate { return e.Cached.AutomationId; }, delegate { return e.Current.AutomationId; }), autoIdFilter, StringComparison.Ordinal)) return false;
7441
+ if (roleFilter != null && !String.Equals(UiSelectorRead(budget, delegate { return e.Cached.ControlType; }, delegate { return e.Current.ControlType; }).ProgrammaticName.Replace("ControlType.", ""), roleFilter, StringComparison.OrdinalIgnoreCase)) return false;
7442
+ if (nameFilter != null && (UiSelectorRead(budget, delegate { return e.Cached.Name; }, delegate { return e.Current.Name; }) ?? "").IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) < 0) return false;
6621
7443
  return true;
6622
7444
  }
6623
7445
 
@@ -6626,32 +7448,19 @@ namespace DshComputerUse.Worker
6626
7448
  static List<AutomationElement> FindUiMatches(AutomationElement start, string name, string autoId,
6627
7449
  string role, int depth, int maxNodes, int limit, out int seen, out bool truncated)
6628
7450
  {
6629
- var matches = new List<AutomationElement>();
6630
- var q = new Queue<KeyValuePair<AutomationElement, int>>();
6631
- q.Enqueue(new KeyValuePair<AutomationElement, int>(start, 0));
6632
- seen = 0; truncated = false;
6633
- while (q.Count > 0 && seen < maxNodes)
6634
- {
6635
- var kv = q.Dequeue(); seen++;
6636
- try
6637
- {
6638
- if (ElementMatches(kv.Key, name, autoId, role))
6639
- {
6640
- matches.Add(kv.Key);
6641
- if (matches.Count >= limit) { truncated = true; break; }
6642
- }
6643
- if (kv.Value < depth)
6644
- foreach (AutomationElement child in kv.Key.FindAll(TreeScope.Children, Condition.TrueCondition))
6645
- {
6646
- if (seen + q.Count >= maxNodes) { truncated = true; break; }
6647
- q.Enqueue(new KeyValuePair<AutomationElement, int>(child, kv.Value + 1));
6648
- }
6649
- else if (kv.Key.FindFirst(TreeScope.Children, Condition.TrueCondition) != null)
6650
- truncated = true;
6651
- }
6652
- catch { truncated = true; }
6653
- }
6654
- if (q.Count > 0) truncated = true;
7451
+ return FindUiMatchesBudgeted(start, name, autoId, role, depth, maxNodes, limit,
7452
+ new UiReadBudget(1200, UiClockMs), out seen, out truncated);
7453
+ }
7454
+ static List<AutomationElement> FindUiMatchesBudgeted(AutomationElement start, string name, string autoId,
7455
+ string role, int depth, int maxNodes, int limit, UiReadBudget budget, out int seen, out bool truncated)
7456
+ {
7457
+ int before = budget.Scanned;
7458
+ CacheRequest selectorCache = UiSelectorCache();
7459
+ var matches = UiWalkBounded(start, depth, maxNodes, limit, budget,
7460
+ delegate(AutomationElement element) { return ElementMatchesBudgeted(element, name, autoId, role, budget); },
7461
+ delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetFirstChild(element, selectorCache); },
7462
+ delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetNextSibling(element, selectorCache); });
7463
+ seen = budget.Scanned - before; truncated = budget.Truncated;
6655
7464
  return matches;
6656
7465
  }
6657
7466
 
@@ -6665,35 +7474,46 @@ namespace DshComputerUse.Worker
6665
7474
  IntPtr root = observed == null ? FindHwnd(a) : observed.Hwnd;
6666
7475
  if (observed != null && a.ContainsKey("hwnd") && FindHwnd(a) != root) throw new Exception("TARGET_STALE: target belongs to another window; no action sent");
6667
7476
  if (root != IntPtr.Zero) HostGuard(root, "uiaAct");
6668
- AutomationElement start = root != IntPtr.Zero ? AutomationElement.FromHandle(root) : AutomationElement.RootElement;
6669
7477
  string name = GetS(a, "nameContains", null);
6670
7478
  string autoId = GetS(a, "automationId", null);
6671
7479
  string role = GetS(a, "role", null);
6672
7480
  int index = Math.Max(0, GetI(a, "index", 0));
6673
- int depth = Math.Max(1, Math.Min(24, GetI(a, "depth", 12)));
6674
- int maxNodes = Math.Max(20, Math.Min(4000, GetI(a, "maxNodes", 2000)));
7481
+ int depth = Math.Max(1, Math.Min(24, GetI(a, "depth", 6)));
7482
+ int maxNodes = Math.Max(20, Math.Min(1200, GetI(a, "maxNodes", 120)));
7483
+ bool rebind = GetB(a, "allowRebind", false);
7484
+ if (observed == null || rebind) ValidateUiQueryScope(root != IntPtr.Zero, true,
7485
+ !String.IsNullOrWhiteSpace(GetS(a, "anchor", null)), depth, maxNodes);
7486
+ UiReadBudget readBudget = NewUiBudget(a);
6675
7487
  bool unique = GetB(a, "requireUnique", !a.ContainsKey("index"));
6676
7488
  List<object> skipped = new List<object>();
6677
7489
  int seen = 0; bool truncated = false; string resolution = null;
6678
- var matches = observed == null ? FindUiMatches(start, name, autoId, role, depth, maxNodes,
6679
- unique ? 2 : index + 1, out seen, out truncated) : new List<AutomationElement> {
6680
- ResolveUiTarget(observed, GetB(a, "allowRebind", false), depth, maxNodes, out resolution, out seen) };
7490
+ List<AutomationElement> matches;
7491
+ try {
7492
+ matches = observed == null ? FindUiMatchesBudgeted(UiQueryRoot(a, root, readBudget), name, autoId, role, depth, maxNodes,
7493
+ unique ? 2 : index + 1, readBudget, out seen, out truncated) : new List<AutomationElement> {
7494
+ ResolveUiTarget(observed, rebind, depth, maxNodes, out resolution, out seen, readBudget, a) };
7495
+ } catch (UiBudgetStop) {
7496
+ throw new Exception("INCOMPLETE_UIA_SEARCH: soft provider-read budget exhausted; no action sent. Observe a narrower point/anchor or refresh once; a synchronous COM call cannot be interrupted by this budget.");
7497
+ }
7498
+ truncated = truncated || readBudget.Truncated;
7499
+ if (!readBudget.Continue())
7500
+ throw new Exception("INCOMPLETE_UIA_SEARCH: provider-read budget exhausted; no action sent; use a narrower point/anchor");
6681
7501
  if (unique && matches.Count > 1)
6682
7502
  throw new Exception("AMBIGUOUS_UIA_TARGET: multiple controls match; no action sent. Query computer_uia, then specify hwnd and an exact id.");
6683
7503
  if (unique && truncated)
6684
7504
  throw new Exception("INCOMPLETE_UIA_SEARCH: uniqueness could not be verified; no action sent. Narrow the target window or query with greater depth.");
6685
7505
  AutomationElement found = matches.Count > index ? matches[index] : null;
6686
7506
  if (!unique) for (int i = 0; i < Math.Min(index, matches.Count); i++)
6687
- try { skipped.Add(ElemDict(matches[i])); } catch { }
7507
+ try { skipped.Add(ElemDictBudgeted(matches[i], readBudget)); }
7508
+ catch (UiBudgetStop) { throw new Exception("INCOMPLETE_UIA_SEARCH: provider-read budget exhausted before action; no action sent"); } catch { }
6688
7509
  if (found == null)
6689
7510
  throw new Exception("uiaAct: no element matched (nameContains=" + (name ?? "-") +
6690
7511
  ", automationId=" + (autoId ?? "-") + ", role=" + (role ?? "-") +
6691
7512
  ", index=" + index + ") among " + seen + " elements");
6692
7513
 
6693
7514
  // A UIA mutation shares the same actual-action boundary and takeover checks.
6694
- // A selector WITHOUT hwnd searches the whole desktop, so there the element that
6695
- // actually matched decides the victim - and a host control is never driven through
6696
- // its automation pattern either.
7515
+ // Scoped selectors and observed targets still need a final process
7516
+ // guard; a host control is never driven through its automation pattern.
6697
7517
  int foundPid = 0;
6698
7518
  try { foundPid = found.Current.ProcessId; } catch { }
6699
7519
  HostGuardPid(foundPid, "uiaAct");
@@ -7027,6 +7847,8 @@ namespace DshComputerUse.Worker
7027
7847
  {
7028
7848
  var result = Err(error.Message);
7029
7849
  if (error.Data["glowHealth"] != null) result["glowHealth"] = error.Data["glowHealth"];
7850
+ foreach (string key in new string[] { "dragStarted", "expectedCursor", "actualCursor", "cursorCheck" })
7851
+ if (error.Data.Contains(key)) result[key] = error.Data[key];
7030
7852
  if (error.Data["code"] != null)
7031
7853
  {
7032
7854
  result["code"] = (string)error.Data["code"];
@@ -7120,3 +7942,742 @@ namespace DshComputerUse.Worker
7120
7942
  }
7121
7943
  }
7122
7944
  }
7945
+
7946
+ namespace DshComputerUse.Worker
7947
+ {
7948
+ /// A small, capture-excluded visual replacement at the native pointer hotspot.
7949
+ /// Native cursor resources remain intact for hit evidence and screenshot burn-in.
7950
+ /// Update/Hide/Dispose are called only by Glow's window-owning render thread.
7951
+ public static class AgentPointer
7952
+ {
7953
+ const int LogicalWidth = KitePointerPainter.LogicalWidth, LogicalHeight = KitePointerPainter.LogicalHeight;
7954
+ const int WS_POPUP = unchecked((int)0x80000000), WS_DISABLED = 0x08000000;
7955
+ const int CompanionStyle = 0x00080000 | 0x00000020 | 0x00000080 | 0x08000000 | 0x00000008;
7956
+ [StructLayout(LayoutKind.Sequential)] struct Pt { public int X, Y; }
7957
+ [StructLayout(LayoutKind.Sequential)] struct Sz { public int X, Y; }
7958
+ [StructLayout(LayoutKind.Sequential)] struct Rc { public int Left, Top, Right, Bottom; }
7959
+ [StructLayout(LayoutKind.Sequential)] struct MonitorInfo { public uint Size; public Rc Monitor, Work; public uint Flags; }
7960
+ [StructLayout(LayoutKind.Sequential)] struct CursorInfo { public uint Size, Flags; public IntPtr Cursor; public Pt Position; }
7961
+ [StructLayout(LayoutKind.Sequential, Pack = 1)] struct Blend { public byte Op, Flags, Alpha, Format; }
7962
+ [StructLayout(LayoutKind.Sequential)] struct BitmapHeader
7963
+ {
7964
+ public uint Size; public int Width, Height; public ushort Planes, BitCount;
7965
+ public uint Compression, SizeImage; public int XPelsPerMeter, YPelsPerMeter;
7966
+ public uint ClrUsed, ClrImportant;
7967
+ }
7968
+ [StructLayout(LayoutKind.Sequential)] struct BitmapInfo { public BitmapHeader Header; public uint Color; }
7969
+ [StructLayout(LayoutKind.Sequential)] struct Msg
7970
+ {
7971
+ public IntPtr Hwnd; public uint Message; public UIntPtr WParam;
7972
+ public IntPtr LParam; public uint Time; public Pt Point; public uint Private;
7973
+ }
7974
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
7975
+ static extern IntPtr CreateWindowEx(int ex, string cls, string title, int style, int x, int y, int w, int h, IntPtr parent, IntPtr menu, IntPtr instance, IntPtr param);
7976
+ [DllImport("user32.dll")] static extern bool DestroyWindow(IntPtr hwnd);
7977
+ [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hwnd, int command);
7978
+ [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hwnd);
7979
+ [DllImport("user32.dll", SetLastError = true)] static extern bool SetWindowPos(IntPtr hwnd, IntPtr after, int x, int y, int w, int h, uint flags);
7980
+ [DllImport("user32.dll")] static extern bool GetCursorInfo(ref CursorInfo info);
7981
+ [DllImport("user32.dll")] static extern IntPtr MonitorFromPoint(Pt point, uint flags);
7982
+ [DllImport("user32.dll")] static extern bool GetMonitorInfo(IntPtr monitor, ref MonitorInfo info);
7983
+ [DllImport("shcore.dll")] static extern int GetDpiForMonitor(IntPtr monitor, int kind, out uint x, out uint y);
7984
+ [DllImport("user32.dll")] static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);
7985
+ [DllImport("user32.dll")] static extern IntPtr GetDC(IntPtr hwnd);
7986
+ [DllImport("user32.dll")] static extern int ReleaseDC(IntPtr hwnd, IntPtr dc);
7987
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool PeekMessage(out Msg msg, IntPtr hwnd, uint min, uint max, uint remove);
7988
+ [DllImport("user32.dll")] static extern bool ValidateRect(IntPtr hwnd, IntPtr rect);
7989
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr DispatchMessage(ref Msg msg);
7990
+ [DllImport("gdi32.dll")] static extern IntPtr CreateCompatibleDC(IntPtr dc);
7991
+ [DllImport("gdi32.dll")] static extern bool DeleteDC(IntPtr dc);
7992
+ [DllImport("gdi32.dll")] static extern IntPtr SelectObject(IntPtr dc, IntPtr value);
7993
+ [DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr value);
7994
+ [DllImport("gdi32.dll")] static extern IntPtr CreateDIBSection(IntPtr dc, ref BitmapInfo info, uint use, out IntPtr bits, IntPtr section, uint offset);
7995
+ [DllImport("user32.dll", SetLastError = true)] static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr dc, ref Pt dest, ref Sz size, IntPtr source, ref Pt origin, int key, ref Blend blend, int flags);
7996
+
7997
+ static IntPtr window, screenDc, memoryDc, dib, oldBitmap, pixels, monitor;
7998
+ static int width, height, lastX = int.MinValue, lastY = int.MinValue, lastFrame = -1;
7999
+ static bool visible, failed, lastHolding;
8000
+ static string lastShape;
8001
+ static float scale = 1;
8002
+ static Rc monitorBounds;
8003
+ static byte[] copy;
8004
+ public static string Fault { get; private set; }
8005
+
8006
+ // Only a protected hold animates. Sixteen brightness steps over 2.5 seconds
8007
+ // avoid a repaint on every Glow tick; active input has a static bitmap.
8008
+ public static int AnimationFrame(bool holding, int tick)
8009
+ {
8010
+ return KitePointerPainter.AnimationFrame(holding, tick);
8011
+ }
8012
+
8013
+ public static void Update(bool show, bool holding)
8014
+ {
8015
+ if (!show || failed) { Hide(); return; }
8016
+ try
8017
+ {
8018
+ CursorInfo cursor = new CursorInfo(); cursor.Size = (uint)Marshal.SizeOf(typeof(CursorInfo));
8019
+ // Follow native cursor visibility, including text-entry apps which hide it.
8020
+ if (!GetCursorInfo(ref cursor) || (cursor.Flags & 1) == 0) { Hide(); return; }
8021
+ IntPtr nextMonitor = MonitorFromPoint(cursor.Position, 2);
8022
+ if (nextMonitor != monitor || window == IntPtr.Zero)
8023
+ {
8024
+ MonitorInfo info = new MonitorInfo(); info.Size = (uint)Marshal.SizeOf(typeof(MonitorInfo));
8025
+ if (!GetMonitorInfo(nextMonitor, ref info)) { Hide(); return; }
8026
+ monitorBounds = info.Monitor; monitor = nextMonitor;
8027
+ uint dpiX = 96, dpiY = 96;
8028
+ try { if (GetDpiForMonitor(monitor, 0, out dpiX, out dpiY) != 0) dpiX = 96; }
8029
+ catch (EntryPointNotFoundException) { dpiX = 96; }
8030
+ catch (DllNotFoundException) { dpiX = 96; }
8031
+ scale = Math.Max(1.0f, Math.Min(4.0f, dpiX / 96.0f));
8032
+ EnsureSurface((int)Math.Ceiling(LogicalWidth * scale), (int)Math.Ceiling(LogicalHeight * scale));
8033
+ lastFrame = -1;
8034
+ }
8035
+ // The drawn tip IS the native click point. Never flip or clamp the
8036
+ // surface near an edge: that would lie about the actual hotspot.
8037
+ System.Drawing.Point hotspot = KitePointerPainter.Hotspot(scale);
8038
+ int x = cursor.Position.X - hotspot.X, y = cursor.Position.Y - hotspot.Y;
8039
+ string shape = Program.StandardCursorShape(cursor.Cursor,
8040
+ delegate(int id) { return Native.LoadCursor(IntPtr.Zero, new IntPtr(id)); });
8041
+ int frame = AnimationFrame(holding, Environment.TickCount);
8042
+ if (frame != lastFrame || holding != lastHolding || shape != lastShape)
8043
+ {
8044
+ using (Bitmap bitmap = RenderPreview(scale, holding, frame, shape))
8045
+ {
8046
+ BitmapData data = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format32bppPArgb);
8047
+ try
8048
+ {
8049
+ if (copy == null || copy.Length != width * height * 4) copy = new byte[width * height * 4];
8050
+ for (int row = 0; row < height; row++)
8051
+ Marshal.Copy(IntPtr.Add(data.Scan0, row * data.Stride), copy, row * width * 4, width * 4);
8052
+ Marshal.Copy(copy, 0, pixels, copy.Length);
8053
+ }
8054
+ finally { bitmap.UnlockBits(data); }
8055
+ }
8056
+ Pt target = new Pt { X = x, Y = y }, origin = new Pt();
8057
+ Sz size = new Sz { X = width, Y = height };
8058
+ Blend blend = new Blend { Alpha = 255, Format = 1 };
8059
+ if (!UpdateLayeredWindow(window, screenDc, ref target, ref size, memoryDc, ref origin, 0, ref blend, 2))
8060
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
8061
+ lastFrame = frame; lastHolding = holding; lastShape = shape; lastX = x; lastY = y;
8062
+ }
8063
+ else if (x != lastX || y != lastY)
8064
+ {
8065
+ // Moving a cached tiny layered surface does not redraw the desktop.
8066
+ if (!SetWindowPos(window, new IntPtr(-1), x, y, 0, 0, 0x0001 | 0x0010))
8067
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
8068
+ lastX = x; lastY = y;
8069
+ }
8070
+ if (!visible) { ShowWindow(window, 4); visible = true; }
8071
+ if (!IsWindowVisible(window)) throw new InvalidOperationException("replacement pointer surface is not visible");
8072
+ // Publish a short hide lease only AFTER a successful visible paint.
8073
+ // While the independent guardian starts, retain the native cursor.
8074
+ if (!RequestReplacement(true))
8075
+ {
8076
+ ShowWindow(window, 0); visible = false;
8077
+ if (PointerVisibilityLease.Fault != null) throw new InvalidOperationException(PointerVisibilityLease.Fault);
8078
+ return;
8079
+ }
8080
+ Msg msg;
8081
+ for (int n = 0; n < 8 && PeekMessage(out msg, window, 0, 0, 1); n++)
8082
+ {
8083
+ if (msg.Message == 0x000F) ValidateRect(window, IntPtr.Zero);
8084
+ else DispatchMessage(ref msg);
8085
+ }
8086
+ }
8087
+ catch (Exception error)
8088
+ {
8089
+ // This presentation is optional. A display failure must never block input,
8090
+ // leak a global cursor, or turn a recoverable Glow frame into a fatal one.
8091
+ Fault = error.GetType().Name + ": " + error.Message; failed = true; Dispose();
8092
+ }
8093
+ }
8094
+
8095
+ static void EnsureSurface(int w, int h)
8096
+ {
8097
+ if (window == IntPtr.Zero)
8098
+ {
8099
+ window = CreateWindowEx(CompanionStyle, "STATIC", "DSH kite pointer", WS_POPUP | WS_DISABLED,
8100
+ 0, 0, w, h, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
8101
+ if (window == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
8102
+ // If exclusion is unavailable, omit the decoration rather than paint it
8103
+ // into evidence. WS_DISABLED + TRANSPARENT prevent input interception.
8104
+ if (!SetWindowDisplayAffinity(window, 0x11)) throw new InvalidOperationException("pointer capture exclusion unavailable");
8105
+ screenDc = GetDC(IntPtr.Zero); memoryDc = CreateCompatibleDC(screenDc);
8106
+ if (screenDc == IntPtr.Zero || memoryDc == IntPtr.Zero) throw new InvalidOperationException("pointer DC unavailable");
8107
+ }
8108
+ if (width == w && height == h && dib != IntPtr.Zero) return;
8109
+ ReleaseSurface();
8110
+ BitmapInfo info = new BitmapInfo(); info.Header.Size = (uint)Marshal.SizeOf(typeof(BitmapHeader));
8111
+ info.Header.Width = w; info.Header.Height = -h; info.Header.Planes = 1; info.Header.BitCount = 32;
8112
+ dib = CreateDIBSection(screenDc, ref info, 0, out pixels, IntPtr.Zero, 0);
8113
+ if (dib == IntPtr.Zero) throw new InvalidOperationException("pointer surface unavailable");
8114
+ oldBitmap = SelectObject(memoryDc, dib);
8115
+ if (oldBitmap == IntPtr.Zero || oldBitmap == new IntPtr(-1)) throw new InvalidOperationException("pointer bitmap unavailable");
8116
+ width = w; height = h;
8117
+ }
8118
+
8119
+ // Offscreen previews and the live pointer share the exact painter/hotspot.
8120
+ public static Bitmap RenderPreview(float dpiScale, bool holding, int frame, string semantic = "arrow")
8121
+ { return KitePointerPainter.RenderPreview(dpiScale, holding, frame, semantic); }
8122
+
8123
+ // A narrow seam for private-desktop tests; production uses the guardian lease.
8124
+ static bool RequestReplacement(bool show)
8125
+ { if (show) return PointerVisibilityLease.TryHide(); PointerVisibilityLease.Show(); return false; }
8126
+
8127
+ public static void Hide()
8128
+ {
8129
+ RequestReplacement(false);
8130
+ if (window != IntPtr.Zero && visible) ShowWindow(window, 0);
8131
+ visible = false;
8132
+ }
8133
+ static void ReleaseSurface()
8134
+ {
8135
+ if (oldBitmap != IntPtr.Zero && oldBitmap != new IntPtr(-1) && memoryDc != IntPtr.Zero) SelectObject(memoryDc, oldBitmap);
8136
+ oldBitmap = IntPtr.Zero;
8137
+ if (dib != IntPtr.Zero) DeleteObject(dib);
8138
+ dib = IntPtr.Zero; pixels = IntPtr.Zero; width = height = 0; copy = null;
8139
+ }
8140
+ public static void Dispose()
8141
+ {
8142
+ Hide(); ReleaseSurface();
8143
+ PointerVisibilityLease.Dispose();
8144
+ if (memoryDc != IntPtr.Zero) DeleteDC(memoryDc);
8145
+ if (screenDc != IntPtr.Zero) ReleaseDC(IntPtr.Zero, screenDc);
8146
+ if (window != IntPtr.Zero) DestroyWindow(window);
8147
+ memoryDc = screenDc = window = monitor = IntPtr.Zero;
8148
+ lastFrame = -1; lastX = lastY = int.MinValue;
8149
+ }
8150
+ }
8151
+ }
8152
+
8153
+ namespace DshComputerUse.Worker
8154
+ {
8155
+ public static class KitePointerPainter
8156
+ {
8157
+ public const int LogicalWidth = 39, LogicalHeight = 44;
8158
+ public const float BodyScale = 0.75f, TiltDegrees = -25f;
8159
+ public static readonly PointF LogicalHotspot = new PointF(8, 4);
8160
+ static readonly double Angle = TiltDegrees * Math.PI / 180.0;
8161
+
8162
+ static float Scale(float value)
8163
+ {
8164
+ if (float.IsNaN(value) || float.IsInfinity(value)) return 1f;
8165
+ return Math.Max(1f, Math.Min(4f, value));
8166
+ }
8167
+
8168
+ public static Size CanvasSize(float dpiScale)
8169
+ {
8170
+ float s = Scale(dpiScale);
8171
+ return new Size((int)Math.Ceiling(LogicalWidth * s), (int)Math.Ceiling(LogicalHeight * s));
8172
+ }
8173
+
8174
+ public static Point Hotspot(float dpiScale)
8175
+ {
8176
+ float s = Scale(dpiScale);
8177
+ return new Point((int)Math.Round(LogicalHotspot.X * s, MidpointRounding.AwayFromZero),
8178
+ (int)Math.Round(LogicalHotspot.Y * s, MidpointRounding.AwayFromZero));
8179
+ }
8180
+
8181
+ // Source kite tip is (27,7). Rotation is counterclockwise in screen coordinates:
8182
+ // an upward-pointing head tilts toward the upper-left. Device-pixel H is shared
8183
+ // by the painter and the future cursor hotspot; it never depends on animation.
8184
+ public static PointF Transform(float x, float y, float dpiScale)
8185
+ {
8186
+ float s = Scale(dpiScale);
8187
+ Point h = Hotspot(s);
8188
+ double dx = x - 27, dy = y - 7;
8189
+ return new PointF(h.X + (float)(s * BodyScale * (Math.Cos(Angle) * dx - Math.Sin(Angle) * dy)),
8190
+ h.Y + (float)(s * BodyScale * (Math.Sin(Angle) * dx + Math.Cos(Angle) * dy)));
8191
+ }
8192
+
8193
+ public static int AnimationFrame(bool holding, int tick)
8194
+ {
8195
+ if (!holding) return 16;
8196
+ double phase = (unchecked((uint)tick) % 2500) * 2 * Math.PI / 2500.0;
8197
+ return 7 + (int)Math.Round(4 * (1 - Math.Cos(phase)));
8198
+ }
8199
+
8200
+ static PointF[] Shape(float s, params float[] xy)
8201
+ {
8202
+ PointF[] points = new PointF[xy.Length / 2];
8203
+ for (int i = 0; i < points.Length; i++) points[i] = Transform(xy[2 * i], xy[2 * i + 1], s);
8204
+ return points;
8205
+ }
8206
+
8207
+ public static Bitmap RenderPreview(float dpiScale, bool holding, int frame, string semantic = "arrow")
8208
+ {
8209
+ float s = Scale(dpiScale);
8210
+ Size size = CanvasSize(s);
8211
+ Bitmap bitmap = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppPArgb);
8212
+ float pulse = holding ? Math.Max(0f, Math.Min(1f, (frame - 7) / 8f)) : 1f;
8213
+ int bodyAlpha = holding ? 208 + (int)(36 * pulse) : 248;
8214
+ PointF[] kite = Shape(s, 27, 7, 41, 24, 27, 43, 13, 24);
8215
+ using (Graphics g = Graphics.FromImage(bitmap))
8216
+ {
8217
+ g.Clear(Color.Transparent);
8218
+ g.SmoothingMode = SmoothingMode.AntiAlias;
8219
+ g.PixelOffsetMode = PixelOffsetMode.HighQuality;
8220
+
8221
+ // A slim visible string attaches to the lower point. No satellite star,
8222
+ // duplicate arrow, end bead or external status symbol crowds the tip.
8223
+ using (Pen tail = new Pen(Color.FromArgb(holding ? 185 + (int)(35 * pulse) : 230, 39, 118, 224), 1.1f * s))
8224
+ {
8225
+ tail.StartCap = tail.EndCap = LineCap.Round;
8226
+ g.DrawBezier(tail, Transform(27, 43, s), Transform(25, 53, s),
8227
+ Transform(11, 41, s), Transform(10, 53, s));
8228
+ }
8229
+
8230
+ using (Pen halo = new Pen(Color.FromArgb(holding ? 8 + (int)(20 * pulse) : 28, 54, 132, 255), 2.5f * s))
8231
+ g.DrawPolygon(halo, kite);
8232
+ using (LinearGradientBrush fill = new LinearGradientBrush(Transform(22, 9, s), Transform(33, 43, s),
8233
+ Color.FromArgb(bodyAlpha, holding ? 49 : 57, holding ? 133 : 150, 250),
8234
+ Color.FromArgb(bodyAlpha, 20, 51, holding ? 128 : 151)))
8235
+ g.FillPolygon(fill, kite);
8236
+ using (SolidBrush facet = new SolidBrush(Color.FromArgb(holding ? 78 + (int)(25 * pulse) : 115, 160, 225, 255)))
8237
+ g.FillPolygon(facet, Shape(s, 27, 7, 41, 24, 27, 25));
8238
+ using (SolidBrush shade = new SolidBrush(Color.FromArgb(78, 6, 24, 79)))
8239
+ g.FillPolygon(shade, Shape(s, 27, 25, 41, 24, 27, 43));
8240
+ using (Pen rim = new Pen(Color.FromArgb(242, 191, 231, 255), Math.Max(0.85f, 0.85f * s)))
8241
+ {
8242
+ rim.LineJoin = LineJoin.Miter;
8243
+ g.DrawPolygon(rim, kite);
8244
+ }
8245
+ using (Pen spine = new Pen(Color.FromArgb(72, 213, 242, 255), 0.55f * s))
8246
+ g.DrawLine(spine, Transform(27, 8, s), Transform(27, 42, s));
8247
+
8248
+ // The state mark stays legible at the dimmest hold frame. Geometry and
8249
+ // hotspot remain stationary; only light/alpha changes during breathing.
8250
+ if (NormalizeSemantic(semantic) == "arrow")
8251
+ using (SolidBrush mark = new SolidBrush(Color.FromArgb(245, 236, 249, 255)))
8252
+ {
8253
+ if (holding)
8254
+ {
8255
+ g.FillPolygon(mark, Shape(s, 23.3f, 20.2f, 25.2f, 20.2f, 25.2f, 28.2f, 23.3f, 28.2f));
8256
+ g.FillPolygon(mark, Shape(s, 29.1f, 20.2f, 31.0f, 20.2f, 31.0f, 28.2f, 29.1f, 28.2f));
8257
+ }
8258
+ else g.FillPolygon(mark, Shape(s, 24, 19.8f, 31.4f, 24.2f, 24, 28.6f));
8259
+ }
8260
+ DrawSemantic(g, s, semantic);
8261
+ }
8262
+ return bitmap;
8263
+ }
8264
+
8265
+ public static string NormalizeSemantic(string semantic)
8266
+ {
8267
+ if (String.IsNullOrEmpty(semantic)) return "arrow";
8268
+ string value = semantic.Trim().ToLowerInvariant();
8269
+ if (value == "ibeam" || value == "i-beam") return "text";
8270
+ switch (value)
8271
+ {
8272
+ case "arrow": case "text": case "resize-ew": case "resize-ns":
8273
+ case "resize-nwse": case "resize-nesw": case "move": case "hand":
8274
+ case "cross": case "wait": case "app-starting": case "not-allowed": case "help":
8275
+ return value;
8276
+ default: return "unknown";
8277
+ }
8278
+ }
8279
+
8280
+ // Every semantic is a small upright center mark. Neither the canvas, kite,
8281
+ // tail nor hotspot moves; holding keeps the same mark and only breathes.
8282
+ // An unknown native cursor gets a neutral dash, never a guessed resize/text mark.
8283
+ static void DrawSemantic(Graphics g, float s, string semantic)
8284
+ {
8285
+ semantic = NormalizeSemantic(semantic);
8286
+ if (semantic == "arrow") return;
8287
+ PointF center = Transform(27, 25, s);
8288
+ using (GraphicsPath path = new GraphicsPath())
8289
+ using (Matrix matrix = new Matrix(s, 0, 0, s, center.X, center.Y))
8290
+ {
8291
+ if (semantic == "text")
8292
+ {
8293
+ path.AddLine(-2, -4, 2, -4);
8294
+ path.StartFigure(); path.AddLine(0, -4, 0, 4);
8295
+ path.StartFigure(); path.AddLine(-2, 4, 2, 4);
8296
+ }
8297
+ else if (semantic.StartsWith("resize-"))
8298
+ {
8299
+ float angle = semantic == "resize-ns" ? 90 : semantic == "resize-nwse" ? 45 : semantic == "resize-nesw" ? -45 : 0;
8300
+ AddDoubleArrow(path, 4.0f, 1.9f);
8301
+ using (Matrix rotate = new Matrix()) { rotate.Rotate(angle); path.Transform(rotate); }
8302
+ }
8303
+ else if (semantic == "move")
8304
+ {
8305
+ AddDoubleArrow(path, 3.9f, 1.5f);
8306
+ using (GraphicsPath vertical = new GraphicsPath())
8307
+ using (Matrix rotate = new Matrix())
8308
+ {
8309
+ AddDoubleArrow(vertical, 3.9f, 1.5f); rotate.Rotate(90); vertical.Transform(rotate);
8310
+ path.AddPath(vertical, false);
8311
+ }
8312
+ }
8313
+ else if (semantic == "cross")
8314
+ {
8315
+ path.AddLine(-4, 0, 4, 0); path.StartFigure(); path.AddLine(0, -4, 0, 4);
8316
+ }
8317
+ else if (semantic == "not-allowed")
8318
+ {
8319
+ path.AddEllipse(-3.6f, -3.6f, 7.2f, 7.2f);
8320
+ path.StartFigure(); path.AddLine(-2.5f, 2.5f, 2.5f, -2.5f);
8321
+ }
8322
+ else if (semantic == "wait")
8323
+ {
8324
+ AddHourglass(path, 0, 0, 2.6f, 3.8f);
8325
+ }
8326
+ else if (semantic == "app-starting")
8327
+ {
8328
+ path.AddLines(new PointF[] { new PointF(-3.5f, 2.7f), new PointF(-3.5f, -3.8f), new PointF(-0.2f, -0.4f) });
8329
+ path.StartFigure(); AddHourglass(path, 1.8f, 1.2f, 1.5f, 2.6f);
8330
+ }
8331
+ else if (semantic == "help")
8332
+ {
8333
+ path.AddBezier(-2.4f, -2.4f, -2.0f, -5.3f, 4.1f, -4.0f, 1.7f, -1.3f);
8334
+ path.AddBezier(1.7f, -1.3f, 0.6f, -0.5f, 0, -0.4f, 0, 1.2f);
8335
+ path.StartFigure(); path.AddLine(0, 3.3f, 0, 3.6f);
8336
+ }
8337
+ else if (semantic == "hand")
8338
+ {
8339
+ path.AddPolygon(new PointF[] { new PointF(-3.7f, 0), new PointF(-2.4f, -1),
8340
+ new PointF(-1, 0.4f), new PointF(-1, -4.2f), new PointF(0.6f, -4.2f),
8341
+ new PointF(0.6f, -0.8f), new PointF(1.8f, -1.4f), new PointF(3, -0.5f),
8342
+ new PointF(3.7f, 0.6f), new PointF(3.1f, 3.6f), new PointF(-0.2f, 3.9f), new PointF(-2, 2) });
8343
+ }
8344
+ else
8345
+ {
8346
+ path.AddLine(-2.1f, 0, 2.1f, 0);
8347
+ }
8348
+ path.Transform(matrix);
8349
+ using (Pen contrast = new Pen(Color.FromArgb(110, 8, 32, 89), 2.0f * s))
8350
+ using (Pen ink = new Pen(Color.FromArgb(semantic == "unknown" ? 180 : 248, 232, 247, 255), 1.0f * s))
8351
+ {
8352
+ contrast.LineJoin = ink.LineJoin = LineJoin.Round;
8353
+ contrast.StartCap = contrast.EndCap = ink.StartCap = ink.EndCap = LineCap.Round;
8354
+ g.DrawPath(contrast, path); g.DrawPath(ink, path);
8355
+ }
8356
+ }
8357
+ }
8358
+
8359
+ static void AddDoubleArrow(GraphicsPath path, float radius, float head)
8360
+ {
8361
+ path.AddLine(-radius, 0, radius, 0); path.StartFigure();
8362
+ path.AddLines(new PointF[] { new PointF(-radius + head, -head), new PointF(-radius, 0), new PointF(-radius + head, head) });
8363
+ path.StartFigure();
8364
+ path.AddLines(new PointF[] { new PointF(radius - head, -head), new PointF(radius, 0), new PointF(radius - head, head) });
8365
+ }
8366
+
8367
+ static void AddHourglass(GraphicsPath path, float x, float y, float width, float height)
8368
+ {
8369
+ path.AddLines(new PointF[] { new PointF(x - width, y - height), new PointF(x + width, y - height),
8370
+ new PointF(x - width, y + height), new PointF(x + width, y + height), new PointF(x - width, y - height) });
8371
+ }
8372
+ }
8373
+
8374
+ }
8375
+
8376
+ namespace DshComputerUse.Worker
8377
+ {
8378
+ public static class PointerVisibilityLease
8379
+ {
8380
+ const int Version = 1, Bytes = 128, ReadyOffset = 48, RestoredOffset = 52;
8381
+ const int LeaseMs = 500, RefreshMs = 100, StartupMs = 2000;
8382
+ const string Prefix = "Local\\DSH-CursorLease-";
8383
+ static readonly object Gate = new object();
8384
+ static Session current;
8385
+ static string fault;
8386
+ public static string Fault { get { lock (Gate) return fault; } }
8387
+ internal static long NowMs()
8388
+ { return (long)(System.Diagnostics.Stopwatch.GetTimestamp() * (1000.0 / System.Diagnostics.Stopwatch.Frequency)); }
8389
+
8390
+ sealed class Session
8391
+ {
8392
+ public System.IO.MemoryMappedFiles.MemoryMappedFile Map;
8393
+ public System.IO.MemoryMappedFiles.MemoryMappedViewAccessor View;
8394
+ public System.Diagnostics.Process Guardian;
8395
+ public bool Ready, Revoked, Requested, Finished;
8396
+ public long Sequence, LastRefresh;
8397
+ }
8398
+
8399
+ // Invoke ONLY after the overlay has been successfully presented at the
8400
+ // native hotspot. True means the ready guardian accepted a current lease,
8401
+ // not proof that pixels disappeared. Never blocks waiting for readiness.
8402
+ public static bool TryHide()
8403
+ {
8404
+ lock (Gate)
8405
+ {
8406
+ if (fault != null) return false;
8407
+ if (current != null && current.Finished) current = null;
8408
+ if (current == null)
8409
+ {
8410
+ current = new Session(); Session launch = current;
8411
+ System.Threading.ThreadPool.QueueUserWorkItem(delegate { StartAndMonitor(launch); });
8412
+ return false;
8413
+ }
8414
+ Session s = current;
8415
+ if (s.Revoked || !s.Ready || s.View == null) return false;
8416
+ long now = NowMs();
8417
+ if (!s.Requested || now - s.LastRefresh >= RefreshMs)
8418
+ {
8419
+ s.Requested = true; // Set before writing: unknown write outcome also needs cleanup.
8420
+ try { WriteRequest(s, true, false, now); s.LastRefresh = now; }
8421
+ catch (System.Exception e) { fault = "cursor lease write: " + e.Message; s.Revoked = true; return false; }
8422
+ }
8423
+ return true;
8424
+ }
8425
+ }
8426
+
8427
+ // A nonblocking release request. Guardian observes it within its next
8428
+ // progressing <=20 ms poll; it is not a synchronous compositor fence.
8429
+ public static void Show()
8430
+ {
8431
+ lock (Gate)
8432
+ {
8433
+ Session s = current;
8434
+ if (s == null || s.Finished || s.Revoked) return;
8435
+ s.Revoked = true;
8436
+ if (s.View != null)
8437
+ {
8438
+ try { WriteRequest(s, false, true, NowMs()); }
8439
+ catch (System.Exception e) { fault = "cursor lease release: " + e.Message; }
8440
+ }
8441
+ }
8442
+ }
8443
+ public static void Dispose() { Show(); }
8444
+
8445
+ static void WriteRequest(Session s, bool desired, bool revoked, long stamp)
8446
+ {
8447
+ long even = s.Sequence + 2;
8448
+ s.View.Write(16, even - 1); System.Threading.Thread.MemoryBarrier();
8449
+ s.View.Write(24, desired ? 1 : 0); s.View.Write(28, revoked ? 1 : 0); s.View.Write(32, stamp);
8450
+ System.Threading.Thread.MemoryBarrier(); s.View.Write(16, even); s.Sequence = even;
8451
+ }
8452
+ internal sealed class Request
8453
+ {
8454
+ public long Sequence, Stamp;
8455
+ public bool Desired, Revoked;
8456
+ public Request(long sequence, long stamp, bool desired, bool revoked)
8457
+ { Sequence = sequence; Stamp = stamp; Desired = desired; Revoked = revoked; }
8458
+ }
8459
+ internal static Request ReadRequest(System.IO.MemoryMappedFiles.MemoryMappedViewAccessor view)
8460
+ {
8461
+ long first = view.ReadInt64(16);
8462
+ if (first <= 0 || (first & 1) != 0) return null;
8463
+ System.Threading.Thread.MemoryBarrier();
8464
+ int desired = view.ReadInt32(24), revoked = view.ReadInt32(28); long stamp = view.ReadInt64(32);
8465
+ System.Threading.Thread.MemoryBarrier(); long second = view.ReadInt64(16);
8466
+ if (first != second || (desired != 0 && desired != 1) || (revoked != 0 && revoked != 1)) return null;
8467
+ return new Request(second, stamp, desired == 1, revoked == 1);
8468
+ }
8469
+
8470
+ static void StartAndMonitor(Session s)
8471
+ {
8472
+ System.IO.MemoryMappedFiles.MemoryMappedFile map = null;
8473
+ System.IO.MemoryMappedFiles.MemoryMappedViewAccessor view = null;
8474
+ System.Diagnostics.Process child = null;
8475
+ bool restoreNeeded = false, exitConfirmed = false, stopAttempted = false;
8476
+ try
8477
+ {
8478
+ string name = Prefix + System.Guid.NewGuid().ToString("N");
8479
+ using (System.Diagnostics.Process owner = System.Diagnostics.Process.GetCurrentProcess())
8480
+ {
8481
+ map = System.IO.MemoryMappedFiles.MemoryMappedFile.CreateNew(name, Bytes);
8482
+ view = map.CreateViewAccessor(0, Bytes);
8483
+ view.Write(0, Version); view.Write(4, owner.Id);
8484
+ long start = owner.StartTime.ToUniversalTime().Ticks; view.Write(8, start);
8485
+ lock (Gate)
8486
+ {
8487
+ s.Map = map; s.View = view;
8488
+ if (s.Revoked) return; // No child and no Mag API if released during startup.
8489
+ WriteRequest(s, false, false, NowMs());
8490
+ }
8491
+ var info = new System.Diagnostics.ProcessStartInfo(
8492
+ System.Reflection.Assembly.GetExecutingAssembly().Location,
8493
+ "--pointer-guardian " + name + " " + owner.Id + " " + start);
8494
+ info.UseShellExecute = false; info.CreateNoWindow = true;
8495
+ info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
8496
+ child = System.Diagnostics.Process.Start(info);
8497
+ if (child == null) throw new System.InvalidOperationException("guardian did not start");
8498
+ lock (Gate) s.Guardian = child;
8499
+ }
8500
+ long began = NowMs();
8501
+ while (!child.WaitForExit(20))
8502
+ {
8503
+ int status = view.ReadInt32(ReadyOffset);
8504
+ lock (Gate) s.Ready = status == 1 && !s.Revoked;
8505
+ if (status == 0 && NowMs() - began > StartupMs)
8506
+ {
8507
+ // Only our own Process handle is terminated. It was not
8508
+ // ready, so TryHide has never authorized a hide request.
8509
+ stopAttempted = true;
8510
+ exitConfirmed = ConfirmExit(delegate { return child.HasExited; }, child.Kill, child.WaitForExit);
8511
+ if (!exitConfirmed) throw new System.TimeoutException("guardian termination unconfirmed after 1000 ms");
8512
+ throw new System.TimeoutException("guardian startup timeout");
8513
+ }
8514
+ }
8515
+ exitConfirmed = true;
8516
+ lock (Gate)
8517
+ {
8518
+ s.Ready = false;
8519
+ restoreNeeded = s.Requested && view.ReadInt32(RestoredOffset) != 1;
8520
+ if (!s.Revoked) fault = "pointer guardian ended; native cursor fallback";
8521
+ }
8522
+ }
8523
+ catch (System.Exception e)
8524
+ {
8525
+ lock (Gate) { s.Ready = false; fault = "pointer guardian: " + e.Message; restoreNeeded = s.Requested; }
8526
+ // An alive guardian remains the sole hide writer. Never race an
8527
+ // emergency show against a process that can still issue hide.
8528
+ if (child != null && !exitConfirmed && !stopAttempted)
8529
+ {
8530
+ stopAttempted = true;
8531
+ exitConfirmed = ConfirmExit(delegate { return child.HasExited; }, child.Kill, child.WaitForExit);
8532
+ }
8533
+ if (!exitConfirmed)
8534
+ {
8535
+ restoreNeeded = false;
8536
+ if (child != null) lock (Gate) fault += "; guardian exit unconfirmed, emergency show suppressed and further leases disabled";
8537
+ }
8538
+ }
8539
+ finally
8540
+ {
8541
+ // Guardian has ended before emergency restoration and before a
8542
+ // subsequent session may start. Old cleanup cannot unhide a new lease.
8543
+ if (restoreNeeded && exitConfirmed) EmergencyShow();
8544
+ lock (Gate)
8545
+ {
8546
+ s.Ready = false; s.View = null; s.Map = null; s.Guardian = null;
8547
+ if (view != null) view.Dispose(); if (map != null) map.Dispose();
8548
+ if (child != null) child.Dispose(); s.Finished = true;
8549
+ }
8550
+ }
8551
+ }
8552
+
8553
+ // Bounded process shutdown, shared with fake process tests. Failure is
8554
+ // explicit: the caller must not race show against a possible late hide.
8555
+ internal static bool ConfirmExit(System.Func<bool> hasExited, System.Action kill, System.Func<int, bool> wait)
8556
+ {
8557
+ try
8558
+ {
8559
+ if (hasExited()) return true;
8560
+ try { kill(); } catch { return hasExited(); }
8561
+ return wait(1000) || hasExited();
8562
+ }
8563
+ catch { return false; }
8564
+ }
8565
+
8566
+ static void EmergencyShow()
8567
+ {
8568
+ var native = new NativeBackend(); bool initialized = false;
8569
+ try { initialized = native.Initialize(); if (!initialized || !native.Show(true)) lock (Gate) fault = "emergency cursor restore API failed"; }
8570
+ catch (System.Exception e) { lock (Gate) fault = "emergency cursor restore: " + e.Message; }
8571
+ finally { if (initialized) try { native.Uninitialize(); } catch { } }
8572
+ }
8573
+
8574
+ internal interface Backend { bool Initialize(); bool Show(bool visible); void Uninitialize(); }
8575
+ sealed class NativeBackend : Backend
8576
+ {
8577
+ [System.Runtime.InteropServices.DllImport("Magnification.dll")] static extern bool MagInitialize();
8578
+ [System.Runtime.InteropServices.DllImport("Magnification.dll")] static extern bool MagShowSystemCursor(bool show);
8579
+ [System.Runtime.InteropServices.DllImport("Magnification.dll")] static extern bool MagUninitialize();
8580
+ public bool Initialize() { return MagInitialize(); }
8581
+ public bool Show(bool visible) { return MagShowSystemCursor(visible); }
8582
+ public void Uninitialize() { MagUninitialize(); }
8583
+ }
8584
+ internal sealed class Outcome
8585
+ { public bool HideAttempted, RestoreConfirmed, Success; public string Reason; }
8586
+
8587
+ // Same loop is used in the actual guardian and deterministic fake-backend
8588
+ // tests. No wall-clock waiting or native API is required in those tests.
8589
+ internal static Outcome RunLease(Backend api, System.Func<Request> read,
8590
+ System.Func<long> now, System.Func<bool> ownerAlive, System.Action pause, System.Action ready)
8591
+ {
8592
+ var result = new Outcome(); bool initialized = false, hidden = false;
8593
+ long started = now(), latestSequence = 0, stamp = started; bool everRequested = false;
8594
+ try
8595
+ {
8596
+ initialized = api.Initialize();
8597
+ if (!initialized) { result.Reason = "initialize-failed"; return result; }
8598
+ ready();
8599
+ while (true)
8600
+ {
8601
+ if (!ownerAlive()) { result.Reason = "owner-exited"; break; }
8602
+ // Expiration is terminal even when a newer request has
8603
+ // arrived while this process was not scheduled.
8604
+ if (everRequested && now() - stamp >= LeaseMs) { result.Reason = "lease-expired"; break; }
8605
+ Request request = read(); long currentTime = now();
8606
+ if (request != null && request.Sequence > latestSequence)
8607
+ {
8608
+ latestSequence = request.Sequence;
8609
+ if (request.Revoked || (!request.Desired && everRequested)) { result.Reason = "released"; break; }
8610
+ if (request.Desired)
8611
+ {
8612
+ if (request.Stamp > currentTime || currentTime - request.Stamp >= LeaseMs)
8613
+ { result.Reason = "expired-request"; break; }
8614
+ everRequested = true; stamp = request.Stamp;
8615
+ if (!hidden)
8616
+ {
8617
+ // Re-read the current complete request before the
8618
+ // sole hide writer acts; queued old commands never apply.
8619
+ Request newest = read();
8620
+ if (newest == null || newest.Sequence != request.Sequence) { pause(); continue; }
8621
+ if (!ownerAlive() || now() - stamp >= LeaseMs) { result.Reason = "expired-before-hide"; break; }
8622
+ result.HideAttempted = true;
8623
+ if (!api.Show(false)) { result.Reason = "hide-failed"; break; }
8624
+ hidden = true;
8625
+ // A release concurrent with the API is processed
8626
+ // before sleeping or accepting another lease.
8627
+ Request after = read();
8628
+ if (after != null && after.Sequence > latestSequence && (after.Revoked || !after.Desired))
8629
+ { result.Reason = "released-during-hide"; break; }
8630
+ }
8631
+ }
8632
+ }
8633
+ if (everRequested && currentTime - stamp >= LeaseMs) { result.Reason = "lease-expired"; break; }
8634
+ if (!everRequested && currentTime - started >= StartupMs) { result.Reason = "unused"; break; }
8635
+ pause();
8636
+ }
8637
+ result.Success = result.Reason != "hide-failed";
8638
+ }
8639
+ catch (System.Exception e) { result.Reason = "fault: " + e.Message; }
8640
+ finally
8641
+ {
8642
+ if (initialized)
8643
+ {
8644
+ if (result.HideAttempted)
8645
+ {
8646
+ try { result.RestoreConfirmed = api.Show(true); }
8647
+ catch { result.RestoreConfirmed = false; }
8648
+ if (!result.RestoreConfirmed) result.Success = false;
8649
+ }
8650
+ try { api.Uninitialize(); } catch { result.Success = false; }
8651
+ }
8652
+ }
8653
+ return result;
8654
+ }
8655
+
8656
+ // Dispatch this at the FIRST line of worker Main, before Panic.Init,
8657
+ // hooks, global brake, protocol stdout, or any other worker lifecycle.
8658
+ public static int GuardianMain(string name, int ownerPid, long ownerStartTicks)
8659
+ {
8660
+ if (System.IntPtr.Size != 8 || name == null || !name.StartsWith(Prefix, System.StringComparison.Ordinal) ||
8661
+ name.Length != Prefix.Length + 32) return 2;
8662
+ try
8663
+ {
8664
+ using (System.Diagnostics.Process owner = System.Diagnostics.Process.GetProcessById(ownerPid))
8665
+ using (System.Diagnostics.Process self = System.Diagnostics.Process.GetCurrentProcess())
8666
+ using (var map = System.IO.MemoryMappedFiles.MemoryMappedFile.OpenExisting(name))
8667
+ using (var view = map.CreateViewAccessor(0, Bytes))
8668
+ {
8669
+ if (owner.StartTime.ToUniversalTime().Ticks != ownerStartTicks || owner.SessionId != self.SessionId ||
8670
+ view.ReadInt32(0) != Version || view.ReadInt32(4) != ownerPid || view.ReadInt64(8) != ownerStartTicks) return 3;
8671
+ Outcome result = RunLease(new NativeBackend(), delegate { return ReadRequest(view); }, NowMs,
8672
+ delegate { try { return !owner.HasExited; } catch { return false; } },
8673
+ delegate { System.Threading.Thread.Sleep(20); },
8674
+ delegate { view.Write(ReadyOffset, 1); });
8675
+ view.Write(RestoredOffset, result.RestoreConfirmed ? 1 : 0);
8676
+ System.Threading.Thread.MemoryBarrier(); view.Write(ReadyOffset, result.Success ? 2 : 3);
8677
+ return result.Success ? 0 : 4;
8678
+ }
8679
+ }
8680
+ catch { return 5; }
8681
+ }
8682
+ }
8683
+ }