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

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.
@@ -13,7 +13,7 @@
13
13
  // Safety model: the worker is a faithful actuator. Policy (approvals,
14
14
  // blacklists, masking) lives in the DSH plugin / MCP gate layers. Worker-side
15
15
  // guards: exact physical Ctrl+Esc for manual pause, event-driven human ownership,
16
- // and accepted-input cleanup. Physical activity yields until three seconds quiet.
16
+ // and accepted-input cleanup. Physical takeover during actuation yields until three seconds quiet.
17
17
 
18
18
  using System;
19
19
  using System.Collections.Generic;
@@ -24,23 +24,26 @@ using System.Drawing.Imaging;
24
24
  using System.IO;
25
25
  using System.Linq;
26
26
  using System.Runtime.InteropServices;
27
+ using System.Runtime.InteropServices.WindowsRuntime; // AsBuffer: byte[] -> WinRT IBuffer
27
28
  using System.Text;
28
29
  using System.Threading;
29
30
  using System.Web.Script.Serialization;
30
31
  using System.Windows.Automation;
31
32
  using System.Windows.Forms; // Clipboard (watch out: bare Point stays our own struct)
33
+ // Inbox OCR (zero external dependencies): the union Windows.winmd ships in lib/core/winmd.
34
+ using Windows.Media.Ocr;
35
+ using Windows.Graphics.Imaging;
32
36
 
33
37
  namespace DshComputerUse.Worker
34
38
  {
35
- // Monotonic, event-driven ownership. A brief touch yields immediately; two seconds
36
- // of continued input/holding enters waiting. Only three seconds with no held input
37
- // and no physical event returns control. Ordinary input never creates a STOP file.
39
+ // A live cycle observes physical holds, but only actual actuation can start a handoff.
40
+ // Once yielded, three quiet seconds with no held input returns control to the agent.
38
41
  public sealed class HumanInputState
39
42
  {
40
43
  public const int SustainedMs = 2000, IdleMs = 3000;
41
44
  readonly object sync = new object();
42
45
  readonly HashSet<int> held = new HashSet<int>();
43
- bool enabled, seen;
46
+ bool enabled, acting, seen;
44
47
  string phase = "idle", lastKind = "none";
45
48
  long first, last, revision, burst;
46
49
  public string Phase { get { lock (sync) { return phase; } } }
@@ -67,20 +70,38 @@ namespace DshComputerUse.Worker
67
70
  {
68
71
  if (enabled == on) return;
69
72
  enabled = on;
70
- if (!on) { phase = "idle"; return; }
71
- if (held.Count > 0 || (seen && now - last < IdleMs)) Start(now);
73
+ if (!on) { phase = "idle"; acting = false; }
74
+ }
75
+ }
76
+ public void SetActing(bool on, long now)
77
+ {
78
+ lock (sync)
79
+ {
80
+ acting = on;
81
+ // Do not replay earlier ordinary activity. A key/button still physically
82
+ // held does need ownership before we can inject a conflicting chord.
83
+ if (on && enabled && phase == "idle" && held.Count > 0)
84
+ { last = now; revision++; Start(now); }
72
85
  }
73
86
  }
74
87
  void Start(long now) { phase = "yielding"; first = now; if (!seen) last = now; burst++; }
75
- public void Record(int key, bool down, bool up, long now)
88
+ 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.
95
+ public void Record(int key, bool down, bool up, long now, bool deliberate)
76
96
  {
77
97
  lock (sync)
78
98
  {
79
99
  bool continued = held.Count > 0 || (seen && now - last < IdleMs);
80
100
  lastKind = key == 0 ? "pointer-or-wheel" : key <= 6 ? "button" : "keyboard";
81
101
  if (key != 0) { if (down) held.Add(key); if (up) held.Remove(key); }
82
- seen = true; last = now; revision++;
83
- if (!enabled) return;
102
+ seen = true; last = now;
103
+ if (!enabled || (phase == "idle" && !acting && !deliberate)) return;
104
+ revision++;
84
105
  if (phase == "idle" || !continued) Start(now);
85
106
  if (now - first >= SustainedMs) phase = "waiting";
86
107
  }
@@ -103,6 +124,68 @@ namespace DshComputerUse.Worker
103
124
  }
104
125
  }
105
126
 
127
+ // The UAC secure desktop (and the lock screen) is the ONE surface automation must never
128
+ // reach — Windows enforces that itself. The honest addition (human request 2026-11-04)
129
+ // is to NOTICE the switch, refuse to dispatch while it stands, tell the human exactly
130
+ // what to do, and auto-continue the moment the normal desktop is back. Detection is an
131
+ // OpenInputDesktop probe: while the secure desktop is up, an unprivileged thread cannot
132
+ // open the input desktop at all. Transitions are confirmed over consecutive misses so a
133
+ // single hiccup never pauses dispatch, and one success clears the state immediately.
134
+ public sealed class SecureDesktopWatch
135
+ {
136
+ public const int ConfirmMisses = 3;
137
+ readonly object sync = new object();
138
+ bool active; int misses;
139
+ public bool Active { get { lock (sync) { return active; } } }
140
+ /// Returns true only on a STATE TRANSITION; the caller audits and notifies those alone.
141
+ public bool Observe(bool reachable)
142
+ {
143
+ lock (sync)
144
+ {
145
+ if (reachable)
146
+ {
147
+ misses = 0;
148
+ if (!active) return false;
149
+ active = false;
150
+ return true; // secured -> normal: dispatch may resume
151
+ }
152
+ if (active) return false;
153
+ if (++misses < ConfirmMisses) return false;
154
+ active = true;
155
+ return true; // normal -> secured
156
+ }
157
+ }
158
+ }
159
+
160
+ // Sum only physical displacement, rebasing every injected cursor event. Agent
161
+ // motion cannot turn a 1-pixel hand tremor into a takeover. Back-and-forth jitter
162
+ // cancels rather than accumulating path length; a deliberate 16-pixel move yields.
163
+ public sealed class PointerHandoffTolerance
164
+ {
165
+ public const int DistancePx = 16, BurstGapMs = 250;
166
+ readonly object sync = new object();
167
+ bool seen;
168
+ int x, y;
169
+ long dx, dy, lastPhysical;
170
+ public void Reset(int px, int py)
171
+ { lock (sync) { seen = true; x = px; y = py; dx = dy = 0; lastPhysical = 0; } }
172
+ public bool Record(int px, int py, bool physical, bool acting, bool yielded, long now)
173
+ {
174
+ lock (sync)
175
+ {
176
+ long mx = seen ? (long)px - x : 0, my = seen ? (long)py - y : 0;
177
+ x = px; y = py; seen = true;
178
+ if (!physical) return false;
179
+ if (!acting && !yielded) { dx = dy = 0; lastPhysical = now; return false; }
180
+ if (mx == 0 && my == 0) return false;
181
+ if (yielded) return true;
182
+ if (now - lastPhysical > BurstGapMs) dx = dy = 0;
183
+ lastPhysical = now; dx += mx; dy += my;
184
+ return dx * dx + dy * dy >= DistancePx * DistancePx;
185
+ }
186
+ }
187
+ }
188
+
106
189
  // A temporary pause is an explicit, single-use claim. No clock grants permission.
107
190
  public sealed class RecoveryPermit
108
191
  {
@@ -177,7 +260,12 @@ namespace DshComputerUse.Worker
177
260
  if (!active || paused || exited) return null;
178
261
  lock (sync)
179
262
  {
180
- if (hwnd == 0 || pid == 0) return "no target has been activated in this cycle";
263
+ if (hwnd == 0 || pid == 0)
264
+ {
265
+ const string missing = "this worker has no confirmed target for the current cycle";
266
+ if (host || foreground == 0 || process == 0) return missing;
267
+ return missing + ". Observed foreground hwnd=" + foreground + ", pid=" + process;
268
+ }
181
269
  if (!targetExists) return "the activated target window no longer exists";
182
270
  if (host) return "the agent host is in front of the target window";
183
271
  if (hwnd != foreground || pid != process) return "the foreground no longer matches the activated target";
@@ -389,6 +477,13 @@ namespace DshComputerUse.Worker
389
477
  public static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, uint diFlags);
390
478
  [DllImport("user32.dll")]
391
479
  public static extern uint SendNotifyMessage(IntPtr hWnd, uint msg, IntPtr w, IntPtr l);
480
+ // Secure-desktop probe: while the UAC prompt (or the lock screen) is up, an unprivileged
481
+ // thread CANNOT open the input desktop — that refusal is the only reliable, permission-free
482
+ // witness that the machine switched to the surface automation must never touch.
483
+ [DllImport("user32.dll", SetLastError = true)]
484
+ public static extern IntPtr OpenInputDesktop(uint flags, bool inherit, uint access);
485
+ [DllImport("user32.dll")]
486
+ public static extern bool CloseDesktop(IntPtr h);
392
487
 
393
488
  // ---- low-level keyboard hook: the human's emergency brake ----
394
489
  public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
@@ -415,7 +510,7 @@ namespace DshComputerUse.Worker
415
510
  [StructLayout(LayoutKind.Sequential)]
416
511
  public struct KBDLLHOOKSTRUCT { public uint vkCode, scanCode, flags, time; public IntPtr dwExtraInfo; }
417
512
  [StructLayout(LayoutKind.Sequential)]
418
- public struct NativeMsg { public IntPtr hwnd; public uint message; public IntPtr wParam, lParam; public uint time; public int ptX, ptY; }
513
+ public struct NativeMsg { public IntPtr hwnd; public uint message; public IntPtr wParam, lParam; public uint time; public int ptX, ptY; public uint lPrivate; }
419
514
  public const int WH_KEYBOARD_LL = 13, WM_KEYDOWN_ = 0x0100, WM_SYSKEYDOWN_ = 0x0104,
420
515
  WM_KEYUP_ = 0x0101, WM_SYSKEYUP_ = 0x0105;
421
516
  // KBDLLHOOKSTRUCT.flags bit 4: the key did NOT come from the physical keyboard — some
@@ -455,7 +550,6 @@ namespace DshComputerUse.Worker
455
550
  [StructLayout(LayoutKind.Sequential)] struct SIZEL { public int cx, cy; }
456
551
  [StructLayout(LayoutKind.Sequential, Pack = 1)]
457
552
  struct BLENDFUNCTION { public byte BlendOp, BlendFlags, SourceConstantAlpha, AlphaFormat; }
458
- [StructLayout(LayoutKind.Sequential)] struct MSG { public IntPtr hwnd; public uint message; public IntPtr wParam, lParam; public uint time; public int ptX, ptY; }
459
553
 
460
554
  [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
461
555
  static extern IntPtr CreateWindowEx(int exStyle, string cls, string name, int style,
@@ -465,49 +559,77 @@ namespace DshComputerUse.Worker
465
559
  [DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
466
560
  [DllImport("user32.dll")] static extern IntPtr GetDC(IntPtr h);
467
561
  [DllImport("user32.dll")] static extern int ReleaseDC(IntPtr h, IntPtr dc);
468
- [DllImport("user32.dll")] static extern bool PeekMessage(out MSG m, IntPtr h, uint min, uint max, uint remove);
562
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool PeekMessage(out Native.NativeMsg m, IntPtr h, uint min, uint max, uint remove);
563
+ [DllImport("user32.dll")] static extern bool ValidateRect(IntPtr h, IntPtr rect);
469
564
  [DllImport("gdi32.dll")] static extern IntPtr CreateCompatibleDC(IntPtr dc);
470
565
  [DllImport("gdi32.dll")] static extern bool DeleteDC(IntPtr dc);
471
566
  [DllImport("gdi32.dll")] static extern IntPtr SelectObject(IntPtr dc, IntPtr o);
472
567
  [DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr o);
473
568
  [DllImport("gdi32.dll")] static extern IntPtr CreateDIBSection(IntPtr dc, ref Native.BITMAPINFO bmi, uint usage, out IntPtr bits, IntPtr section, uint offset);
474
- [DllImport("user32.dll")] static extern bool UpdateLayeredWindow(IntPtr h, IntPtr dst, ref POINTL pptDst, ref SIZEL psize,
569
+ [DllImport("user32.dll", SetLastError = true)] static extern bool UpdateLayeredWindow(IntPtr h, IntPtr dst, ref POINTL pptDst, ref SIZEL psize,
475
570
  IntPtr src, ref POINTL pptSrc, int colorKey, ref BLENDFUNCTION blend, int flags);
476
571
  [DllImport("user32.dll")] static extern int GetSystemMetrics(int i);
572
+ [DllImport("user32.dll")] static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);
477
573
 
478
574
  static Thread _thread;
479
- static volatile int _lastTouchMs; // 0 = never; else Environment.TickCount of the last actuation
480
- static volatile int _holdMs = 900;
575
+ static volatile bool _acting;
576
+ static volatile bool _preparing;
577
+ static volatile int _preparationStart;
578
+ static int _preparationSerial, _presentedPreparation, _completedPreparation;
579
+ static volatile string _rendererStage = "not-started", _rendererFault;
580
+ static volatile int _lastLoopTick, _lastPresentTick, _lastFailedError;
581
+ static int _frames, _presentAttempts, _presentSuccesses, _lastAttemptWant, _lastAttemptSerial;
582
+ static int _lastAttemptedPreparationSerial = -1, _lastFailedSerial = -1, _lastFailedWant, _rebuiltSerial = -1;
583
+ public static Dictionary<string, object> Health()
584
+ {
585
+ var thread = _thread;
586
+ int now = Environment.TickCount;
587
+ return Program.Dict("workerPid", System.Diagnostics.Process.GetCurrentProcess().Id,
588
+ "threadAlive", thread != null && thread.IsAlive, "stage", _rendererStage, "fault", _rendererFault,
589
+ "want", Want, "preparationSerial", Volatile.Read(ref _preparationSerial), "presentedSerial", Volatile.Read(ref _presentedPreparation),
590
+ "completedSerial", Volatile.Read(ref _completedPreparation),
591
+ "loopAgeMs", Volatile.Read(ref _frames) == 0 ? (object)null : unchecked((uint)(now - _lastLoopTick)),
592
+ "presentAttempts", Volatile.Read(ref _presentAttempts), "lastAttemptWant", Volatile.Read(ref _lastAttemptWant),
593
+ "lastAttemptSerial", Volatile.Read(ref _lastAttemptSerial), "presentSuccesses", Volatile.Read(ref _presentSuccesses),
594
+ "lastAttemptedPreparationSerial", Volatile.Read(ref _lastAttemptedPreparationSerial),
595
+ "lastSuccessAgeMs", Volatile.Read(ref _presentSuccesses) == 0 ? (object)null : unchecked((uint)(now - _lastPresentTick)),
596
+ "lastFailedSerial", Volatile.Read(ref _lastFailedSerial), "lastFailedWant", Volatile.Read(ref _lastFailedWant),
597
+ "lastWin32Error", Volatile.Read(ref _lastFailedSerial) < 0 ? (object)null : _lastFailedError,
598
+ "rebuiltSerial", Volatile.Read(ref _rebuiltSerial));
599
+ }
600
+ public const int PreparationMs = 1200; // human spec 2026-11-04: shortened from 1500 after review
481
601
  static volatile bool _enabled = true;
482
602
  static volatile string _label = "AI 正在操控此电脑 · 监听中";
483
603
 
484
- /// Mark "the agent is acting right now" — the glow stays lit for holdMs afterwards.
604
+ /// Legacy indicator requests announce monitoring; only real input can paint deep blue.
485
605
  public static void Touch(int holdMs)
486
606
  {
487
607
  if (!_enabled) return;
488
- _holdMs = holdMs < 200 ? 200 : holdMs;
489
- _lastTouchMs = Environment.TickCount;
608
+ _thinking = true;
490
609
  EnsureStarted();
491
610
  }
492
- public static void Off() { _lastTouchMs = 0; _thinking = false; }
611
+ public static void Off() { _acting = false; _preparing = false; _thinking = false; }
612
+ public static bool Acting { get { return _acting; } }
613
+ public static bool Preparing { get { return _preparing; } }
614
+ public static bool PreparationVisible { get { return _preparing && Volatile.Read(ref _presentedPreparation) == Volatile.Read(ref _preparationSerial); } }
615
+ 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; }
618
+ public static void Prepare(bool on)
619
+ {
620
+ if (!on) { _preparing = false; return; }
621
+ _preparationStart = 0;
622
+ Interlocked.Increment(ref _preparationSerial);
623
+ _preparing = true; _enabled = true; EnsureStarted();
624
+ }
493
625
 
494
- // ---- "INPUT IS HAPPENING RIGHT NOW" IS A FACT, NOT A GUESS (indicator bug, 2026-09-13) ----
495
- // Measured on the production build: a real move(moveDurationMs=3500) took 3823 ms, and the
496
- // deep-blue ACTING border had already expired at its fixed 1200 ms hold WHILE SendInput steps
497
- // were still being injected — pixels went blue at 1737 ms, then cyan/green at 2153/3438 ms with
498
- // moveStillPending:true on all eight frames. Nothing here lengthens the hold: the hold is
499
- // still 1200 ms. What changes is WHERE IT IS ANCHORED (the last real injection, not the moment
500
- // the op began) and that an injection SEQUENCE in flight is a state the light can read.
626
+ // Nested injection sequences stay observable across drag/key/typing gaps.
627
+ // The enclosing actual action scope controls ownership and the deep-blue cue.
501
628
  static int _injecting = 0; // refcount; Interlocked supplies the barriers
502
629
  public static void InputBegin() { Interlocked.Increment(ref _injecting); }
503
630
  public static void InputEnd() { if (Interlocked.Decrement(ref _injecting) < 0) _injecting = 0; }
504
631
  public static bool Injecting { get { return _injecting > 0; } }
505
- /// Re-anchor the ACTING light on the injection itself: called from the ONE function every real
506
- /// SendInput goes through, so a multi-step op (type/drag/scroll/key/animated move) stays blue
507
- /// for as long as its events keep coming, and still fades on the same short tail once they
508
- /// stop. The old behaviour anchored on the op's START, so any op longer than the hold went
509
- /// cyan mid-flight — the "not a long think, the input is still running" report.
510
- public static void KeepActing() { _lastTouchMs = Environment.TickCount; }
632
+ public static void KeepActing() { /* Actual operation scope owns the cue, with no trailing hold. */ }
511
633
 
512
634
  // Emergency stop must LOOK like one. The normal fade is a slow ease (~1.6 s) which,
513
635
  // during a panic, reads as "nothing happened" — so this snaps the overlay to zero on
@@ -515,7 +637,7 @@ namespace DshComputerUse.Worker
515
637
  static volatile bool _killNow = false;
516
638
  public static void Kill()
517
639
  {
518
- _lastTouchMs = 0; _thinking = false; _flashStart = 0;
640
+ _acting = false; _preparing = false; _thinking = false; _flashStart = 0;
519
641
  _killNow = true;
520
642
  }
521
643
 
@@ -567,7 +689,7 @@ namespace DshComputerUse.Worker
567
689
  public static int DeriveWant(bool flashOn, int committed)
568
690
  {
569
691
  if (Panic.Exited) return 0;
570
- return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (flashOn ? 2 : committed)));
692
+ return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (_preparing ? 7 : (Acting ? 1 : (flashOn ? 2 : committed)))));
571
693
  }
572
694
  /// The title states the same fact as the light, derived the same way (an ended session must
573
695
  /// not advertise "已暂停 · Ctrl+Alt+R 继续" in the window list).
@@ -575,7 +697,7 @@ namespace DshComputerUse.Worker
575
697
  {
576
698
  if (Panic.Exited) return "AI 已停止操控 · 未监听";
577
699
  return _askMode ? "AI 正在提问 · 在 DSH 聊天里回答 · 无倒计时(Ctrl+Alt+R 撤销)"
578
- : (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : label);
700
+ : (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : _preparing ? "即将开始操作,请暂停键鼠 · 1.2 秒准备" : label);
579
701
  }
580
702
  static volatile string _shownTitle = null;
581
703
  // What the indicator is ACTUALLY rendering right now, readable from outside via `probe`.
@@ -589,14 +711,9 @@ namespace DshComputerUse.Worker
589
711
  static extern bool SetWindowText(IntPtr h, string text);
590
712
  public static void Stopped(bool on)
591
713
  {
714
+ // Only the window owner sets the title (in Run). A physical brake must
715
+ // never wait for WM_SETTEXT to be serviced by the overlay thread.
592
716
  _stoppedMode = on;
593
- // The overlay's WINDOW TITLE is part of the text channel: renaming it means any
594
- // window list — the agent's own computer_windows, or a human's Task Manager —
595
- // states plainly that the session is stopped, with no screenshot required.
596
- if (_hwnd != IntPtr.Zero)
597
- {
598
- try { SetWindowText(_hwnd, on ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : _label); } catch { }
599
- }
600
717
  }
601
718
 
602
719
  /// Mark "the agent just captured the screen": a yellow border flashes around the
@@ -663,37 +780,90 @@ namespace DshComputerUse.Worker
663
780
 
664
781
  static void ThreadProc()
665
782
  {
666
- try { Run(); } catch { /* the indicator must never break actuation */ }
783
+ try { Run(); }
784
+ catch (Exception ex)
785
+ {
786
+ _rendererFault = _rendererStage + ": " + ex.Message;
787
+ Program.AuditRecord("GLOW-RENDER-ERROR", _rendererFault);
788
+ }
789
+ finally
790
+ {
791
+ _rendererStage = "stopped";
792
+ lock (typeof(Glow)) { _thread = null; }
793
+ }
794
+ }
795
+
796
+ static void PumpOverlayMessages(IntPtr hwnd)
797
+ {
798
+ _rendererStage = "message-pump";
799
+ const uint WM_PAINT = 0x000F;
800
+ Native.NativeMsg msg;
801
+ for (int i = 0; i < 32 && PeekMessage(out msg, hwnd, 0, 0, PM_REMOVE); i++)
802
+ {
803
+ // UpdateLayeredWindow provides all pixels. WM_PAINT stays pending
804
+ // until its update region is validated, even with PM_REMOVE.
805
+ if (msg.message == WM_PAINT) ValidateRect(msg.hwnd, IntPtr.Zero);
806
+ else Native.DispatchMessage(ref msg);
807
+ }
667
808
  }
668
809
 
669
810
  static void Run()
670
811
  {
812
+ while (RenderOverlay()) { } // Rebuild at most once per unpresented preparation.
813
+ }
814
+
815
+ static bool RenderOverlay()
816
+ {
817
+ _rendererFault = null;
818
+ _rendererStage = "display-metrics";
671
819
  int vx = GetSystemMetrics(Native.SM_XVIRTUALSCREEN);
672
820
  int vy = GetSystemMetrics(Native.SM_YVIRTUALSCREEN);
673
821
  int vw = GetSystemMetrics(Native.SM_CXVIRTUALSCREEN);
674
822
  int vh = GetSystemMetrics(Native.SM_CYVIRTUALSCREEN);
675
- if (vw <= 0 || vh <= 0) return;
823
+ if (vw <= 0 || vh <= 0) throw new InvalidOperationException("empty virtual desktop");
676
824
 
825
+ _rendererStage = "create-window";
677
826
  IntPtr hwnd = CreateWindowEx(
678
827
  WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TOPMOST,
679
828
  "STATIC", _label, WS_POPUP, vx, vy, vw, vh, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
680
- if (hwnd == IntPtr.Zero) return;
681
- _hwnd = hwnd;
682
-
683
- IntPtr screenDc = GetDC(IntPtr.Zero);
684
- IntPtr memDc = CreateCompatibleDC(screenDc);
685
- Native.BITMAPINFO bmi = new Native.BITMAPINFO();
686
- bmi.bmiHeader.biSize = (uint)Marshal.SizeOf(typeof(Native.BITMAPINFOHEADER));
687
- bmi.bmiHeader.biWidth = vw;
688
- bmi.bmiHeader.biHeight = -vh; // top-down
689
- bmi.bmiHeader.biPlanes = 1;
690
- bmi.bmiHeader.biBitCount = 32;
691
- bmi.bmiHeader.biCompression = 0;
692
- IntPtr bits;
693
- IntPtr dib = CreateDIBSection(screenDc, ref bmi, 0, out bits, IntPtr.Zero, 0);
694
- if (dib == IntPtr.Zero) { DeleteDC(memDc); ReleaseDC(IntPtr.Zero, screenDc); return; }
695
- IntPtr oldBmp = SelectObject(memDc, dib);
829
+ if (hwnd == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
830
+ IntPtr screenDc = IntPtr.Zero, memDc = IntPtr.Zero, dib = IntPtr.Zero, oldBmp = IntPtr.Zero;
831
+ try
832
+ {
833
+ TryExcludeOverlayFromCapture(hwnd);
834
+ _hwnd = hwnd;
835
+ _rendererStage = "create-surface";
836
+ screenDc = GetDC(IntPtr.Zero);
837
+ if (screenDc == IntPtr.Zero) throw new InvalidOperationException("GetDC failed");
838
+ memDc = CreateCompatibleDC(screenDc);
839
+ if (memDc == IntPtr.Zero) throw new InvalidOperationException("CreateCompatibleDC failed");
840
+ Native.BITMAPINFO bmi = new Native.BITMAPINFO();
841
+ bmi.bmiHeader.biSize = (uint)Marshal.SizeOf(typeof(Native.BITMAPINFOHEADER));
842
+ bmi.bmiHeader.biWidth = vw;
843
+ bmi.bmiHeader.biHeight = -vh; // top-down
844
+ bmi.bmiHeader.biPlanes = 1;
845
+ bmi.bmiHeader.biBitCount = 32;
846
+ IntPtr bits;
847
+ dib = CreateDIBSection(screenDc, ref bmi, 0, out bits, IntPtr.Zero, 0);
848
+ if (dib == IntPtr.Zero) throw new InvalidOperationException("CreateDIBSection failed");
849
+ oldBmp = SelectObject(memDc, dib);
850
+ if (oldBmp == IntPtr.Zero || oldBmp == new IntPtr(-1)) throw new InvalidOperationException("SelectObject failed");
851
+ _rendererStage = "rendering";
852
+ return RenderFrames(hwnd, screenDc, memDc, bits, vx, vy, vw, vh);
853
+ }
854
+ finally
855
+ {
856
+ _hwnd = IntPtr.Zero; _shownTitle = null;
857
+ if (oldBmp != IntPtr.Zero && oldBmp != new IntPtr(-1)) SelectObject(memDc, oldBmp);
858
+ if (dib != IntPtr.Zero) DeleteObject(dib);
859
+ if (memDc != IntPtr.Zero) DeleteDC(memDc);
860
+ if (screenDc != IntPtr.Zero) ReleaseDC(IntPtr.Zero, screenDc);
861
+ DestroyWindow(hwnd);
862
+ }
863
+ }
696
864
 
865
+ static bool RenderFrames(IntPtr hwnd, IntPtr screenDc, IntPtr memDc, IntPtr bits, int vx, int vy, int vw, int vh)
866
+ {
697
867
  POINTL dst = new POINTL(); dst.x = vx; dst.y = vy;
698
868
  POINTL src = new POINTL(); src.x = 0; src.y = 0;
699
869
  SIZEL size = new SIZEL(); size.cx = vw; size.cy = vh;
@@ -701,48 +871,32 @@ namespace DshComputerUse.Worker
701
871
  // ---- state machine with dwell hysteresis (no strobing) ----
702
872
  int curBitmap = 0; // 0 = none, 1 = blue border, 2 = yellow flash
703
873
  int paintedFlash = 0;
874
+ int lastPreparationPaint = 0;
704
875
  int alpha = 0;
705
876
  bool visible = false;
706
- int committed = 0; // 0 idle / 1 acting / 3 thinking, committed after a dwell
707
- int pending = -1;
708
- int pendingSince = 0;
877
+ int committed = 0;
709
878
  while (true)
710
879
  {
711
- // ACTING = input in flight (a sequence, across its sleeps) OR the short tail after the
712
- // last injection. Never "a longer guess": the hold is unchanged and the tail still ends.
713
- bool acting = _enabled && (Injecting ||
714
- (_lastTouchMs != 0 && unchecked(Environment.TickCount - _lastTouchMs) < _holdMs));
880
+ _lastLoopTick = Environment.TickCount;
881
+ _rendererStage = "rendering";
882
+ 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;
715
886
  bool thinking = ThinkingActive;
716
887
  bool flashOn = _flashStart != 0 && unchecked(Environment.TickCount - _flashStart) < _flashMs;
717
888
 
718
- int live = acting ? 1 : (thinking ? 3 : 0);
719
- // The flash is an OVERRIDE, never a state that lingers.
720
- //
721
- // 2026-09-13: the flash used to write `committed = 2`, so once it ended the dwell
722
- // hysteresis (600 ms) held the yellow bitmap on screen — with the border snapped to
723
- // full alpha that read as "the yellow frame is permanently lit", and the human lost
724
- // the ability to tell WHICH step took a screenshot. Now yellow shows only while the
725
- // flash is actually running (<=400 ms) and the border drops straight back to deep
726
- // blue when acting, or pale breathing when thinking.
727
- if (!flashOn)
728
- {
729
- if (live != committed)
730
- {
731
- // require a state to hold for 600 ms before switching: kills strobe
732
- if (pending != live) { pending = live; pendingSince = Environment.TickCount; }
733
- else if (unchecked(Environment.TickCount - pendingSince) >= 600) { committed = live; pending = -1; }
734
- }
735
- else { pending = -1; }
736
- }
889
+ committed = acting ? 1 : (thinking ? 3 : 0);
737
890
 
738
891
  // ONE derivation (Glow.DeriveWant): the exit flag outranks ask/stop/flash/committed,
739
892
  // so no later state change on another thread can paint a light on an ended session.
893
+ int preparationSerial = Volatile.Read(ref _preparationSerial);
740
894
  int want = DeriveWant(flashOn, committed);
741
895
  _want = want; // publish it: the state the light is showing, as a fact
742
896
 
743
897
  // Keep the overlay's WINDOW TITLE in sync with the state, from the render loop.
744
- // 2026-09-13: the title was only written by Stopped(), which runs BEFORE this thread
745
- // creates the window — so a process that INHERITED a stop (SyncFromFile at startup)
898
+ // When the title was only written by Stopped(), a process that INHERITED a stop
899
+ // before this thread created the window (SyncFromFile at startup)
746
900
  // showed a red border with the title "AI 正在操控此电脑". The title is the text
747
901
  // channel the agent and the human both read, so a lying title is worse than none.
748
902
  // It is derived from the same flags (Glow.DeriveTitle) and a hidden window still has
@@ -760,14 +914,14 @@ namespace DshComputerUse.Worker
760
914
  _killNow = false;
761
915
  alpha = 0;
762
916
  if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
763
- curBitmap = 0; committed = 0; pending = -1; paintedFlash = 0;
764
- MSG mk; while (PeekMessage(out mk, hwnd, 0, 0, PM_REMOVE)) { }
917
+ curBitmap = 0; committed = 0; paintedFlash = 0;
918
+ PumpOverlayMessages(hwnd);
765
919
  Thread.Sleep(12);
766
920
  continue;
767
921
  }
768
922
  // want: 0 idle | 1 acting (deep blue) | 3 thinking (pale blue) | 2 flash (yellow)
769
923
  // 4 emergency-stopped (RED, steady — the session is dead until re-armed)
770
- int needBitmap = want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : (want == 1 ? 3 : (want == 4 || want == 5 ? 4 : 1)));
924
+ int needBitmap = want == 7 ? 6 : want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : (want == 1 ? 3 : (want == 4 || want == 5 ? 4 : 1)));
771
925
  // A REPAINT MUST REACH THE SCREEN EVEN WHEN ALPHA DOES NOT MOVE.
772
926
  //
773
927
  // The present below used to be gated on `diff != 0` alone, while the flash snaps
@@ -778,7 +932,15 @@ namespace DshComputerUse.Worker
778
932
  // RED frame when the border happened to be at full alpha already — whether the
779
933
  // brake was visible was luck. Present when EITHER the bitmap or the alpha changed.
780
934
  bool repaint = false;
781
- if (needBitmap == 2 && paintedFlash != _flashStart) { PaintFlash(bits, vw, vh); paintedFlash = _flashStart; curBitmap = 2; alpha = 204; repaint = true; /* snap ON at 80%: a flash is an event, not a fade */ }
935
+ int preparationElapsed = -1;
936
+ if (want == 7 && (curBitmap != 6 || !PreparationVisible || unchecked(Environment.TickCount - lastPreparationPaint) >= 50))
937
+ {
938
+ lastPreparationPaint = Environment.TickCount;
939
+ preparationElapsed = PreparationVisible ? unchecked(lastPreparationPaint - _preparationStart) : 0;
940
+ PaintPreparation(bits, vw, vh, preparationElapsed);
941
+ curBitmap = 6; repaint = true;
942
+ }
943
+ else if (needBitmap == 2 && paintedFlash != _flashStart) { PaintFlash(bits, vw, vh); paintedFlash = _flashStart; curBitmap = 2; alpha = 204; repaint = true; /* snap ON at 80%: a flash is an event, not a fade */ }
782
944
  else if (needBitmap == 5 && curBitmap != 5) { PaintHumanYield(bits, vw, vh); curBitmap = 5; repaint = true; }
783
945
  else if (needBitmap == 4 && curBitmap != 4) { PaintStop(bits, vw, vh); curBitmap = 4; repaint = true; }
784
946
  else if (needBitmap == 3 && curBitmap != 3) { PaintBlue(bits, vw, vh, true); curBitmap = 3; repaint = true; }
@@ -790,7 +952,7 @@ namespace DshComputerUse.Worker
790
952
  int target;
791
953
  bool flashFrame = false;
792
954
  bool breathFrame = false; // breathing states are ABSOLUTE, never eased
793
- if (want == 0) target = 0;
955
+ if (want == 0) { target = 0; breathFrame = true; }
794
956
  else if (want == 2)
795
957
  {
796
958
  // The capture cue runs its OWN envelope (wind-up, burst, short fade): an absolute
@@ -798,7 +960,8 @@ namespace DshComputerUse.Worker
798
960
  target = FlashAlpha(unchecked(Environment.TickCount - _flashStart));
799
961
  flashFrame = true;
800
962
  }
801
- else if (want == 1) target = FULL;
963
+ else if (want == 1) { target = FULL; breathFrame = true; }
964
+ else if (want == 7) { target = 255; breathFrame = true; }
802
965
  else if (want == 4) target = FULL; // steady red, no breathing
803
966
  else if (want == 6)
804
967
  {
@@ -808,22 +971,23 @@ namespace DshComputerUse.Worker
808
971
  else if (want == 5)
809
972
  {
810
973
  // ASK: red like a stop, but at TWICE the cyan breath rate (1250 ms vs 2500 ms)
811
- // and FULL SWING 0..100, so the pulse is unmistakable at a glance.
974
+ // and a full swing — so the pulse is unmistakable at a glance. Human spec
975
+ // 2026-11-04, raised the same day after review: the first floor (25) still
976
+ // read as too dim at the trough; the whole band moved up to 50..140.
812
977
  double askPhase = (Environment.TickCount % 1250) / 1250.0;
813
- target = 50 + (int)(Math.Sin(askPhase * 2 * Math.PI) * 50);
978
+ target = 95 + (int)(Math.Sin(askPhase * 2 * Math.PI) * 45);
814
979
  breathFrame = true;
815
980
  }
816
981
  else
817
982
  {
818
983
  // Faint blue "thinking", breathing so a still screen still shows life.
819
984
  // 2.5 s cycle = 20% faster than the original 3.0 s.
820
- // FULL SWING 0..100 (human spec 2026-09-12). The old 58..104 band was chosen so
821
- // the trough never read as "the agent went away" — but between the damped band
822
- // and the easing below, the border barely moved at all, which is strictly worse:
823
- // a border that is always faintly there says nothing. 0 at the trough is the
824
- // point: the ring INHALES and goes dark, then comes back.
985
+ // Human spec 2026-11-04, raised the same day after review: the first floor
986
+ // (25) still read as "going dark" at the trough; the whole band moved up to
987
+ // 50..140 — the halo stays clearly visible at its dimmest while the breathing
988
+ // contrast (swing 90) is preserved.
825
989
  double phase = (Environment.TickCount % 2500) / 2500.0;
826
- target = 50 + (int)(Math.Sin(phase * 2 * Math.PI) * 50);
990
+ target = 95 + (int)(Math.Sin(phase * 2 * Math.PI) * 45);
827
991
  breathFrame = true;
828
992
  }
829
993
 
@@ -860,156 +1024,225 @@ namespace DshComputerUse.Worker
860
1024
  SetWindowPos(hwnd, new IntPtr(-1), vx, vy, vw, vh, 0x0004 | 0x0010 | 0x0040);
861
1025
  visible = true;
862
1026
  }
863
- UpdateLayeredWindow(hwnd, screenDc, ref dst, ref size, memDc, ref src, 0, ref bf, ULW_ALPHA);
1027
+ _rendererStage = "presenting";
1028
+ Volatile.Write(ref _lastAttemptWant, want);
1029
+ Volatile.Write(ref _lastAttemptSerial, preparationSerial);
1030
+ if (want == 7) Volatile.Write(ref _lastAttemptedPreparationSerial, preparationSerial);
1031
+ Interlocked.Increment(ref _presentAttempts);
1032
+ bool presented = UpdateLayeredWindow(hwnd, screenDc, ref dst, ref size, memDc, ref src, 0, ref bf, ULW_ALPHA);
1033
+ int nativeError = presented ? 0 : Marshal.GetLastWin32Error();
1034
+ if (presented)
1035
+ {
1036
+ _lastPresentTick = Environment.TickCount;
1037
+ Interlocked.Increment(ref _presentSuccesses);
1038
+ }
1039
+ else
1040
+ {
1041
+ bool firstFailure = Volatile.Read(ref _lastFailedSerial) != preparationSerial;
1042
+ _lastFailedError = nativeError;
1043
+ Volatile.Write(ref _lastFailedWant, want);
1044
+ Volatile.Write(ref _lastFailedSerial, preparationSerial);
1045
+ if (firstFailure) Program.AuditRecord("GLOW-PRESENT-FAILED", "serial=" + preparationSerial + "; want=" + want + "; win32=" + nativeError);
1046
+ if (want == 7 && !PreparationVisible && Volatile.Read(ref _rebuiltSerial) != preparationSerial)
1047
+ {
1048
+ Volatile.Write(ref _rebuiltSerial, preparationSerial);
1049
+ return true;
1050
+ }
1051
+ }
1052
+ if (presented && want == 7 && !PreparationVisible)
1053
+ {
1054
+ _preparationStart = Environment.TickCount;
1055
+ Volatile.Write(ref _presentedPreparation, preparationSerial);
1056
+ }
1057
+ if (presented && want == 7 && preparationElapsed >= PreparationMs)
1058
+ Volatile.Write(ref _completedPreparation, preparationSerial);
864
1059
  }
865
1060
  else if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
866
1061
  }
867
- MSG m;
868
- while (PeekMessage(out m, hwnd, 0, 0, PM_REMOVE)) { }
1062
+ PumpOverlayMessages(hwnd);
869
1063
  Thread.Sleep(16);
870
1064
  }
871
1065
  }
872
1066
 
873
- /// The agent's activity border, in two clearly different states.
874
- ///
875
- /// 2026-09-12 (human request): the two states no longer share a width, and they no longer
876
- /// share a hue. "thinking" is now a WIDER CYAN halo and "acting" a NARROWER DEEP BLUE one,
877
- /// so the pair reads unambiguously: an outer cyan ring breathing = the agent is thinking
878
- /// (it has not touched the machine yet), the inner steady blue = it is driving right now.
879
- /// The old design made them the same shape and the same hue at different densities, which
880
- /// is exactly the case where "is it doing something?" becomes a guess.
881
- ///
882
- /// Drawn as N concentric rounded-rect strokes whose alpha follows (1-t)^1.8 — a
883
- /// hand-rolled gradient guaranteed to reach alpha 0 at the INNER edge (a residual
884
- /// tint there read as a hard seam against near-black windows).
885
- static void PaintBlue(IntPtr bits, int w, int h, bool intense)
1067
+ // Thin, square screen-edge rails leave the application itself completely clear.
1068
+ // Each pixel belongs to exactly one inset ring: corners meet without gaps or
1069
+ // overlapping hot spots. Human request 2026-11-04, doubled the same day after
1070
+ // review: the bloom must span 1.6 cm of physical screen for every colour, not
1071
+ // the old fixed eight pixels (which read as a hairline on a 2560x1600 panel at
1072
+ // 175%), so the ring count below is derived from the overlay window's own DPI:
1073
+ // ~60 rings at 96 dpi, ~106 at 168.
1074
+ static int EdgeRailThickness()
1075
+ {
1076
+ // Offscreen painting (visual fixtures) has no window; GetDpiForWindow then
1077
+ // fails and the 96-dpi fallback keeps the fixture geometry deterministic.
1078
+ uint dpi = 96;
1079
+ IntPtr window = _hwnd;
1080
+ if (window != IntPtr.Zero)
1081
+ {
1082
+ uint live = Native.GetDpiForWindow(window);
1083
+ if (live > 0) dpi = live;
1084
+ }
1085
+ int rings = (int)Math.Round(1.6 / 2.54 * dpi); // 1.6 cm in this panel's pixels
1086
+ return Math.Max(8, Math.Min(rings, 192));
1087
+ }
1088
+
1089
+ // The fixed eight-ring ramp was {244,216,94,53,30,16,8,3}: two bright core
1090
+ // rings, then a falloff to near zero. Same profile here, stretched over the
1091
+ // DPI-derived width: ring 0-1 keep their core alpha, rings 2..n-1 decay
1092
+ // geometrically from 94 down to ~3 at the innermost ring.
1093
+ static int EdgeRailAlpha(int ring, int rings)
886
1094
  {
887
- WithGraphics(bits, w, h, delegate (System.Drawing.Graphics g)
888
- {
889
- // Width and intensity are both scaled from ONE place per state, and the layered
890
- // window's constant alpha does the intensity: perceived brightness = bitmap alpha x
891
- // layer alpha, so scaling BOTH would give 0.64 where the human asked for 0.8.
892
- // (2026-09-13: "all frames 80% as dense, 80% as wide".)
893
- // The CAP has to carry the width: at 1600p the cap is what binds, so a divisor
894
- // change alone moves nothing (min(46, 1600/26) == min(46, 1600/32) == 46).
895
- // acting : 37 px (80% of the original 46) — stays INSIDE the thinking ring
896
- // thinking: 52 px (~40% wider) — reaches OUTSIDE it
897
- int band = intense
898
- ? Math.Max(22, Math.Min(37, Math.Min(w, h) / 32))
899
- : Math.Max(30, Math.Min(52, Math.Min(w, h) / 23));
900
- int r = band + 10;
901
- int peak = intense ? 240 : 168;
902
- System.Drawing.Color col = intense
903
- ? System.Drawing.Color.FromArgb(255, 46, 132, 255) // deep, saturated blue
904
- : System.Drawing.Color.FromArgb(255, 34, 226, 240); // cyan halo (thinking)
905
- const int N = 60;
906
- float step = (float)band / N;
907
- for (int i = 0; i < N; i++)
1095
+ if (ring == 0) return 244;
1096
+ if (ring == 1) return 216;
1097
+ return (int)Math.Round(94 * Math.Pow(3.0 / 94.0, (double)(ring - 2) / (rings - 3)));
1098
+ }
1099
+
1100
+ static void PaintEdgeRail(System.Drawing.Graphics g, Rectangle bounds, Color tint)
1101
+ {
1102
+ int rings = EdgeRailThickness();
1103
+ var smoothing = g.SmoothingMode;
1104
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
1105
+ try
1106
+ {
1107
+ for (int i = 0; i < rings; i++)
908
1108
  {
909
- double t = (double)i / N;
910
- int a = (int)Math.Round(peak * Math.Pow(1 - t, intense ? 2.0 : 1.8));
911
- if (a <= 0) continue;
912
- float inset = i * step;
913
- using (System.Drawing.Pen pen = new System.Drawing.Pen(
914
- System.Drawing.Color.FromArgb(a, col.R, col.G, col.B), step + 1.2f))
1109
+ int alpha = EdgeRailAlpha(i, rings);
1110
+ int x = bounds.Left + i, y = bounds.Top + i;
1111
+ int width = bounds.Width - 2 * i, height = bounds.Height - 2 * i;
1112
+ if (width <= 0 || height <= 0) break;
1113
+ Color start = Color.FromArgb(alpha, Math.Min(255, tint.R + 18), Math.Min(255, tint.G + 14), Math.Min(255, tint.B + 8));
1114
+ Color end = Color.FromArgb(alpha, tint.R, tint.G, tint.B);
1115
+ using (var brush = new System.Drawing.Drawing2D.LinearGradientBrush(
1116
+ new RectangleF(bounds.X, bounds.Y, Math.Max(1, bounds.Width), Math.Max(1, bounds.Height)), start, end, 32f))
915
1117
  {
916
- System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
917
- AddRound(path, inset, inset, w - inset * 2, h - inset * 2, Math.Max(1, r - inset));
918
- g.DrawPath(pen, path);
919
- path.Dispose();
1118
+ g.FillRectangle(brush, x, y, width, 1);
1119
+ if (height > 1) g.FillRectangle(brush, x, y + height - 1, width, 1);
1120
+ if (height > 2)
1121
+ {
1122
+ g.FillRectangle(brush, x, y + 1, 1, height - 2);
1123
+ if (width > 1) g.FillRectangle(brush, x + width - 1, y + 1, 1, height - 2);
1124
+ }
920
1125
  }
921
1126
  }
922
- PaintEscBadge(g, intense ? 238 : 148);
1127
+ }
1128
+ finally { g.SmoothingMode = smoothing; }
1129
+ }
1130
+
1131
+ static Rectangle OverlayDisplay(int w, int h)
1132
+ {
1133
+ var display = new Rectangle(0, 0, w, h);
1134
+ if (w == Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN) && h == Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN))
1135
+ {
1136
+ display = Screen.PrimaryScreen.Bounds;
1137
+ display.Offset(-Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), -Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN));
1138
+ }
1139
+ return display;
1140
+ }
1141
+
1142
+ // WDA_EXCLUDEFROMCAPTURE must not be sent on older Windows, where its fallback
1143
+ // can black out the covered image. Unsupported APIs simply leave the overlay visible.
1144
+ static void TryExcludeOverlayFromCapture(IntPtr hwnd)
1145
+ {
1146
+ try
1147
+ {
1148
+ object build = Microsoft.Win32.Registry.GetValue(
1149
+ @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "CurrentBuildNumber", null);
1150
+ int number;
1151
+ if (int.TryParse(Convert.ToString(build), out number) && number >= 19041)
1152
+ SetWindowDisplayAffinity(hwnd, 0x00000011);
1153
+ }
1154
+ catch { /* display preference only; never changes control or capture behavior */ }
1155
+ }
1156
+
1157
+ static void PaintBlue(IntPtr bits, int w, int h, bool intense)
1158
+ {
1159
+ WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
1160
+ PaintEdgeRail(g, new Rectangle(0, 0, w, h), intense
1161
+ ? Color.FromArgb(49, 116, 248) : Color.FromArgb(36, 206, 216));
923
1162
  });
924
1163
  }
925
1164
 
926
- /// Semi-transparent badge in the top-left corner. The human must be able to SEE that a
927
- /// brake exists (and whether it is currently on) — a brake nobody can see feels exactly
928
- /// like no brake at all.
929
1165
  public static int HumanYieldAlpha(int tick)
930
- { return 48 + (int)Math.Round(30 * (1 - Math.Cos((unchecked((uint)tick) % 2800) * 2 * Math.PI / 2800.0))); }
1166
+ { return 90 + (int)Math.Round(35 * (1 - Math.Cos((unchecked((uint)tick) % 2800) * 2 * Math.PI / 2800.0))); }
931
1167
  static void PaintHumanYield(IntPtr bits, int w, int h)
932
1168
  {
933
1169
  WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
934
- int band = Math.Max(20, Math.Min(40, Math.Min(w, h) / 30));
935
- for (int i = 0; i < 48; i++)
936
- {
937
- float inset = (float)i * band / 48;
938
- int alpha = (int)(175 * Math.Pow(1 - i / 48.0, 1.8));
939
- using (var pen = new Pen(Color.FromArgb(alpha, 255, 231, 151), band / 48f + 1.2f))
940
- using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
941
- { AddRound(shape, inset, inset, w - 2 * inset, h - 2 * inset, Math.Max(1, band + 8 - inset)); g.DrawPath(pen, shape); }
942
- }
943
- PaintBadge(g, "已让出键鼠 · 静默 3 秒后继续 · Ctrl+Esc 暂停", 175, false);
1170
+ Color tint = Color.FromArgb(238, 190, 102);
1171
+ PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
1172
+ PaintStatusChip(g, w, h, "已让出键鼠 · 静默 3 秒后接续", tint);
944
1173
  });
945
1174
  }
946
1175
 
947
- static void PaintEscBadge(System.Drawing.Graphics g, int opacity)
1176
+ static void PaintPreparation(IntPtr bits, int w, int h, int elapsed)
948
1177
  {
949
- PaintBadge(g, "监听中 · Ctrl+Esc 暂停 · Ctrl+Alt+Q 退出", opacity, false);
1178
+ WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
1179
+ Color tint = Color.FromArgb(166, 139, 250);
1180
+ PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
1181
+ Rectangle display = OverlayDisplay(w, h);
1182
+ float scale = Math.Max(1f, Math.Min(1.25f, display.Width / 1920f));
1183
+ float bw = Math.Min(408 * scale, display.Width - 24), bh = 64 * scale;
1184
+ float x = display.Left + (display.Width - bw) / 2, y = display.Top + 10 * scale;
1185
+ float progress = Math.Max(0f, Math.Min(1f, elapsed / (float)PreparationMs));
1186
+ using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
1187
+ using (var fill = new System.Drawing.Drawing2D.LinearGradientBrush(new RectangleF(x, y, bw, bh),
1188
+ Color.FromArgb(224, 23, 27, 39), Color.FromArgb(224, 35, 31, 53), 0f))
1189
+ using (var outline = new Pen(Color.FromArgb(110, tint), 1f))
1190
+ using (var title = new Font("Microsoft YaHei UI", 12.5f * scale, FontStyle.Bold))
1191
+ using (var detail = new Font("Microsoft YaHei UI", 9f * scale))
1192
+ using (var white = new SolidBrush(Color.FromArgb(255, 246, 247, 253)))
1193
+ using (var soft = new SolidBrush(Color.FromArgb(238, 183, 188, 207)))
1194
+ using (var ring = new Pen(Color.FromArgb(70, tint), 2 * scale))
1195
+ using (var accent = new Pen(Color.FromArgb(245, tint), 2 * scale))
1196
+ using (var track = new SolidBrush(Color.FromArgb(52, tint)))
1197
+ using (var bar = new System.Drawing.Drawing2D.LinearGradientBrush(
1198
+ new RectangleF(x + 48 * scale, y + 56 * scale, bw - 64 * scale, 2 * scale),
1199
+ Color.FromArgb(220, 167, 139, 250), Color.FromArgb(235, 96, 165, 250), 0f))
1200
+ {
1201
+ AddRound(shape, x, y, bw, bh, 14 * scale); g.FillPath(fill, shape); g.DrawPath(outline, shape);
1202
+ g.DrawEllipse(ring, x + 16 * scale, y + 20 * scale, 20 * scale, 20 * scale);
1203
+ if (progress > 0) g.DrawArc(accent, x + 16 * scale, y + 20 * scale, 20 * scale, 20 * scale, -90, 360 * progress);
1204
+ g.DrawString("即将开始操作,请暂停键鼠", title, white, x + 48 * scale, y + 9 * scale);
1205
+ g.DrawString("Ctrl+Esc 暂停", detail, soft, x + 48 * scale, y + 34 * scale);
1206
+ string remaining = (Math.Max(0, PreparationMs - elapsed) / 1000.0).ToString("0.0") + " 秒后开始";
1207
+ SizeF size = g.MeasureString(remaining, detail);
1208
+ g.DrawString(remaining, detail, soft, x + bw - 16 * scale - size.Width, y + 34 * scale);
1209
+ g.FillRectangle(track, x + 48 * scale, y + 56 * scale, bw - 64 * scale, 2 * scale);
1210
+ if (progress > 0) g.FillRectangle(bar, x + 48 * scale, y + 56 * scale, (bw - 64 * scale) * progress, 2 * scale);
1211
+ }
1212
+ });
950
1213
  }
951
1214
 
952
- static void PaintBadge(System.Drawing.Graphics g, string txt, int opacity, bool red)
1215
+ static void PaintStatusChip(System.Drawing.Graphics g, int w, int h, string text, Color tint)
953
1216
  {
954
- // The human asked for a QUIETER badge (2026-09-13): they already know what the overlay
955
- // means, so it must inform without shouting. Font at 75% (15 -> 11.25 pt), and text,
956
- // outline and background all pulled well below full opacity.
957
- int o = Math.Max(40, Math.Min(255, opacity));
958
- using (System.Drawing.Font f = new System.Drawing.Font("Microsoft YaHei", 11.25f, System.Drawing.FontStyle.Bold))
1217
+ Rectangle display = OverlayDisplay(w, h);
1218
+ float scale = Math.Max(1f, Math.Min(1.25f, display.Width / 1920f));
1219
+ using (var font = new Font("Microsoft YaHei UI", 9f * scale))
959
1220
  {
960
- System.Drawing.SizeF sz = g.MeasureString(txt, f);
961
- float bw = sz.Width + 24f, bh = sz.Height + 12f;
962
- float x = 22f, y = 22f;
963
- System.Drawing.Drawing2D.GraphicsPath p = new System.Drawing.Drawing2D.GraphicsPath();
964
- AddRound(p, x, y, bw, bh, 10f);
965
- System.Drawing.Color edge = red
966
- ? System.Drawing.Color.FromArgb((int)(o * 0.42), 255, 150, 150)
967
- : System.Drawing.Color.FromArgb((int)(o * 0.34), 150, 200, 255);
968
- using (System.Drawing.SolidBrush b = new System.Drawing.SolidBrush(
969
- red ? System.Drawing.Color.FromArgb((int)(o * 0.30), 46, 8, 12)
970
- : System.Drawing.Color.FromArgb((int)(o * 0.26), 10, 14, 22)))
971
- g.FillPath(b, p);
972
- using (System.Drawing.Pen pen = new System.Drawing.Pen(edge, 1.4f))
973
- g.DrawPath(pen, p);
974
- p.Dispose();
975
- using (System.Drawing.SolidBrush tb = new System.Drawing.SolidBrush(
976
- red ? System.Drawing.Color.FromArgb((int)(o * 0.70), 255, 226, 226)
977
- : System.Drawing.Color.FromArgb((int)(o * 0.62), 236, 245, 255)))
978
- g.DrawString(txt, f, tb, x + 12f, y + 6f);
1221
+ SizeF measured = g.MeasureString(text, font);
1222
+ float bw = Math.Min(measured.Width + 38 * scale, display.Width - 24), bh = 28 * scale;
1223
+ float x = display.Left + (display.Width - bw) / 2, y = display.Top + 8 * scale;
1224
+ using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
1225
+ using (var fill = new SolidBrush(Color.FromArgb(222, 22, 26, 36)))
1226
+ using (var outline = new Pen(Color.FromArgb(90, tint), 1f))
1227
+ using (var accent = new SolidBrush(Color.FromArgb(255, tint)))
1228
+ using (var ink = new SolidBrush(Color.FromArgb(250, 235, 239, 248)))
1229
+ {
1230
+ AddRound(shape, x, y, bw, bh, 12 * scale); g.FillPath(fill, shape); g.DrawPath(outline, shape);
1231
+ g.FillEllipse(accent, x + 12 * scale, y + 11 * scale, 6 * scale, 6 * scale);
1232
+ g.DrawString(text, font, ink, x + 24 * scale, y + (bh - measured.Height) / 2);
1233
+ }
979
1234
  }
980
1235
  }
981
1236
 
982
- /// The stopped state: a RED border plus a red badge, steady (no breathing) — the
983
- /// computer-use session is dead until a human re-arms it.
984
1237
  static void PaintStop(IntPtr bits, int w, int h)
985
1238
  {
986
- WithGraphics(bits, w, h, delegate (System.Drawing.Graphics g)
987
- {
988
- int band = Math.Max(22, Math.Min(37, Math.Min(w, h) / 32)); // 80% of what it was (cap included)
989
- int r = band + 10;
990
- const int N = 60;
991
- float step = (float)band / N;
992
- for (int i = 0; i < N; i++)
993
- {
994
- double t = (double)i / N;
995
- int a = (int)Math.Round(248 * Math.Pow(1 - t, 2.0));
996
- if (a <= 0) continue;
997
- float inset = i * step;
998
- using (System.Drawing.Pen pen = new System.Drawing.Pen(
999
- System.Drawing.Color.FromArgb(a, 238, 66, 74), step + 1.2f))
1000
- {
1001
- System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
1002
- AddRound(path, inset, inset, w - inset * 2, h - inset * 2, Math.Max(1, r - inset));
1003
- g.DrawPath(pen, path);
1004
- path.Dispose();
1005
- }
1006
- }
1007
- PaintBadge(g, "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出", 252, true);
1239
+ WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
1240
+ Color tint = Color.FromArgb(240, 84, 102);
1241
+ PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
1242
+ PaintStatusChip(g, w, h, "操作已暂停 · Ctrl+Alt+R 继续", tint);
1008
1243
  });
1009
1244
  }
1010
1245
 
1011
- /// Yellow "a screenshot was just taken" rectangle around the captured region —
1012
- /// same stepped-gradient treatment, no strokes, no hard edges.
1013
1246
  static void PaintFlash(IntPtr bits, int w, int h)
1014
1247
  {
1015
1248
  int fx = _fx, fy = _fy, fw = _fw, fh = _fh;
@@ -1019,32 +1252,8 @@ namespace DshComputerUse.Worker
1019
1252
  if (fx + fw > w) fw = w - fx;
1020
1253
  if (fy + fh > h) fh = h - fy;
1021
1254
  if (fw < 8 || fh < 8) { fx = 0; fy = 0; fw = w; fh = h; }
1022
-
1023
- WithGraphics(bits, w, h, delegate (System.Drawing.Graphics g)
1024
- {
1025
- // 2026-09-12 (human): the capture cue on its own, 40% narrower again — 32 -> 19.
1026
- // The corner radius scales with it (13 -> 8) so the strip still reads as a thin
1027
- // glow instead of a fat rounded rectangle. Width only: the alpha profile is
1028
- // untouched, so the cue keeps its brightness.
1029
- int band = 19;
1030
- int r = 8;
1031
- const int N = 40;
1032
- float step = (float)band / N;
1033
- for (int i = 0; i < N; i++)
1034
- {
1035
- double t = (double)i / N;
1036
- int a = (int)Math.Round(200 * Math.Pow(1 - t, 1.9));
1037
- if (a <= 0) continue;
1038
- float inset = i * step;
1039
- using (System.Drawing.Pen pen = new System.Drawing.Pen(
1040
- System.Drawing.Color.FromArgb(a, 255, 186, 20), step + 1.2f))
1041
- {
1042
- System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
1043
- AddRound(path, fx + inset, fy + inset, fw - inset * 2, fh - inset * 2, Math.Max(1, r - inset));
1044
- g.DrawPath(pen, path);
1045
- path.Dispose();
1046
- }
1047
- }
1255
+ WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
1256
+ PaintEdgeRail(g, new Rectangle(fx, fy, fw, fh), Color.FromArgb(247, 193, 62));
1048
1257
  });
1049
1258
  }
1050
1259
 
@@ -1056,6 +1265,7 @@ namespace DshComputerUse.Worker
1056
1265
  using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp))
1057
1266
  {
1058
1267
  g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
1268
+ g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
1059
1269
  g.Clear(System.Drawing.Color.Transparent);
1060
1270
  draw(g);
1061
1271
  }
@@ -1232,10 +1442,12 @@ namespace DshComputerUse.Worker
1232
1442
  // separate statements, so a Q between the check and the write re-lit a dead cycle.
1233
1443
  lock (SyncLock)
1234
1444
  {
1445
+ Program.InvalidateActiveInput();
1235
1446
  _temporary = null;
1236
1447
  _engaged = false;
1237
1448
  _stopped = false;
1238
1449
  CycleLit = false; // R2: the cycle is dead and must not be revived by anyone
1450
+ _controlsReleased = true;
1239
1451
  Disarm(); // ... and its monitors die WITH it, not at the next turn
1240
1452
  try { Glow.Kill(); } catch { }
1241
1453
  try { Glow.Ask(false); } catch { }
@@ -1258,6 +1470,7 @@ namespace DshComputerUse.Worker
1258
1470
  {
1259
1471
  if (_exited) return false;
1260
1472
  if (!CycleLit) Program.ResetKickoffFocus();
1473
+ _controlsReleased = false;
1261
1474
  CycleLit = true; // an agent cycle is live: cyan is on (R1)
1262
1475
  Arm(); // "Computer use 一旦启用,立即挂打断监听"
1263
1476
  // A BRAKE OUTRANKS "THINKING" (cold-start bug, 2026-09-13): if a stop is already
@@ -1281,6 +1494,19 @@ namespace DshComputerUse.Worker
1281
1494
  return true;
1282
1495
  }
1283
1496
  }
1497
+ public static bool TryEndCycle()
1498
+ {
1499
+ SyncExitedFromFile(); SyncFromFile();
1500
+ lock (SyncLock)
1501
+ {
1502
+ if (_engaged || _stopped || PauseRequested || ExitRequested) return false;
1503
+ _controlsReleased = true;
1504
+ CycleLit = false;
1505
+ Disarm(); Glow.Kill();
1506
+ Program.ReleaseOwnedInputs(); Program.ReleaseDesktopOwnership();
1507
+ return true;
1508
+ }
1509
+ }
1284
1510
 
1285
1511
  /// Was the record written after this process started? Only then is it news rather than an
1286
1512
  /// inherited fact. (The record carries its own `at=` line precisely so this is answerable
@@ -1355,7 +1581,7 @@ namespace DshComputerUse.Worker
1355
1581
  }
1356
1582
  // OUTSIDE the lock: Notify writes to stdout, and a full pipe must never hold the lock the
1357
1583
  // exit and the cycle-opener share.
1358
- if (adopted) ReleaseStuck(); // adopted exit == detected exit, cleanup included
1584
+ if (adopted) { ReleaseStuck(); Program.ReleaseDesktopOwnership(); }
1359
1585
  if (announce) Notify("exit", ExitSummary());
1360
1586
  }
1361
1587
 
@@ -1451,6 +1677,9 @@ namespace DshComputerUse.Worker
1451
1677
  static IntPtr _hook = IntPtr.Zero;
1452
1678
  static Native.LowLevelKeyboardProc _proc; // keep alive: the GC must not collect it
1453
1679
  static IntPtr _mouseHook = IntPtr.Zero;
1680
+ static readonly uint CurrentPid = (uint)Process.GetCurrentProcess().Id;
1681
+ static readonly ManualResetEvent _hooksInitialized = new ManualResetEvent(false);
1682
+ static volatile bool _hooksReady;
1454
1683
  static Native.LowLevelMouseProc _mouseProc; // same: the GC must not collect it
1455
1684
  static Thread _thread;
1456
1685
 
@@ -1720,9 +1949,14 @@ namespace DshComputerUse.Worker
1720
1949
  _proc = HookCallback;
1721
1950
  IntPtr hmod = Native.GetModuleHandle(null);
1722
1951
  _hook = Native.SetWindowsHookEx(Native.WH_KEYBOARD_LL, _proc, hmod, 0);
1952
+ int keyboardError = _hook == IntPtr.Zero ? Marshal.GetLastWin32Error() : 0;
1723
1953
  // Same thread, same message loop: a second low-level hook for the wheel.
1724
1954
  _mouseProc = MouseHookCallback;
1725
1955
  _mouseHook = Native.SetWindowsHookEx(Native.WH_MOUSE_LL, _mouseProc, hmod, 0);
1956
+ int mouseError = _mouseHook == IntPtr.Zero ? Marshal.GetLastWin32Error() : 0;
1957
+ _hooksReady = _hook != IntPtr.Zero && _mouseHook != IntPtr.Zero;
1958
+ _hooksInitialized.Set();
1959
+ if (!_hooksReady) Program.AuditRecord("CONTROL-HOOK-ERROR", "keyboard=" + keyboardError + "; mouse=" + mouseError);
1726
1960
  Native.NativeMsg msg;
1727
1961
  while (Native.GetMessage(out msg, IntPtr.Zero, 0, 0) > 0)
1728
1962
  {
@@ -1730,13 +1964,33 @@ namespace DshComputerUse.Worker
1730
1964
  Native.DispatchMessage(ref msg);
1731
1965
  }
1732
1966
  }
1733
- catch { /* the brake must never take the worker down */ }
1967
+ catch (Exception ex) { Program.AuditRecord("CONTROL-HOOK-ERROR", ex.GetType().Name); }
1968
+ finally { _hooksReady = false; _hooksInitialized.Set(); }
1969
+ }
1970
+
1971
+ public static void RequireControlHooks()
1972
+ {
1973
+ if (!_hooksInitialized.WaitOne(1500) || !_hooksReady)
1974
+ {
1975
+ var error = new Exception("CONTROL_HOOK_UNAVAILABLE: physical input monitoring did not start or has stopped; no input was dispatched.");
1976
+ error.Data["code"] = "CONTROL_HOOK_UNAVAILABLE";
1977
+ error.Data["outcome"] = "not-dispatched";
1978
+ throw error;
1979
+ }
1734
1980
  }
1735
1981
 
1736
1982
  // Hook callbacks only record ownership and enqueue control work. Serial execution
1737
1983
  // preserves Ctrl+Esc / resume / exit ordering without blocking the OS hook on I/O.
1738
- static int _pendingPauses;
1984
+ static int _pendingPauses, _pendingExits;
1739
1985
  public static bool PauseRequested { get { return Volatile.Read(ref _pendingPauses) > 0; } }
1986
+ public static bool ExitRequested { get { return Volatile.Read(ref _pendingExits) > 0; } }
1987
+ public static bool FinishOneShotControls()
1988
+ {
1989
+ if (!PauseRequested && !ExitRequested) return true;
1990
+ // A one-shot process must give the recognised chord a chance to persist
1991
+ // STOP/EXITED before Main returns and its background threads disappear.
1992
+ return SpinWait.SpinUntil(delegate { return !PauseRequested && !ExitRequested; }, 5000);
1993
+ }
1740
1994
  static readonly ConcurrentQueue<Action> ControlQueue = new ConcurrentQueue<Action>();
1741
1995
  static int _controlWorkQueued;
1742
1996
  internal static void QueueControl(Action action)
@@ -1751,6 +2005,31 @@ namespace DshComputerUse.Worker
1751
2005
  } while (!ControlQueue.IsEmpty && Interlocked.CompareExchange(ref _controlWorkQueued, 1, 0) == 0);
1752
2006
  });
1753
2007
  }
2008
+ static void QueueDesktopControl(Action action, Action finished = null)
2009
+ {
2010
+ QueueControl(delegate {
2011
+ // Serialize with graceful release: a queued physical hotkey may not
2012
+ // reopen a completed cycle or steer an exit into its continuing chat.
2013
+ try
2014
+ {
2015
+ lock (SyncLock)
2016
+ {
2017
+ if (!ControlListenersEnabled || !Program.TryClaimDesktop("physical control"))
2018
+ { Program.AuditRecord("CONTROL-SKIPPED", ControlListenersEnabled ? "desktop owned by another worker" : "cycle released"); return; }
2019
+ try { action(); }
2020
+ finally { if (!CycleLit) Program.ReleaseDesktopOwnership(); }
2021
+ }
2022
+ }
2023
+ catch (Exception ex) { Program.AuditRecord("CONTROL-ERROR", ex.GetType().Name); }
2024
+ finally { if (finished != null) finished(); }
2025
+ });
2026
+ }
2027
+
2028
+ static void QueueExit(Action exit)
2029
+ {
2030
+ Interlocked.Increment(ref _pendingExits);
2031
+ QueueDesktopControl(exit, delegate { Interlocked.Decrement(ref _pendingExits); });
2032
+ }
1754
2033
  public static string ControlHotkey(uint vk, bool down, bool human, bool ctrl, bool alt, bool shift, bool win, bool live)
1755
2034
  {
1756
2035
  if (!down || !human) return "";
@@ -1760,8 +2039,7 @@ namespace DshComputerUse.Worker
1760
2039
  return "";
1761
2040
  }
1762
2041
  static bool _suppressPauseEsc;
1763
- static bool _havePhysicalPoint;
1764
- static POINT _physicalPoint;
2042
+ static readonly PointerHandoffTolerance PointerMotion = new PointerHandoffTolerance();
1765
2043
  public static bool PhysicalEvent(IntPtr extra, uint flags, uint injectedMask)
1766
2044
  { return extra != OwnMagic && (flags & injectedMask) == 0; }
1767
2045
 
@@ -1773,15 +2051,18 @@ namespace DshComputerUse.Worker
1773
2051
  {
1774
2052
  var m = (Native.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(Native.MSLLHOOKSTRUCT));
1775
2053
  if (m.dwExtraInfo != OwnMagic) Interlocked.Increment(ref _externalInput);
1776
- if (PhysicalEvent(m.dwExtraInfo, m.flags, 3))
2054
+ bool physical = PhysicalEvent(m.dwExtraInfo, m.flags, 3);
2055
+ int message = wParam.ToInt32();
2056
+ // Significant idle motion invalidates observations, but does not yield.
2057
+ bool movement = message == 0x200 && PointerMotion.Record(m.pt.X, m.pt.Y, physical,
2058
+ true, Handoff.Active, HumanClock.ElapsedMilliseconds);
2059
+ if (physical)
1777
2060
  {
1778
- int message = wParam.ToInt32(), key = 0;
1779
- bool down = false, up = false, activity = true;
2061
+ int key = 0;
2062
+ bool down = false, up = false, activity = true, deliberate = false;
1780
2063
  switch (message)
1781
2064
  {
1782
- case 0x200:
1783
- activity = !_havePhysicalPoint || m.pt.X != _physicalPoint.X || m.pt.Y != _physicalPoint.Y;
1784
- _physicalPoint = m.pt; _havePhysicalPoint = true; break;
2065
+ case 0x200: activity = movement; deliberate = movement; break;
1785
2066
  case 0x201: key = 1; down = true; break;
1786
2067
  case 0x202: key = 1; up = true; break;
1787
2068
  case 0x204: key = 2; down = true; break;
@@ -1793,7 +2074,7 @@ namespace DshComputerUse.Worker
1793
2074
  case 0x20A: case 0x20E: activity = unchecked((short)(m.mouseData >> 16)) != 0; break;
1794
2075
  default: activity = false; break;
1795
2076
  }
1796
- if (activity) RecordHumanInput(key, down, up);
2077
+ if (activity) RecordHumanInput(key, down, up, deliberate);
1797
2078
  }
1798
2079
  }
1799
2080
  }
@@ -1820,28 +2101,26 @@ namespace DshComputerUse.Worker
1820
2101
  { if (up) _suppressPauseEsc = false; return new IntPtr(1); }
1821
2102
  bool ctrl = _humanCtrl && (Native.GetAsyncKeyState(Native.VK_CONTROL) & 0x8000) != 0;
1822
2103
  bool alt = _humanAlt && (Native.GetAsyncKeyState(Native.VK_MENU) & 0x8000) != 0;
1823
- string hotkey = ControlHotkey(k.vkCode, down, human, ctrl, alt,
1824
- Handoff.Held(0x10), Handoff.Held(0x5B) || Handoff.Held(0x5C), CycleLit && !_exited);
2104
+ string hotkey = ControlListenersEnabled ? ControlHotkey(k.vkCode, down, human, ctrl, alt,
2105
+ Handoff.Held(0x10), Handoff.Held(0x5B) || Handoff.Held(0x5C), CycleLit && !_exited) : "";
1825
2106
  if (hotkey == "pause")
1826
2107
  {
1827
2108
  _suppressPauseEsc = true;
1828
- Interlocked.Increment(ref _pendingPauses);
1829
2109
  // The hook marks ownership immediately; do not perform SendInput or
1830
2110
  // filesystem work on this OS hook thread.
1831
- string evidence = ChordEvidence(k, ctrl, alt);
1832
- QueueControl(delegate {
1833
- try { Engage("Ctrl+Esc chord detected", false, "hook", evidence); }
1834
- finally { Interlocked.Decrement(ref _pendingPauses); }
1835
- });
2111
+ var evidence = ChordEvidence(k, ctrl, alt);
2112
+ Interlocked.Increment(ref _pendingPauses);
2113
+ QueueDesktopControl(delegate { Engage("Ctrl+Esc chord detected", false, "hook", evidence()); },
2114
+ delegate { Interlocked.Decrement(ref _pendingPauses); });
1836
2115
  return new IntPtr(1); // Ctrl+Esc must not also open the Windows Start menu.
1837
2116
  }
1838
2117
  if (hotkey == "exit")
1839
2118
  {
1840
- string evidence = ChordEvidence(k, ctrl, alt);
1841
- QueueControl(delegate { Exit("Ctrl+Alt+Q chord detected", "hook", evidence); });
2119
+ var evidence = ChordEvidence(k, ctrl, alt);
2120
+ QueueExit(delegate { Exit("Ctrl+Alt+Q chord detected", "hook", evidence()); });
1842
2121
  }
1843
2122
  else if (hotkey == "resume")
1844
- QueueControl(delegate { Clear(); });
2123
+ QueueDesktopControl(delegate { Clear(); });
1845
2124
  }
1846
2125
  }
1847
2126
  }
@@ -1886,6 +2165,7 @@ namespace DshComputerUse.Worker
1886
2165
  // SendInputChecked (which now refuses after an exit): releasing what is stuck is exactly
1887
2166
  // the cleanup that must still be allowed.
1888
2167
  ReleaseStuck();
2168
+ Program.ReleaseDesktopOwnership();
1889
2169
  Program.AuditRecord("EXIT", _exitSource + "\t" + why + "\t" + _exitEvidence);
1890
2170
  Notify("exit", ExitSummary());
1891
2171
  }
@@ -1893,7 +2173,7 @@ namespace DshComputerUse.Worker
1893
2173
  /// The chord as it was SEEN: the fields someone can re-check, including whose Ctrl/Alt the
1894
2174
  /// hook recorded and which window was in front. Deliberately verbose — this line is the
1895
2175
  /// difference between "the human pressed it" and "we cannot know who pressed it".
1896
- static string ChordEvidence(Native.KBDLLHOOKSTRUCT k, bool ctrl, bool alt)
2176
+ static Func<string> ChordEvidence(Native.KBDLLHOOKSTRUCT k, bool ctrl, bool alt)
1897
2177
  {
1898
2178
  StringBuilder sb = new StringBuilder();
1899
2179
  sb.Append("vk=0x").Append(k.vkCode.ToString("X2"));
@@ -1907,31 +2187,37 @@ namespace DshComputerUse.Worker
1907
2187
  // later whether "the human pressed it" was ever measurable — and note that OURSELVES
1908
2188
  // (extra=OUR_MAGIC) is a different, stronger answer than "some injector".
1909
2189
  sb.Append(" injected=").Append((k.flags & Native.LLKHF_INJECTED) != 0 ? "1" : "0");
1910
- try
2190
+ IntPtr fg = Native.GetForegroundWindow();
2191
+ uint fgPid;
2192
+ Native.GetWindowThreadProcessId(fg, out fgPid);
2193
+ sb.Append(" fgPid=").Append(fgPid);
2194
+ // Capture the foreground identity on the hook thread. Enrich it on the
2195
+ // control queue without sending a message to our own overlay window.
2196
+ return delegate
1911
2197
  {
1912
- IntPtr fg = Native.GetForegroundWindow();
1913
- uint fgPid;
1914
- Native.GetWindowThreadProcessId(fg, out fgPid);
1915
- sb.Append(" fgPid=").Append(fgPid);
1916
- try { sb.Append(" fg=").Append(Process.GetProcessById((int)fgPid).ProcessName); } catch { }
1917
- StringBuilder t = new StringBuilder(256);
1918
- if (Native.GetWindowText(fg, t, 256) > 0)
2198
+ try
1919
2199
  {
1920
- string title = t.ToString();
1921
- if (title.Length > 60) title = title.Substring(0, 60);
1922
- sb.Append(" fgTitle=\"").Append(title.Replace("\t", " ")).Append('"');
2200
+ using (var proc = Process.GetProcessById((int)fgPid)) sb.Append(" fg=").Append(proc.ProcessName);
2201
+ uint currentPid;
2202
+ Native.GetWindowThreadProcessId(fg, out currentPid);
2203
+ StringBuilder t = new StringBuilder(256);
2204
+ if (currentPid == fgPid && fgPid != CurrentPid && Native.GetWindowText(fg, t, 256) > 0)
2205
+ {
2206
+ string title = t.ToString();
2207
+ if (title.Length > 60) title = title.Substring(0, 60);
2208
+ sb.Append(" fgTitle=\"").Append(title.Replace("\t", " ")).Append('"');
2209
+ }
1923
2210
  }
1924
- }
1925
- catch { }
1926
- return sb.ToString();
2211
+ catch { }
2212
+ return sb.ToString();
2213
+ };
1927
2214
  }
1928
2215
 
1929
2216
  /// Polling fallback for the human's combos.
1930
2217
  ///
1931
2218
  /// The low-level hook is the primary path, but it only reports what Windows hands it: a
1932
- /// missed keydown would leave the machine stopped with no discoverable way back. This thread
1933
- /// independently watches the key STATE — the "pressed since the last call" bit cannot be
1934
- /// missed when polled every 40 ms — so Q always exits and R always releases, hook or no hook.
2219
+ /// missed keydown would otherwise have no fallback. Poll the press bits as an additional
2220
+ /// witness for Q/R while the hook has confirmed human-held modifiers.
1935
2221
  static void PollThread()
1936
2222
  {
1937
2223
  bool prevQ = false, prevR = false;
@@ -1963,21 +2249,19 @@ namespace DshComputerUse.Worker
1963
2249
  bool r = (Native.GetAsyncKeyState(Native.VK_R) & 1) != 0;
1964
2250
  bool agentQuiet = _agentActAt == 0 || unchecked(Environment.TickCount - _agentActAt) > 400;
1965
2251
  if (warm < 3) warm++;
1966
- else if (_humanCtrl && _humanAlt && asyncCtrl && asyncAlt && agentQuiet)
2252
+ else if (ControlListenersEnabled && _humanCtrl && _humanAlt && asyncCtrl && asyncAlt && agentQuiet)
1967
2253
  {
1968
2254
  // R FIRST: Ctrl+Alt+R is the human's explicit re-arm, and a stale Q bit must
1969
2255
  // never beat it — pressing the key that brings computer use BACK used to be
1970
2256
  // able to END it instead.
1971
- if (r && !prevR) QueueControl(delegate { Clear(); });
2257
+ if (r && !prevR) QueueDesktopControl(delegate { Clear(); });
1972
2258
  else if (q && !prevQ)
1973
2259
  {
1974
2260
  Native.KBDLLHOOKSTRUCT kk = new Native.KBDLLHOOKSTRUCT();
1975
2261
  kk.vkCode = Native.VK_Q;
1976
- string evidence =
1977
- "q-latch + hook-tracked modifiers + measured async key state " +
1978
- "(no keydown was seen by the hook); " +
1979
- ChordEvidence(kk, asyncCtrl, asyncAlt);
1980
- QueueControl(delegate { Exit("Ctrl+Alt+Q chord detected", "poll", evidence); });
2262
+ var evidence = ChordEvidence(kk, asyncCtrl, asyncAlt);
2263
+ QueueExit(delegate { Exit("Ctrl+Alt+Q chord detected", "poll",
2264
+ "q-latch + hook-tracked modifiers + measured async key state; " + evidence()); });
1981
2265
  }
1982
2266
  }
1983
2267
  prevQ = q; prevR = r;
@@ -1993,6 +2277,8 @@ namespace DshComputerUse.Worker
1993
2277
  // durable manual brake and never creates a STOP record.
1994
2278
  static int _agentActAt;
1995
2279
  static volatile bool _armed = false;
2280
+ static volatile bool _controlsReleased = true;
2281
+ public static bool ControlListenersEnabled { get { return !_controlsReleased; } }
1996
2282
  public static volatile bool CycleLit;
1997
2283
  public static bool Armed { get { return _armed; } }
1998
2284
  public static bool Exited { get { return _exited; } }
@@ -2000,27 +2286,77 @@ namespace DshComputerUse.Worker
2000
2286
  static readonly HumanInputState Handoff = new HumanInputState();
2001
2287
  static readonly object HandoffPublishLock = new object();
2002
2288
  static string _publishedHandoff = "idle";
2289
+ static long _physicalInputEpoch, _publishedInputEpoch;
2003
2290
  static int _handoffWorkQueued;
2004
2291
  public static bool HumanActive { get { return Handoff.Active; } }
2005
2292
  public static long HumanRevision { get { return Handoff.Revision; } }
2006
2293
  public static bool HumanHolds(int vk) { return Handoff.Held(vk); }
2007
- public static object CooperationState() { return Handoff.Snapshot(HumanClock.ElapsedMilliseconds); }
2294
+ public static long InputEpoch { get { return Interlocked.Read(ref _physicalInputEpoch); } }
2295
+ public static object CooperationState()
2296
+ {
2297
+ var state = (Dictionary<string, object>)Handoff.Snapshot(HumanClock.ElapsedMilliseconds);
2298
+ state["inputEpoch"] = InputEpoch; state["mouseTolerancePx"] = PointerHandoffTolerance.DistancePx;
2299
+ return state;
2300
+ }
2008
2301
  static void ResetMonitorRun() { /* Manual recovery must not erase physical input history. */ }
2302
+ // ---------- secure-desktop (UAC / lock screen) dispatch gate, human request 2026-11-04 ----------
2303
+ static readonly SecureDesktopWatch SecureWatch = new SecureDesktopWatch();
2304
+ public static bool SecureDesktopActive { get { return SecureWatch.Active; } }
2305
+ static bool InputDesktopReachable()
2306
+ {
2307
+ IntPtr desktop = Native.OpenInputDesktop(0, false, 0x0001); // DESKTOP_READOBJECT
2308
+ if (desktop != IntPtr.Zero) { Native.CloseDesktop(desktop); return true; }
2309
+ return false;
2310
+ }
2311
+ /// Polled from the monitor thread. Audits and notifies TRANSITIONS only.
2312
+ static void PollSecureDesktop()
2313
+ {
2314
+ if (!SecureWatch.Observe(InputDesktopReachable())) return;
2315
+ bool active = SecureWatch.Active;
2316
+ Program.AuditRecord(active ? "SECURE-DESKTOP-ENGAGED" : "SECURE-DESKTOP-CLEARED",
2317
+ active
2318
+ ? "input desktop unreachable (UAC prompt or lock screen); input dispatch paused until it returns"
2319
+ : "input desktop reachable again; dispatch may continue");
2320
+ Notify("secure", active ? "active" : "cleared");
2321
+ }
2322
+ /// Input ops refuse to dispatch while the secure desktop stands: no injection can reach
2323
+ /// it anyway, and a clean refusal plus a notified human beats a blind timeout mid-cycle.
2324
+ public static void RequireNormalDesktop()
2325
+ {
2326
+ if (!SecureWatch.Active) return;
2327
+ var error = new Exception("SECURE_DESKTOP_ACTIVE: the Windows secure desktop (a UAC prompt or the lock screen) is up; no input was dispatched. Ask the human to answer the prompt; dispatch resumes automatically once the normal desktop returns.");
2328
+ error.Data["code"] = "SECURE_DESKTOP_ACTIVE";
2329
+ error.Data["outcome"] = "not-dispatched";
2330
+ throw error;
2331
+ }
2009
2332
  public static void Arm() { _armed = true; Handoff.Enable(CycleLit && !_exited, HumanClock.ElapsedMilliseconds); }
2010
2333
  public static void Disarm() { _armed = false; Handoff.Enable(false, HumanClock.ElapsedMilliseconds); PublishHandoff(); }
2011
2334
  public static void AgentActed() { _agentActAt = Environment.TickCount; }
2335
+ public static void BeginActuation()
2336
+ {
2337
+ POINT point; if (Native.GetCursorPos(out point)) PointerMotion.Reset(point.X, point.Y);
2338
+ Glow.ActuationBegin();
2339
+ Handoff.SetActing(true, HumanClock.ElapsedMilliseconds);
2340
+ }
2341
+ public static void EndActuation()
2342
+ {
2343
+ Handoff.SetActing(false, HumanClock.ElapsedMilliseconds);
2344
+ Glow.ActuationEnd();
2345
+ }
2012
2346
  static bool IsBareModifier(uint vk)
2013
2347
  {
2014
2348
  return vk == Native.VK_SHIFT || vk == Native.VK_CONTROL || vk == Native.VK_MENU ||
2015
2349
  vk == 0xA0 || vk == 0xA1 || vk == 0xA2 || vk == 0xA3 ||
2016
2350
  vk == 0xA4 || vk == 0xA5 || vk == 0x5B || vk == 0x5C;
2017
2351
  }
2018
- public static void RecordHumanInput(int key, bool down, bool up)
2352
+ public static void RecordHumanInput(int key, bool down, bool up) { RecordHumanInput(key, down, up, false); }
2353
+ public static void RecordHumanInput(int key, bool down, bool up, bool deliberate)
2019
2354
  {
2355
+ Interlocked.Increment(ref _physicalInputEpoch);
2020
2356
  long now = HumanClock.ElapsedMilliseconds;
2021
2357
  bool was = Handoff.Active;
2022
2358
  Handoff.Enable(_armed && CycleLit && !_exited, now);
2023
- Handoff.Record(key, down, up, now);
2359
+ Handoff.Record(key, down, up, now, deliberate);
2024
2360
  if (!was && Handoff.Active && Interlocked.CompareExchange(ref _handoffWorkQueued, 1, 0) == 0)
2025
2361
  ThreadPool.QueueUserWorkItem(delegate {
2026
2362
  try { Program.ReleaseOwnedInputs(); PublishHandoff(); }
@@ -2032,16 +2368,22 @@ namespace DshComputerUse.Worker
2032
2368
  lock (HandoffPublishLock)
2033
2369
  {
2034
2370
  string phase = Handoff.Phase;
2035
- if (phase == _publishedHandoff) return;
2371
+ long epoch = InputEpoch;
2372
+ if (phase == _publishedHandoff && epoch == _publishedInputEpoch) return;
2373
+ bool changed = phase != _publishedHandoff;
2036
2374
  _publishedHandoff = phase;
2375
+ _publishedInputEpoch = epoch;
2376
+ if (!ControlListenersEnabled && !CycleLit && phase == "idle") return;
2037
2377
  if (phase != "idle") Glow.HumanYield();
2038
- Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + HumanRevision + "; kind=" + Handoff.LastKind);
2378
+ if (changed) Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + HumanRevision + "; kind=" + Handoff.LastKind);
2039
2379
  Notify("handoff", phase);
2040
2380
  }
2041
2381
  }
2042
2382
  static void MonitorHuman()
2043
2383
  {
2384
+ if (!ControlListenersEnabled) return;
2044
2385
  SyncExitedFromFile();
2386
+ PollSecureDesktop();
2045
2387
  long now = HumanClock.ElapsedMilliseconds;
2046
2388
  Handoff.Enable(_armed && CycleLit && !_exited, now);
2047
2389
  Handoff.Tick(now);
@@ -2371,7 +2713,7 @@ namespace DshComputerUse.Worker
2371
2713
  static readonly HashSet<string> ReadOnlyOps = new HashSet<string> {
2372
2714
  "selftest", "cursor", "panic", "resume", "recover", "echoargs", "ping", "screens",
2373
2715
  "windows", "uia", "uiaFromPoint", "uiaFocused", "waitForIdle", "clipRead",
2374
- "capture", "annotate", "waitStable", "waitChange", "focus", "frameSig", "probe",
2716
+ "capture", "ocr", "annotate", "waitStable", "waitChange", "focus", "frameSig", "probe",
2375
2717
  // diagnostics that must be able to ANSWER while a brake is up: they send no input and
2376
2718
  // light nothing, which is exactly when someone needs to inspect the release decision
2377
2719
  "keyClass", "stuckPlan",
@@ -2459,6 +2801,7 @@ namespace DshComputerUse.Worker
2459
2801
  try { Glow.Stopped(false); } catch { }
2460
2802
  // Re-arming re-baselines the human monitor: the travel that tripped the brake must not
2461
2803
  // still be sitting in the accumulator, or the very next poll would brake again.
2804
+ if (CycleLit && !_exited) Glow.Thinking(0);
2462
2805
  _agentActAt = 0;
2463
2806
  }
2464
2807
  if (was) Notify("resume", "");
@@ -2648,12 +2991,16 @@ namespace DshComputerUse.Worker
2648
2991
  bool finished;
2649
2992
  ClipboardLease(IClipboardAccess access, List<ClipboardBytes> previous, uint written)
2650
2993
  { this.access = access; this.previous = previous; this.written = written; }
2994
+ /// Returns null when the previous clipboard cannot be preserved (private formats, odd
2995
+ /// sizes). The caller then downgrades to a plain overwrite-paste with a warning rather
2996
+ /// than refusing — see Snapshot's incident note (2026-11-04).
2651
2997
  public static ClipboardLease Begin(IClipboardAccess access, string text)
2652
2998
  {
2653
2999
  using (var session = access.Open())
2654
3000
  {
2655
3001
  if (session.Sequence == 0) throw new Exception("Clipboard sequence unavailable; clipboard was not changed.");
2656
- var previous = session.Snapshot(); // refuse unsupported formats BEFORE clearing anything
3002
+ var previous = session.Snapshot(); // null = cannot preserve: caller downgrades, we do not clear anything here
3003
+ if (previous == null) return null;
2657
3004
  try
2658
3005
  {
2659
3006
  session.Replace(new List<ClipboardBytes> {
@@ -2692,8 +3039,9 @@ namespace DshComputerUse.Worker
2692
3039
  }
2693
3040
  }
2694
3041
 
2695
- // Copy only explicitly supported HGLOBAL formats. Unknown/private or GDI formats are refused
2696
- // without mutation; callers can use Unicode input instead of losing an opaque clipboard item.
3042
+ // Copy only explicitly supported HGLOBAL formats. Unknown/private or GDI formats cannot be
3043
+ // snapshotted — Snapshot returns null for them and the caller downgrades to
3044
+ // paste-without-restore with a warning (incident 2026-11-04) instead of losing the paste.
2697
3045
  public sealed class NativeClipboardAccess : IClipboardAccess, IDisposable
2698
3046
  {
2699
3047
  readonly NativeWindow window = new NativeWindow();
@@ -2717,14 +3065,6 @@ namespace DshComputerUse.Worker
2717
3065
  [DllImport("kernel32.dll")] static extern UIntPtr GlobalSize(IntPtr memory);
2718
3066
  [DllImport("kernel32.dll")] static extern IntPtr GlobalFree(IntPtr memory);
2719
3067
  [DllImport("kernel32.dll")] static extern void SetLastError(uint error);
2720
- static Exception Unavailable(string reason)
2721
- {
2722
- var error = new Exception("CLIPBOARD_PRESERVATION_UNAVAILABLE: " + reason +
2723
- "; clipboard unchanged. Use mode:unicode if suitable for the target editor.");
2724
- error.Data["code"] = "CLIPBOARD_PRESERVATION_UNAVAILABLE";
2725
- error.Data["outcome"] = "not-dispatched";
2726
- return error;
2727
- }
2728
3068
  public static bool Supported(uint format, string name)
2729
3069
  {
2730
3070
  if (format == 1 || format == 7 || format == 8 || format == 13 || format == 15 || format == 16 || format == 17) return true;
@@ -2746,6 +3086,13 @@ namespace DshComputerUse.Worker
2746
3086
  }
2747
3087
  public uint Sequence { get { return GetClipboardSequenceNumber(); } }
2748
3088
  public void Dispose() { if (opened) { CloseClipboard(); opened = false; } }
3089
+ /// Returns null when the current clipboard cannot be fully snapshotted (private or GDI
3090
+ /// formats, too many/oversized entries, lock failures). NULL MEANS "cannot preserve",
3091
+ /// not "refuse to work" (human incident 2026-11-04): a WeChat-session clipboard carried
3092
+ /// private format 49161, paste was refused, the JS auto-fallback re-typed 37 CJK chars
3093
+ /// as unicode keystrokes and a garbled message actually went out. Losing an opaque
3094
+ /// clipboard item is a recoverable local cost; a mis-typed external message is not —
3095
+ /// so the caller downgrades to paste-without-restore instead of refusing.
2749
3096
  public List<ClipboardBytes> Snapshot()
2750
3097
  {
2751
3098
  var formats = new List<uint>();
@@ -2756,27 +3103,26 @@ namespace DshComputerUse.Worker
2756
3103
  format = EnumClipboardFormats(format);
2757
3104
  if (format == 0)
2758
3105
  {
2759
- if (Marshal.GetLastWin32Error() != 0) throw Unavailable("format enumeration failed");
3106
+ if (Marshal.GetLastWin32Error() != 0) return null;
2760
3107
  break;
2761
3108
  }
2762
3109
  var name = new StringBuilder(256);
2763
3110
  if (format >= 0xC000) GetClipboardFormatName(format, name, name.Capacity);
2764
- if (!Supported(format, name.ToString())) throw Unavailable("unsupported format " + format);
3111
+ if (!Supported(format, name.ToString())) return null;
2765
3112
  formats.Add(format);
2766
- if (formats.Count > 64) throw Unavailable("too many formats");
3113
+ if (formats.Count > 64) return null;
2767
3114
  }
2768
3115
  var result = new List<ClipboardBytes>();
2769
3116
  long total = 0;
2770
3117
  foreach (uint id in formats)
2771
3118
  {
2772
3119
  IntPtr handle = GetClipboardData(id);
2773
- if (handle == IntPtr.Zero) throw Unavailable("data unavailable for format " + id);
3120
+ if (handle == IntPtr.Zero) return null;
2774
3121
  ulong size = GlobalSize(handle).ToUInt64();
2775
- if (size == 0 || size > 32 * 1024 * 1024 || total + (long)size > 32 * 1024 * 1024)
2776
- throw Unavailable("clipboard snapshot exceeds supported size");
3122
+ if (size == 0 || size > 32 * 1024 * 1024 || total + (long)size > 32 * 1024 * 1024) return null;
2777
3123
  total += (long)size;
2778
3124
  IntPtr data = GlobalLock(handle);
2779
- if (data == IntPtr.Zero) throw Unavailable("clipboard data could not be locked");
3125
+ if (data == IntPtr.Zero) return null;
2780
3126
  try
2781
3127
  {
2782
3128
  var bytes = new byte[(int)size]; Marshal.Copy(data, bytes, 0, bytes.Length);
@@ -2822,13 +3168,13 @@ namespace DshComputerUse.Worker
2822
3168
  static readonly object OutLock = new object(); // responses AND unsolicited events share stdout
2823
3169
  static readonly DateTime Epoch = new DateTime(1970, 1, 1);
2824
3170
 
2825
- // Position correction is diagnostic only. Physical input immediately yields;
2826
- // no distance, reversal, wheel-count or screen-corner rule latches a pause.
3171
+ // Position correction is diagnostic only. Deliberate physical takeover during
3172
+ // actual input yields; incidental mouse jitter and idle use do not pause the task.
2827
3173
  static int _positionCorrections;
2828
3174
  static string _dpiNote = "unset";
2829
3175
  static readonly HashSet<string> ObsOps = new HashSet<string>
2830
3176
  {
2831
- "ping", "echoargs", "screens", "cursor", "capture", "annotate",
3177
+ "ping", "echoargs", "screens", "cursor", "capture", "ocr", "annotate",
2832
3178
  "windows", "uia", "uiaFromPoint", "uiaFocused", "waitForIdle", "clipRead",
2833
3179
  "selftest", "indicator", "waitStable", "waitChange", "focus", "frameSig", "probe"
2834
3180
  };
@@ -2841,16 +3187,44 @@ namespace DshComputerUse.Worker
2841
3187
  "activate", "windowOp", "calibrate", "selectRange", "uiaAct"
2842
3188
  };
2843
3189
  static readonly object InputLock = new object();
3190
+ static readonly DesktopOwnershipLease DesktopLease = new DesktopOwnershipLease(DesktopOwnershipLease.DefaultPath());
3191
+ static bool _releaseDesktopPending;
3192
+ public static bool DesktopOwned { get { return DesktopLease.Owned; } }
3193
+ public static bool TryClaimDesktop(string owner)
3194
+ { string occupiedBy; return DesktopLease.TryAcquire(owner, out occupiedBy); }
3195
+ static void EnsureDesktopOwnership(string op)
3196
+ {
3197
+ if (op == "ping" || op == "echoargs" || op == "keyClass" || op == "stuckPlan" || op == "probe") return;
3198
+ string occupiedBy;
3199
+ if (!DesktopLease.TryAcquire("native cycle: " + op, out occupiedBy))
3200
+ {
3201
+ var error = InputInterrupted("MACHINE_OWNED", "Another agent owns this desktop's computer-use cycle (" + occupiedBy + "). This operation was not dispatched; keep the task pending until that cycle releases control.");
3202
+ error.Data["outcome"] = "not-dispatched"; throw error;
3203
+ }
3204
+ }
3205
+ public static void ReleaseDesktopOwnership()
3206
+ {
3207
+ lock (InputLock)
3208
+ {
3209
+ _releaseDesktopPending = true;
3210
+ if (!_operationLive && OwnedUps.Count == 0) { DesktopLease.Dispose(); _releaseDesktopPending = false; }
3211
+ }
3212
+ }
2844
3213
  static readonly Dictionary<string, INPUT> OwnedUps = new Dictionary<string, INPUT>();
2845
3214
  static readonly ConcurrentDictionary<int, bool> CancelledRequests = new ConcurrentDictionary<int, bool>();
2846
3215
  static long _cancelGeneration;
3216
+ public static void InvalidateActiveInput() { Interlocked.Increment(ref _cancelGeneration); }
2847
3217
  static long _operationCancelGeneration, _operationHumanRevision;
2848
3218
  static volatile bool _operationLive, _operationDispatched;
3219
+ static bool _operationActing;
3220
+ static string _lastAnnouncedSeries;
3221
+ static long _announcedHumanRevision, _announcedInputEpoch, _preparedInputEpoch;
2849
3222
  static volatile int _activeRequestId, _lastCompletedRequestId;
2850
3223
  // The seam is internal to the compiled program; no tool can replace it.
2851
3224
  internal static Func<INPUT[], uint> InputSender = delegate(INPUT[] inputs) {
2852
3225
  return Native.SendInput((uint)inputs.Length, inputs, InputCbSize);
2853
3226
  };
3227
+ internal static Func<IntPtr> PreparationForeground = delegate { return Native.GetForegroundWindow(); };
2854
3228
 
2855
3229
  static Exception InputInterrupted(string code, string message)
2856
3230
  {
@@ -2861,16 +3235,89 @@ namespace DshComputerUse.Worker
2861
3235
  }
2862
3236
  public static void CheckHumanInput()
2863
3237
  {
3238
+ if (Panic.ExitRequested)
3239
+ throw InputInterrupted("CYCLE-ENDED", "Ctrl+Alt+Q requested exit; no further input will be sent for this operation.");
2864
3240
  if (Panic.PauseRequested)
2865
3241
  throw InputInterrupted("ABORTED", "Ctrl+Esc requested a manual pause; wait for manual recovery.");
2866
3242
  if (_operationLive && _operationCancelGeneration != Interlocked.Read(ref _cancelGeneration))
2867
3243
  throw InputInterrupted("INPUT_CANCELLED", "the owning tool was cancelled; no further input will be sent.");
2868
3244
  if (Panic.HumanActive || (_operationLive && _operationHumanRevision != Panic.HumanRevision))
2869
- throw InputInterrupted("HUMAN_YIELD", "Physical input owns the desktop. Wait for three quiet seconds, then inspect the current target before continuing; never replay a partial write.");
3245
+ throw InputInterrupted("HUMAN_YIELD", "Temporary interruption only: the user currently owns input and the original task remains active. Wait locally until control returns after three quiet seconds, then immediately obtain fresh target evidence and continue the remaining authorized task. Do not abandon, cancel or declare completion; never replay a partial write.");
2870
3246
  }
2871
3247
  static void MarkMutation()
2872
3248
  {
2873
- Panic.Check("actuation"); CheckHumanInput(); _operationDispatched = true;
3249
+ Panic.Check("actuation"); BeginActuation(); CheckHumanInput(); _operationDispatched = true;
3250
+ }
3251
+ static void BeginActuation()
3252
+ {
3253
+ if (_operationActing) return;
3254
+ if (_operationLive && _preparedInputEpoch != Panic.InputEpoch)
3255
+ throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop before input began. No action was dispatched. Obtain fresh target evidence and continue the original task; this is a temporary checkpoint, not cancellation.");
3256
+ _operationActing = true;
3257
+ Panic.BeginActuation();
3258
+ CheckHumanInput();
3259
+ if (_operationLive && _preparedInputEpoch != Panic.InputEpoch)
3260
+ throw InputInterrupted("HUMAN_REOBSERVE", "The desktop changed at the input boundary. No action was dispatched. Read the current target and continue the pending task.");
3261
+ }
3262
+ static void EndActuationForReadback()
3263
+ {
3264
+ lock (InputLock)
3265
+ {
3266
+ // Nested selection/chord helpers may still own a modifier or button.
3267
+ // Keep their sequence protected until every accepted down is released.
3268
+ if (OwnedUps.Count != 0) return;
3269
+ _operationActing = false;
3270
+ Panic.EndActuation();
3271
+ }
3272
+ }
3273
+ static void PrepareActuation(string op, Dictionary<string, object> request)
3274
+ {
3275
+ object raw;
3276
+ var args = request.TryGetValue("args", out raw) ? raw as Dictionary<string, object> : null;
3277
+ string series = args == null ? "" : GetS(args, "__inputSeries", "");
3278
+ if (!string.IsNullOrEmpty(series) && series == _lastAnnouncedSeries &&
3279
+ _announcedHumanRevision == Panic.HumanRevision)
3280
+ {
3281
+ if (_announcedInputEpoch != Panic.InputEpoch)
3282
+ throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop between batch steps. Obtain fresh target evidence and continue the remaining task without replaying completed input.");
3283
+ _preparedInputEpoch = Panic.InputEpoch; return;
3284
+ }
3285
+ Panic.Check(op); CheckHumanInput();
3286
+ long inputEpoch = Panic.InputEpoch;
3287
+ IntPtr foreground = PreparationForeground();
3288
+ Glow.Prepare(true);
3289
+ try
3290
+ {
3291
+ var startup = Stopwatch.StartNew();
3292
+ while (!Glow.PreparationVisible)
3293
+ {
3294
+ Panic.Check(op); CheckHumanInput();
3295
+ if (startup.ElapsedMilliseconds >= 5000) throw PreparationUnavailable("first-frame");
3296
+ Thread.Sleep(10);
3297
+ }
3298
+ var timer = Stopwatch.StartNew();
3299
+ while (timer.ElapsedMilliseconds < Glow.PreparationMs || !Glow.PreparationComplete)
3300
+ {
3301
+ Panic.Check(op); CheckHumanInput();
3302
+ if (timer.ElapsedMilliseconds >= 5000) throw PreparationUnavailable("countdown-frame");
3303
+ int remaining = Glow.PreparationMs - (int)timer.ElapsedMilliseconds;
3304
+ Thread.Sleep(remaining > 0 ? Math.Min(25, remaining) : 10);
3305
+ }
3306
+ Panic.Check(op); CheckHumanInput();
3307
+ if (inputEpoch != Panic.InputEpoch || foreground != PreparationForeground())
3308
+ throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop during the preparation cue. No action was dispatched. Preserve the task, obtain fresh target evidence, and continue the authorized task immediately; this is not cancellation or abandonment.");
3309
+ _lastAnnouncedSeries = series; _announcedHumanRevision = Panic.HumanRevision;
3310
+ _announcedInputEpoch = _preparedInputEpoch = inputEpoch;
3311
+ }
3312
+ finally { Glow.Prepare(false); }
3313
+ }
3314
+ static Exception PreparationUnavailable(string phase)
3315
+ {
3316
+ var health = Glow.Health();
3317
+ health["failurePhase"] = phase;
3318
+ var error = InputInterrupted("PREPARATION_UNAVAILABLE", "The preparation notice could not be displayed through completion; no input was dispatched. glowHealth=" + Json.Serialize(health));
3319
+ error.Data["glowHealth"] = health;
3320
+ return error;
2874
3321
  }
2875
3322
  public static bool CancelInput(int requestId)
2876
3323
  {
@@ -2912,6 +3359,7 @@ namespace DshComputerUse.Worker
2912
3359
  // cross-process STOP may hold SyncLock while releasing owned input.
2913
3360
  if (Panic.Engaged || Panic.Stopped || Panic.Exited)
2914
3361
  throw InputInterrupted("ABORTED", "manual pause or exit owns the desktop; no further input will be sent.");
3362
+ BeginActuation();
2915
3363
  if (inputs.Length == 1)
2916
3364
  {
2917
3365
  bool isUp; INPUT release; int physical;
@@ -3106,6 +3554,10 @@ namespace DshComputerUse.Worker
3106
3554
  else if (kind == "resume")
3107
3555
  hint = "Re-armed: a human pressed Ctrl+Alt+R (the worker notifies `resume` from Clear() " +
3108
3556
  "alone) — working may continue from the break point.";
3557
+ else if (kind == "secure")
3558
+ hint = why == "active"
3559
+ ? "The Windows secure desktop is up (a UAC prompt or the lock screen). Input dispatch is PAUSED — nothing you send will be dispatched until it closes. Tell the human a system dialog needs their answer; dispatch resumes automatically the moment the normal desktop returns, then simply retry the step."
3560
+ : "The normal desktop is back. Input dispatch has resumed automatically — retry the interrupted step and continue the task.";
3109
3561
  else
3110
3562
  hint = "worker state change \"" + kind + "\": " + why;
3111
3563
  WriteLine(Json.Serialize(Dict(
@@ -3136,6 +3588,11 @@ namespace DshComputerUse.Worker
3136
3588
  Dictionary<string, object> resp;
3137
3589
  try { resp = Handle(oneshot); }
3138
3590
  catch (Exception ex) { resp = Err(ex); }
3591
+ if (!Panic.FinishOneShotControls())
3592
+ {
3593
+ Program.AuditRecord("CONTROL-DRAIN-TIMEOUT", "one-shot control finalization exceeded 5s");
3594
+ resp = Err("CONTROL-DRAIN-TIMEOUT: physical brake finalization could not be confirmed; verify STOP/EXITED before continuing.");
3595
+ }
3139
3596
  AuditActuation(oneshot, resp);
3140
3597
  WriteLine(Json.Serialize(resp));
3141
3598
  return 0;
@@ -3218,20 +3675,30 @@ namespace DshComputerUse.Worker
3218
3675
  {
3219
3676
  var request = Json.Deserialize<Dictionary<string, object>>(line);
3220
3677
  string op = GetS(request, "op", "");
3221
- bool input = ActOps.Contains(op) || op == "shiftClick" || op == "selftest";
3678
+ object rawArgs;
3679
+ var args = request.TryGetValue("args", out rawArgs) ? rawArgs as Dictionary<string, object> : null;
3680
+ bool input = ActOps.Contains(op) || op == "shiftClick" || op == "selftest" ||
3681
+ (op == "focus" && args != null && GetB(args, "refocus", false));
3682
+ bool dryInput = input && args != null && GetB(args, "dryRun", false);
3683
+ // Only the coordinate click probe has a native read-only implementation.
3684
+ // No other input may use the flag to skip the controls and then dispatch.
3685
+ if (dryInput && (op != "click" || !args.ContainsKey("x") || !args.ContainsKey("y")))
3686
+ return Err("UNSUPPORTED_DRY_RUN: native input simulation requires a click with x and y; no input was dispatched.");
3687
+ EnsureDesktopOwnership(op);
3222
3688
  int requestId = GetI(request, "id", 0);
3223
3689
  bool ignored;
3224
3690
  _activeRequestId = requestId;
3225
3691
  _operationHumanRevision = Panic.HumanRevision;
3226
3692
  _operationCancelGeneration = Interlocked.Read(ref _cancelGeneration);
3227
3693
  _operationDispatched = false; _operationLive = input;
3694
+ bool scopedCycle = false;
3228
3695
  try
3229
3696
  {
3230
3697
  if (CancelledRequests.TryRemove(requestId, out ignored))
3231
3698
  throw InputInterrupted("INPUT_CANCELLED", "cancelled before dispatch.");
3232
3699
  if (input) CheckHumanInput();
3233
3700
  string pauseId = Panic.TemporaryPauseId;
3234
- Dictionary<string, object> result = HandleCore(line);
3701
+ Dictionary<string, object> result = HandleCore(line, input, dryInput, ref scopedCycle);
3235
3702
  if (input) CheckHumanInput();
3236
3703
  if (!string.IsNullOrEmpty(pauseId) && GetB(result, "ok", false) &&
3237
3704
  (op == "windows" || op == "uia" || op == "uiaFromPoint" || op == "uiaFocused" || op == "capture"))
@@ -3240,14 +3707,20 @@ namespace DshComputerUse.Worker
3240
3707
  }
3241
3708
  finally
3242
3709
  {
3243
- if (input) ReleaseOwnedInputs();
3710
+ if (input)
3711
+ {
3712
+ try { ReleaseOwnedInputs(); }
3713
+ finally { _operationActing = false; Panic.EndActuation(); Glow.Prepare(false); }
3714
+ }
3244
3715
  _operationLive = false; _activeRequestId = 0;
3716
+ if (scopedCycle) Panic.TryEndCycle();
3717
+ if (_releaseDesktopPending) ReleaseDesktopOwnership();
3245
3718
  _lastCompletedRequestId = Math.Max(_lastCompletedRequestId, requestId);
3246
3719
  CancelledRequests.TryRemove(requestId, out ignored);
3247
3720
  }
3248
3721
  }
3249
3722
 
3250
- static Dictionary<string, object> HandleCore(string line)
3723
+ static Dictionary<string, object> HandleCore(string line, bool input, bool dryInput, ref bool scopedCycle)
3251
3724
  {
3252
3725
  Dictionary<string, object> req;
3253
3726
  try { req = Json.Deserialize<Dictionary<string, object>>(line); }
@@ -3257,6 +3730,7 @@ namespace DshComputerUse.Worker
3257
3730
  Dictionary<string, object> a = (req.TryGetValue("args", out av) && av is Dictionary<string, object>) ? (Dictionary<string, object>)av : new Dictionary<string, object>();
3258
3731
  bool cycleOp = GetB(a, "__cycle", false) && op != null &&
3259
3732
  op != "abort" && op != "ping" && op != "echoargs" &&
3733
+ op != "probe" && op != "keyClass" && op != "stuckPlan" &&
3260
3734
  op != "indicator" && op != "calm" && op != "panic" && op != "resume" && op != "recover" &&
3261
3735
  // ... AND THE CYCLE HAS TO BE ALIVE TO BE LIT (2026-09-13). "Lit by any call" is how
3262
3736
  // a DEAD cycle came back as a cyan breathing border: after Ctrl+Alt+Q ended the
@@ -3279,7 +3753,22 @@ namespace DshComputerUse.Worker
3279
3753
  // human re-arms it (CheckOp's allowlist keeps the health probe and re-arm alive).
3280
3754
  // Emergency stop gates EVERY op that could drive the machine; a DRY RUN drives nothing
3281
3755
  // and is therefore treated as the observation it really is.
3282
- Panic.CheckOp((op == "click" || op == "move" || op == "type" || op == "key") && GetB(a, "dryRun", false) ? "probe" : op);
3756
+ Panic.CheckOp(dryInput ? "probe" : op);
3757
+
3758
+ if (input && !dryInput)
3759
+ {
3760
+ // Direct native input (including --op) has no JS cycle signature.
3761
+ // Arm physical controls before the purple cue and release this scope
3762
+ // when the request ends. A human pause or exit retains its own latch.
3763
+ if (!Panic.CycleLit)
3764
+ {
3765
+ scopedCycle = Panic.TryLightCycle(0);
3766
+ if (!scopedCycle) Panic.Check(op);
3767
+ }
3768
+ Panic.RequireControlHooks();
3769
+ Panic.RequireNormalDesktop();
3770
+ PrepareActuation(op, req);
3771
+ }
3283
3772
 
3284
3773
  switch (op)
3285
3774
  {
@@ -3291,7 +3780,7 @@ namespace DshComputerUse.Worker
3291
3780
  case "beginAsk": return Ok(BeginAskOp(a));
3292
3781
  case "endAsk": return Ok(EndAskOp(a));
3293
3782
  case "exit": return Ok(ExitOp(a));
3294
- case "ping": return Ok(Dict("pong", true, "cooperation", Panic.CooperationState()));
3783
+ case "ping": return Ok(Dict("pong", true, "cooperation", Panic.CooperationState(), "glowHealth", Glow.Health()));
3295
3784
  // TEST SEAM (D, 2026-09-13). A fixture may not re-implement the rule it measures, so
3296
3785
  // this op drives the PRODUCTION transition (Panic.ClassifyAndTrackKey — the same
3297
3786
  // function the low-level hook calls) with explicit evidence: ours / injected / vk.
@@ -3357,6 +3846,7 @@ namespace DshComputerUse.Worker
3357
3846
  case "screens": return Ok(VirtualScreenDict());
3358
3847
  case "cursor": POINT cp; Native.GetCursorPos(out cp); return Ok(PtDict(cp.X, cp.Y));
3359
3848
  case "capture": return Ok(Capture(a));
3849
+ case "ocr": return Ok(Ocr(a));
3360
3850
  case "annotate": return Ok(Annotate(a));
3361
3851
  case "move": FailsafeCheck(a); return Ok(Move(a));
3362
3852
  case "click": FailsafeCheck(a); return Ok(Click(a));
@@ -3378,13 +3868,15 @@ namespace DshComputerUse.Worker
3378
3868
  case "probe":
3379
3869
  {
3380
3870
  string what = GetS(a, "what", "point");
3381
- Dictionary<string, object> pr = what == "focus" ? ProbeFocus()
3871
+ Dictionary<string, object> pr = what == "glow" ? Dict() : what == "focus" ? ProbeFocus()
3382
3872
  : (what == "window" ? WinBrief(FindHwnd(a)) : ProbePoint(GetI(a, "x", 0), GetI(a, "y", 0)));
3873
+ pr["glowHealth"] = Glow.Health();
3383
3874
  // The state machine as a FACT, in the one op that exists to answer "what is
3384
3875
  // actually true right now". Read-only, so asking never changes the answer.
3385
3876
  pr["cycle"] = Dict(
3386
3877
  "cycleLit", Panic.CycleLit,
3387
3878
  "armed", Panic.Armed,
3879
+ "secureDesktop", Panic.SecureDesktopActive,
3388
3880
  "engaged", Panic.Engaged,
3389
3881
  "stopped", Panic.Stopped,
3390
3882
  "exited", Panic.Exited,
@@ -3399,8 +3891,9 @@ namespace DshComputerUse.Worker
3399
3891
  pr["glow"] = Dict(
3400
3892
  "want", Glow.Want,
3401
3893
  // Is a real injection sequence in flight RIGHT NOW? The ACTING light is driven
3402
- // by this as well as by its short tail, so the state is reported as a fact.
3894
+ // by the actual action scope, so both are reported as observable facts.
3403
3895
  "injecting", Glow.Injecting,
3896
+ "acting", Glow.Acting, "preparing", Glow.Preparing, "preparationMs", Glow.PreparationMs,
3404
3897
  "title", Glow.ShownTitle == null ? "" : Glow.ShownTitle);
3405
3898
  // EVENTS THE OS ACTUALLY INSERTED (not the plan): 0 after a plain pause with
3406
3899
  // nothing held is the observable form of "no ghost right-click / Alt menu".
@@ -3417,9 +3910,9 @@ namespace DshComputerUse.Worker
3417
3910
  // like the emergency brake. Called when the agent's turn ends, so a lingering blue
3418
3911
  // border can never claim the machine is still being driven after control stopped.
3419
3912
  case "calm":
3420
- Panic.CycleLit = false; // the turn is over: the cycle is dead (R2)
3421
- Panic.Disarm(); // the human monitors stop WITH it
3422
- Glow.Kill(); return Ok(Dict("indicator", "off", "hard", true));
3913
+ if (!Panic.TryEndCycle())
3914
+ return Ok(Dict("released", false, "reason", "A manual pause or question still owns this cycle; its brake and ownership were preserved."));
3915
+ return Ok(Dict("released", true, "indicator", "off", "hard", true));
3423
3916
  case "focus": return Ok(FocusReport(a, GetB(a, "refocus", false)));
3424
3917
  case "frameSig": return Ok(FrameSigOp(a));
3425
3918
  // `source=agent` (not "unknown"): this op exists ONLY for the agent's own `stop`, and a
@@ -3465,6 +3958,90 @@ namespace DshComputerUse.Worker
3465
3958
 
3466
3959
  static long NowMs() { return (long)(DateTime.UtcNow - Epoch).TotalMilliseconds; }
3467
3960
 
3961
+ // ---------- OCR: text WITH bounding boxes, straight from the worker ----------
3962
+ // Zero-UIA apps (WeChat incident 2026-11-04) burned one screenshot per decision because
3963
+ // finding "傳送" or a contact name meant reading pixels with model vision. Windows' inbox
3964
+ // OCR (Windows.Media.Ocr — ships with the OS, zero external dependencies) returns text
3965
+ // with per-word rectangles, so the model can locate controls BY NAME and spend vision
3966
+ // only where looks actually matter. Coordinates are ABSOLUTE screen pixels (region origin
3967
+ // added), ready for computer_click without further math.
3968
+ static OcrEngine _ocrEngine;
3969
+ static string _ocrLanguage;
3970
+ static Dictionary<string, object> Ocr(Dictionary<string, object> a)
3971
+ {
3972
+ ValidateKickoffCapture(a);
3973
+ if (_ocrEngine == null)
3974
+ {
3975
+ _ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages();
3976
+ if (_ocrEngine != null) _ocrLanguage = _ocrEngine.RecognizerLanguage.LanguageTag;
3977
+ }
3978
+ if (_ocrEngine == null)
3979
+ throw new Exception("OCR_UNAVAILABLE: no OCR recognizer language is installed on this machine. " +
3980
+ "Install one (Settings > Time & language > Language > add a language with 'Text recognition') " +
3981
+ "or read regions with computer_shot instead.");
3982
+ int vx = Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN);
3983
+ int vy = Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN);
3984
+ int vw = Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN);
3985
+ int vh = Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN);
3986
+ int x = GetI(a, "x", vx), y = GetI(a, "y", vy);
3987
+ int w = GetI(a, "width", vw), h = GetI(a, "height", vh);
3988
+ if (GetB(a, "clamp", true))
3989
+ {
3990
+ int x2 = Math.Max(x, vx), y2 = Math.Max(y, vy);
3991
+ int x3 = Math.Min(x + w, vx + vw), y3 = Math.Min(y + h, vy + vh);
3992
+ if (x3 <= x2 || y3 <= y2) throw new Exception("region outside virtual desktop");
3993
+ x = x2; y = y2; w = x3 - x2; h = y3 - y2;
3994
+ }
3995
+ // OCR cost scales with pixels; one decision never needs more than 16M of them.
3996
+ if ((long)w * h > 16L * 1024 * 1024)
3997
+ throw new Exception("OCR region exceeds 16M pixels (" + w + "x" + h + "); crop tighter.");
3998
+ byte[] bgra = new byte[w * h * 4];
3999
+ IntPtr screen = IntPtr.Zero, mem = IntPtr.Zero, dib = IntPtr.Zero, oldBmp = IntPtr.Zero;
4000
+ try
4001
+ {
4002
+ screen = Native.GetDC(IntPtr.Zero);
4003
+ mem = Native.CreateCompatibleDC(screen);
4004
+ Native.BITMAPINFO bmi = new Native.BITMAPINFO();
4005
+ bmi.bmiHeader.biSize = (uint)Marshal.SizeOf(typeof(Native.BITMAPINFOHEADER));
4006
+ bmi.bmiHeader.biWidth = w; bmi.bmiHeader.biHeight = -h; // top-down
4007
+ bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32;
4008
+ bmi.bmiHeader.biCompression = 0;
4009
+ IntPtr bits;
4010
+ dib = Native.CreateDIBSection(screen, ref bmi, 0, out bits, IntPtr.Zero, 0);
4011
+ if (dib == IntPtr.Zero) throw new Exception("OCR: CreateDIBSection failed");
4012
+ oldBmp = Native.SelectObject(mem, dib);
4013
+ Native.BitBlt(mem, 0, 0, w, h, screen, x, y, 0x00CC0020 | 0x40000000);
4014
+ Marshal.Copy(bits, bgra, 0, bgra.Length);
4015
+ }
4016
+ finally
4017
+ {
4018
+ if (oldBmp != IntPtr.Zero && oldBmp != new IntPtr(-1)) Native.SelectObject(mem, oldBmp);
4019
+ if (dib != IntPtr.Zero) Native.DeleteObject(dib);
4020
+ if (mem != IntPtr.Zero) Native.DeleteDC(mem);
4021
+ if (screen != IntPtr.Zero) Native.ReleaseDC(IntPtr.Zero, screen);
4022
+ }
4023
+ SoftwareBitmap bitmap = SoftwareBitmap.CreateCopyFromBuffer(
4024
+ WindowsRuntimeBufferExtensions.AsBuffer(bgra), BitmapPixelFormat.Bgra8, w, h);
4025
+ OcrResult result = _ocrEngine.RecognizeAsync(bitmap).AsTask().GetAwaiter().GetResult();
4026
+ List<object> lines = new List<object>();
4027
+ StringBuilder joined = new StringBuilder();
4028
+ foreach (OcrLine line in result.Lines)
4029
+ {
4030
+ List<object> words = new List<object>();
4031
+ foreach (OcrWord word in line.Words)
4032
+ {
4033
+ words.Add(Dict("text", word.Text,
4034
+ "x", x + (int)word.BoundingRect.X, "y", y + (int)word.BoundingRect.Y,
4035
+ "w", (int)word.BoundingRect.Width, "h", (int)word.BoundingRect.Height));
4036
+ }
4037
+ lines.Add(Dict("text", line.Text, "words", words));
4038
+ if (joined.Length > 0) joined.Append('\n');
4039
+ joined.Append(line.Text);
4040
+ }
4041
+ return Dict("lines", lines, "text", joined.ToString(), "language", _ocrLanguage,
4042
+ "region", new int[] { x, y, w, h });
4043
+ }
4044
+
3468
4045
  static Dictionary<string, object> Capture(Dictionary<string, object> a)
3469
4046
  {
3470
4047
  ValidateKickoffCapture(a); // before even the pixel-stability sampler
@@ -3706,6 +4283,7 @@ namespace DshComputerUse.Worker
3706
4283
  // ACTING light on it, and clear the mark in a finally so the blue can never outlive the
3707
4284
  // input it is describing.
3708
4285
  CheckHumanInput();
4286
+ BeginActuation();
3709
4287
  Glow.InputBegin();
3710
4288
  try
3711
4289
  {
@@ -3767,7 +4345,8 @@ namespace DshComputerUse.Worker
3767
4345
  string btn = GetS(a, "button", "left");
3768
4346
  int clicks = Math.Max(1, Math.Min(3, GetI(a, "clicks", 1)));
3769
4347
  int gap = GetI(a, "intervalMs", 60);
3770
- AgentActing(a, "click");
4348
+ bool dryRun = GetB(a, "dryRun", false);
4349
+ if (!dryRun) AgentActing(a, "click");
3771
4350
  CheckHumanInput();
3772
4351
  // If the caller NAMED the window it wants to click in, put that window in front
3773
4352
  // first: a click on a covered pixel is delivered to whatever is on top of it (the
@@ -3778,7 +4357,6 @@ namespace DshComputerUse.Worker
3778
4357
  // would change the thing it measures. That bug hid a real occlusion for a whole test
3779
4358
  // round (2026-09-13): every dry run reported "ok" because it had just activated the
3780
4359
  // window it was asked about.
3781
- bool dryRun = GetB(a, "dryRun", false);
3782
4360
  // A caller-supplied window is re-asserted (raised, ALT tap, titlebar click) on the
3783
4361
  // next line, so it has to be checked BEFORE that: the pixel check further down comes
3784
4362
  // far too late to protect a window the worker was told to bring forward.
@@ -3921,6 +4499,7 @@ namespace DshComputerUse.Worker
3921
4499
  SendMouse(down, 0, 0, 0); Thread.Sleep(30); SendMouse(up, 0, 0, 0);
3922
4500
  if (i < clicks - 1) Thread.Sleep(gap);
3923
4501
  }
4502
+ EndActuationForReadback();
3924
4503
  double changedPct = -1;
3925
4504
  if (verify && before != null)
3926
4505
  {
@@ -3940,8 +4519,10 @@ namespace DshComputerUse.Worker
3940
4519
  {
3941
4520
  clickOut["changedPct"] = changedPct;
3942
4521
  if (changedPct < 0.15 && !a.ContainsKey("verify"))
3943
- clickOut["warning"] = "nothing changed near the click point within ~240ms: the click may not have reached " +
3944
- "its target (dead UI, disabled control, or a wrong pixel). Probe the point before retrying.";
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.";
3945
4526
  }
3946
4527
  return clickOut;
3947
4528
  }
@@ -4047,16 +4628,8 @@ namespace DshComputerUse.Worker
4047
4628
  {
4048
4629
  _lastRefocus = EnsureTargetForeground();
4049
4630
  }
4050
- // Stays lit across the agent's thinking gaps: the glow means "the agent is
4051
- // driving this machine right now", not "a click happened a moment ago".
4052
- // 1200 ms, not 30000 (2026-09-13). The hold is how long the DEEP BLUE "acting" border
4053
- // stays after an input injection — and 30 s meant a 50 ms click painted half a minute of
4054
- // "I am driving the machine", during which the cyan "thinking" state could never show
4055
- // (the render loop shows acting in preference to thinking). The human watched the blue
4056
- // light sit still through a whole page read and asked, correctly, why. Blue now means
4057
- // "input, right now, plus a short tail"; cyan is what a reading agent looks like.
4058
- int hold = GetI(a, "indicatorMs", 1200);
4059
- Glow.Touch(hold);
4631
+ // Validation and focus readback do not own input. The first real mutation
4632
+ // or SendInput opens the actual scope and its deep-blue cue.
4060
4633
  }
4061
4634
 
4062
4635
  static Dictionary<string, object> Indicator(Dictionary<string, object> a)
@@ -4360,11 +4933,16 @@ namespace DshComputerUse.Worker
4360
4933
 
4361
4934
  // Explicit window scope excludes unrelated desktop animation without loosening the
4362
4935
  // change threshold. A moved/minimized/replaced window invalidates the observation.
4363
- static Func<byte[]> WaitSampler(Dictionary<string, object> a, int step, out object scope)
4936
+ static Func<byte[]> WaitSampler(Dictionary<string, object> a, int step, out object scope,
4937
+ out int rx, out int ry, out int rw, out int rh)
4364
4938
  {
4365
4939
  if (!a.ContainsKey("hwnd") || GetI(a, "hwnd", 0) == 0)
4366
4940
  {
4367
- scope = Dict("kind", "virtual-desktop");
4941
+ rx = Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN);
4942
+ ry = Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN);
4943
+ rw = Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN);
4944
+ rh = Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN);
4945
+ scope = Dict("kind", "virtual-desktop", "region", Dict("x", rx, "y", ry, "width", rw, "height", rh));
4368
4946
  return delegate { return FrameSignature(step); };
4369
4947
  }
4370
4948
  IntPtr h = new IntPtr(GetI(a, "hwnd", 0));
@@ -4376,7 +4954,8 @@ namespace DshComputerUse.Worker
4376
4954
  Rectangle visible = Rectangle.Intersect(virtualRect, Rectangle.FromLTRB(original.Left, original.Top, original.Right, original.Bottom));
4377
4955
  if (visible.Width <= 16 || visible.Height <= 16)
4378
4956
  throw new Exception("WAIT_TARGET_UNAVAILABLE: window has no usable visible screen region");
4379
- scope = Dict("kind", "window", "hwnd", h.ToInt64(), "region", Dict("x", visible.X, "y", visible.Y, "width", visible.Width, "height", visible.Height));
4957
+ rx = visible.X; ry = visible.Y; rw = visible.Width; rh = visible.Height;
4958
+ scope = Dict("kind", "window", "hwnd", h.ToInt64(), "region", Dict("x", rx, "y", ry, "width", rw, "height", rh));
4380
4959
  return delegate {
4381
4960
  RECT current;
4382
4961
  if (!Native.IsWindow(h) || Native.IsIconic(h) || PidOf(h) != pid || !Native.GetWindowRect(h, out current) ||
@@ -4386,6 +4965,29 @@ namespace DshComputerUse.Worker
4386
4965
  };
4387
4966
  }
4388
4967
 
4968
+ /// Bounding box (absolute screen px) of every signature cell that differs beyond the
4969
+ /// threshold — "where did it change", not just "did it" (test report 2026-11-04: a
4970
+ /// 0.344% diff cannot distinguish a sent message from a blinking caret without this).
4971
+ static int[] SigDiffBounds(byte[] a, byte[] b, int threshold, int cols, int step, int rx, int ry)
4972
+ {
4973
+ if (a == null || b == null || a.Length != b.Length || cols <= 0) return null;
4974
+ int minX = -1, minY = -1, maxX = -1, maxY = -1;
4975
+ for (int i = 0; i < a.Length; i++)
4976
+ {
4977
+ int d = a[i] - b[i];
4978
+ if (d < 0) d = -d;
4979
+ if (d <= threshold) continue;
4980
+ int c = i % cols, r = i / cols;
4981
+ if (minX < 0 || c < minX) minX = c;
4982
+ if (c > maxX) maxX = c;
4983
+ if (minY < 0 || r < minY) minY = r;
4984
+ if (r > maxY) maxY = r;
4985
+ }
4986
+ if (minX < 0) return null;
4987
+ return new int[] { rx + minX * step, ry + minY * step,
4988
+ (maxX - minX + 1) * step, (maxY - minY + 1) * step };
4989
+ }
4990
+
4389
4991
  static Dictionary<string, object> WaitStable(Dictionary<string, object> a)
4390
4992
  {
4391
4993
  int capMs = Math.Max(400, GetI(a, "timeoutMs", 8000));
@@ -4394,8 +4996,34 @@ namespace DshComputerUse.Worker
4394
4996
  int step = Math.Max(4, Math.Min(64, GetI(a, "step", 8)));
4395
4997
  double diffPct = GetD(a, "diffPct", 0.30);
4396
4998
  int waited, estimate; double lastDiff;
4397
- object scope; Func<byte[]> sample = WaitSampler(a, step, out scope);
4999
+ int rx, ry, rw, rh;
5000
+ object scope; Func<byte[]> sample = WaitSampler(a, step, out scope, out rx, out ry, out rw, out rh);
4398
5001
  bool ok = SettleSampleWait(capMs, stableMs, pollMs, diffPct, sample, out waited, out lastDiff, out estimate);
5002
+ if (!ok)
5003
+ {
5004
+ // CARET / BLINK DETECTION (test report 2026-11-04): a blinking caret is a ~1-2 Hz
5005
+ // periodic sub-percent change that never settles — Notepad burned the full 8 s cap
5006
+ // on lastDiffPct 0.388%. Probe one extra window (~2.4 s covers several blink
5007
+ // cycles): if EVERY consecutive diff is tiny but nonzero, the only motion is the
5008
+ // caret — call it stable and say exactly why.
5009
+ int probes = Math.Max(6, 2400 / pollMs);
5010
+ byte[] prev = sample();
5011
+ double max = 0; bool zero = false, large = false;
5012
+ for (int i = 0; i < probes; i++)
5013
+ {
5014
+ Thread.Sleep(pollMs);
5015
+ byte[] cur = sample();
5016
+ double d = SigDiffPct(prev, cur, 8);
5017
+ prev = cur;
5018
+ if (d == 0) zero = true;
5019
+ if (d > 1.0) large = true;
5020
+ if (d > max) max = d;
5021
+ }
5022
+ if (!large && !zero && max < 1.0)
5023
+ return Dict("stable", true, "waitedMs", waited, "lastDiffPct", Math.Round(max, 3),
5024
+ "estimateMs", estimate, "scope", scope, "caretSteady", true,
5025
+ "note", "the only sampled motion is a small periodic change consistent with a blinking caret (~1-2 Hz); treated as stable. lastDiffPct here is the LARGEST probe difference. This still does not verify task completion.");
5026
+ }
4399
5027
  return Dict("stable", ok, "waitedMs", waited, "lastDiffPct", Math.Round(lastDiff, 3),
4400
5028
  "estimateMs", estimate, "scope", scope,
4401
5029
  "note", ok ? "sampled pixels settled; this does not verify task completion"
@@ -4408,10 +5036,12 @@ namespace DshComputerUse.Worker
4408
5036
  int pollMs = Math.Max(100, GetI(a, "pollMs", 220));
4409
5037
  int step = Math.Max(4, Math.Min(64, GetI(a, "step", 8)));
4410
5038
  double diffPct = GetD(a, "diffPct", 0.30);
4411
- object scope; Func<byte[]> sample = WaitSampler(a, step, out scope);
5039
+ int rx, ry, rw, rh;
5040
+ object scope; Func<byte[]> sample = WaitSampler(a, step, out scope, out rx, out ry, out rw, out rh);
4412
5041
  byte[] first = sample();
4413
5042
  long t0 = NowMs();
4414
5043
  int samples = 0;
5044
+ int cols = (rw - 1) / step + 1;
4415
5045
  while (NowMs() - t0 < timeoutMs)
4416
5046
  {
4417
5047
  Thread.Sleep(pollMs);
@@ -4419,7 +5049,15 @@ namespace DshComputerUse.Worker
4419
5049
  samples++;
4420
5050
  double d = SigDiffPct(first, cur, 8);
4421
5051
  if (d > diffPct)
4422
- return Dict("changed", true, "afterMs", NowMs() - t0, "diffPct", Math.Round(d, 3), "samples", samples, "scope", scope);
5052
+ {
5053
+ // WHERE it changed (test report 2026-11-04): 0.344% cannot tell a sent
5054
+ // message from a caret blink; the bounding box can — input area vs caret.
5055
+ int[] bounds = SigDiffBounds(first, cur, 8, cols, step, rx, ry);
5056
+ Dictionary<string, object> hit = Dict("changed", true, "afterMs", NowMs() - t0,
5057
+ "diffPct", Math.Round(d, 3), "samples", samples, "scope", scope);
5058
+ if (bounds != null) hit["changeBounds"] = Dict("x", bounds[0], "y", bounds[1], "width", bounds[2], "height", bounds[3]);
5059
+ return hit;
5060
+ }
4423
5061
  }
4424
5062
  return Dict("changed", false, "afterMs", NowMs() - t0, "samples", samples, "scope", scope,
4425
5063
  "note", "no sampled pixel change exceeded the threshold; inspect task state before retrying a write");
@@ -4665,6 +5303,7 @@ namespace DshComputerUse.Worker
4665
5303
  KeyboardChord.Run(vks, SendKey, ReleaseKeyForCleanup,
4666
5304
  delegate(ushort vk) { return (Native.GetAsyncKeyState(vk) & 0x8000) != 0; },
4667
5305
  delegate { WaitForKeyHold(Math.Max(20, GetI(a, "holdMs", 60))); });
5306
+ EndActuationForReadback();
4668
5307
  Thread.Sleep(30);
4669
5308
  Dictionary<string, object> keyOut = Dict("combo", combo, "focus", focus);
4670
5309
  if (_lastRefocus != null) keyOut["refocused"] = _lastRefocus;
@@ -4717,6 +5356,24 @@ namespace DshComputerUse.Worker
4717
5356
  {
4718
5357
  CheckHumanInput();
4719
5358
  ClipboardLease lease = ClipboardLease.Begin(access, text);
5359
+ if (lease == null)
5360
+ {
5361
+ // UNPRESERVABLE CLIPBOARD, 2026-11-04: the previous clipboard holds
5362
+ // private/oversized content we cannot snapshot. Overwrite and paste
5363
+ // anyway — a lost opaque clipboard item is recoverable, while the old
5364
+ // refusal fed CJK text to the unicode fallback and an IME-garbled
5365
+ // message actually went out (WeChat, format 49161).
5366
+ MarkMutation();
5367
+ Clipboard.SetDataObject(text, true);
5368
+ Thread.Sleep(60);
5369
+ Key(Dict("combo", "ctrl+v", "holdMs", 40, "indicatorMs", 800));
5370
+ Dictionary<string, object> downgraded = Dict("mode", "paste", "chars", text.Length, "focus", focus,
5371
+ "clipboardRestore", "skipped-unpreservable-clipboard");
5372
+ 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
+ if (typeWarn != null) downgraded["warning"] = typeWarn;
5374
+ if (_lastRefocus != null) downgraded["refocused"] = _lastRefocus;
5375
+ return downgraded;
5376
+ }
4720
5377
  Exception inputError = null;
4721
5378
  try
4722
5379
  {
@@ -4820,12 +5477,18 @@ namespace DshComputerUse.Worker
4820
5477
 
4821
5478
  static Dictionary<string, object> Windows(Dictionary<string, object> a)
4822
5479
  {
5480
+ uint self = (uint)System.Diagnostics.Process.GetCurrentProcess().Id;
4823
5481
  List<object> list = new List<object>();
4824
5482
  Native.EnumWindows(delegate(IntPtr h, IntPtr l)
4825
5483
  {
4826
5484
  if (!Native.IsWindowVisible(h)) return true;
4827
5485
  StringBuilder sb = new StringBuilder(512);
4828
5486
  if (Native.GetWindowText(h, sb, 512) == 0) return true;
5487
+ // Hide this worker's own click-through indicator overlay (test report 2026-11-04):
5488
+ // it is a full-screen topmost layer, never a target, and listing it only invites
5489
+ // the model to "activate" the thing that must never take focus.
5490
+ uint pid; Native.GetWindowThreadProcessId(h, out pid);
5491
+ if (pid == self) return true;
4829
5492
  list.Add(WinDict(h));
4830
5493
  return true;
4831
5494
  }, IntPtr.Zero);
@@ -4886,15 +5549,16 @@ namespace DshComputerUse.Worker
4886
5549
  // The whole ladder now lives in RefocusWindow (shared with the keyboard focus
4887
5550
  // guard, which must re-assert the same window before typing).
4888
5551
  string how = RefocusWindow(h, GetB(a, "clickFallback", true));
4889
- if (how == null || !Native.IsWindow(h) || !IsForeground(h)) how = "failed";
5552
+ uint confirmedPid = 0;
5553
+ if (how == null || !Native.IsWindow(h) || !IsForeground(h) || (confirmedPid = PidOf(h)) == 0) how = "failed";
4890
5554
  else
4891
5555
  {
4892
5556
  SetSticky(h);
4893
- _kickoffFocus.Confirm(h.ToInt64(), PidOf(h));
5557
+ _kickoffFocus.Confirm(h.ToInt64(), confirmedPid);
4894
5558
  }
4895
5559
  CheckHumanInput();
4896
5560
  return Dict("activated", how != "failed", "activatedVia", how,
4897
- "hwnd", h.ToInt64(),
5561
+ "hwnd", h.ToInt64(), "pid", confirmedPid == 0 ? null : (object)confirmedPid,
4898
5562
  "hint", how == "failed"
4899
5563
  ? "foreground lock refused every method; click the window (or its taskbar button) and retry"
4900
5564
  : null);
@@ -4933,13 +5597,21 @@ namespace DshComputerUse.Worker
4933
5597
  IntPtr fg = TopLevel(Native.GetForegroundWindow());
4934
5598
  string problem = _kickoffFocus.CaptureProblem(Panic.CycleLit, Panic.Stopped, Panic.Exited,
4935
5599
  target != IntPtr.Zero && Native.IsWindow(target), fg.ToInt64(), PidOf(fg), IsHostWindow(fg));
4936
- if (problem != null) throw new Exception(
4937
- "KICKOFF-FOCUS-REQUIRED: " + problem + ". No screenshot was returned. " +
4938
- "First use computer_state {windows:true} to identify the intended app by window/process. " +
4939
- "Then computer_window {hwnd:...,op:'activate'}: it restores minimized windows and tries " +
4940
- "the foreground/attachment, topmost and verified-titlebar fallback methods. " +
4941
- "Require activated:true before taking a screenshot. If activation fails, inspect the text " +
4942
- "result and re-identify the target; do not treat the agent desktop as the task page.");
5600
+ if (problem != null)
5601
+ {
5602
+ // NAME THE ACTUAL FOREGROUND (test report 2026-11-04): "identify the intended
5603
+ // window" without saying who holds the foreground forced an extra computer_state
5604
+ // round trip just to learn the one fact this refusal already measured.
5605
+ StringBuilder fgTitle = new StringBuilder(256);
5606
+ if (fg != IntPtr.Zero) Native.GetWindowText(fg, fgTitle, fgTitle.Capacity);
5607
+ throw new Exception(
5608
+ "KICKOFF-FOCUS-REQUIRED: " + problem + ". No screenshot was returned. " +
5609
+ "Foreground now: hwnd=" + fg.ToInt64() + ", pid=" + PidOf(fg) +
5610
+ ", title=\"" + fgTitle.ToString() + "\". " +
5611
+ "If the reported foreground HWND and PID match the target already verified for this task, reactivate it. " +
5612
+ "Otherwise identify the intended window with computer_state {windows:true}. " +
5613
+ "Use computer_window {hwnd:...,op:'activate'} and require activated:true before retrying the screenshot.");
5614
+ }
4943
5615
  }
4944
5616
 
4945
5617
  static IntPtr TopLevel(IntPtr h)
@@ -5550,8 +6222,8 @@ namespace DshComputerUse.Worker
5550
6222
  {
5551
6223
  CheckHumanInput();
5552
6224
  if (h == IntPtr.Zero || !Native.IsWindow(h)) return null;
5553
- if (!Native.IsWindowVisible(h)) Native.ShowWindow(h, Native.SW_SHOWNOACTIVATE);
5554
- if (Native.IsIconic(h)) Native.ShowWindow(h, Native.SW_RESTORE);
6225
+ if (!Native.IsWindowVisible(h)) { MarkMutation(); Native.ShowWindow(h, Native.SW_SHOWNOACTIVATE); }
6226
+ if (Native.IsIconic(h)) { MarkMutation(); Native.ShowWindow(h, Native.SW_RESTORE); }
5555
6227
  Thread.Sleep(40);
5556
6228
  CheckHumanInput();
5557
6229
  if (IsForeground(h)) return "already-foreground";
@@ -6018,8 +6690,7 @@ namespace DshComputerUse.Worker
6018
6690
  ", automationId=" + (autoId ?? "-") + ", role=" + (role ?? "-") +
6019
6691
  ", index=" + index + ") among " + seen + " elements");
6020
6692
 
6021
- // This changes the UI, so the human gets the cue — but there is no pointer to
6022
- // fight, so the quiet gate is skipped on purpose.
6693
+ // A UIA mutation shares the same actual-action boundary and takeover checks.
6023
6694
  // A selector WITHOUT hwnd searches the whole desktop, so there the element that
6024
6695
  // actually matched decides the victim - and a host control is never driven through
6025
6696
  // its automation pattern either.
@@ -6028,11 +6699,9 @@ namespace DshComputerUse.Worker
6028
6699
  HostGuardPid(foundPid, "uiaAct");
6029
6700
  Panic.Check("uiaAct");
6030
6701
  CheckHumanInput();
6031
- Glow.Touch(GetI(a, "indicatorMs", 1200)); // same reason: see AgentActing
6032
6702
 
6033
6703
  object pat = null;
6034
6704
  string via = null;
6035
- MarkMutation();
6036
6705
  switch (action)
6037
6706
  {
6038
6707
  case "invoke":
@@ -6064,6 +6733,7 @@ namespace DshComputerUse.Worker
6064
6733
  default:
6065
6734
  throw new Exception("action must be invoke|select|expand|collapse|toggle|setValue|focus|scrollIntoView");
6066
6735
  }
6736
+ EndActuationForReadback();
6067
6737
  if (via == null)
6068
6738
  throw new Exception("element does not support '" + action + "': " + ElemDict(found)["name"]);
6069
6739
 
@@ -6356,6 +7026,7 @@ namespace DshComputerUse.Worker
6356
7026
  static Dictionary<string, object> Err(Exception error)
6357
7027
  {
6358
7028
  var result = Err(error.Message);
7029
+ if (error.Data["glowHealth"] != null) result["glowHealth"] = error.Data["glowHealth"];
6359
7030
  if (error.Data["code"] != null)
6360
7031
  {
6361
7032
  result["code"] = (string)error.Data["code"];
@@ -6373,3 +7044,79 @@ namespace DshComputerUse.Worker
6373
7044
  static void WriteLine(string s) { lock (OutLock) { Console.Out.WriteLine(s); Console.Out.Flush(); } }
6374
7045
  }
6375
7046
  }
7047
+
7048
+ // Integrated verbatim into worker.cs by the native worker build. Tests compile this
7049
+ // production class without starting hooks, overlays or desktop input.
7050
+ namespace DshComputerUse.Worker
7051
+ {
7052
+ public sealed class DesktopOwnershipLease : System.IDisposable
7053
+ {
7054
+ readonly object sync = new object();
7055
+ readonly string path;
7056
+ System.IO.FileStream stream;
7057
+ public DesktopOwnershipLease(string file) { path = file; }
7058
+ public bool Owned { get { lock (sync) { return stream != null; } } }
7059
+ public static string DefaultPath()
7060
+ {
7061
+ string isolated = System.Environment.GetEnvironmentVariable("DSH_COMPUTER_USE_OWNER_FILE");
7062
+ if (!string.IsNullOrEmpty(isolated)) return isolated;
7063
+ return System.IO.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData),
7064
+ "dsh-computer-use", "desktop-" + System.Diagnostics.Process.GetCurrentProcess().SessionId + ".owner");
7065
+ }
7066
+ public bool TryAcquire(string owner, out string occupiedBy)
7067
+ {
7068
+ lock (sync)
7069
+ {
7070
+ occupiedBy = "";
7071
+ if (stream != null) return true;
7072
+ System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path)));
7073
+ System.IO.FileStream opened;
7074
+ try
7075
+ {
7076
+ // A persistent handle is the lock. Never delete the pathname, use a
7077
+ // timer, or steal based on PID: Windows releases it when the process dies.
7078
+ opened = new System.IO.FileStream(path, System.IO.FileMode.OpenOrCreate,
7079
+ System.IO.FileAccess.ReadWrite, System.IO.FileShare.Read);
7080
+ }
7081
+ catch (System.IO.IOException error)
7082
+ {
7083
+ int code = System.Runtime.InteropServices.Marshal.GetHRForException(error) & 0xFFFF;
7084
+ if (code != 32 && code != 33) throw;
7085
+ occupiedBy = ReadOwner();
7086
+ return false;
7087
+ }
7088
+ try
7089
+ {
7090
+ byte[] record = System.Text.Encoding.UTF8.GetBytes("pid=" + System.Diagnostics.Process.GetCurrentProcess().Id +
7091
+ "\nowner=" + (owner ?? "worker").Replace("\r", " ").Replace("\n", " ") + "\n");
7092
+ opened.SetLength(0); opened.Write(record, 0, record.Length); opened.Flush();
7093
+ stream = opened;
7094
+ return true;
7095
+ }
7096
+ catch { opened.Dispose(); throw; }
7097
+ }
7098
+ }
7099
+ string ReadOwner()
7100
+ {
7101
+ try
7102
+ {
7103
+ using (var input = new System.IO.FileStream(path, System.IO.FileMode.Open,
7104
+ System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite))
7105
+ using (var reader = new System.IO.StreamReader(input))
7106
+ {
7107
+ char[] buffer = new char[512];
7108
+ return new string(buffer, 0, reader.Read(buffer, 0, buffer.Length)).Trim();
7109
+ }
7110
+ }
7111
+ catch { return "another live computer-use worker"; }
7112
+ }
7113
+ public void Dispose()
7114
+ {
7115
+ lock (sync)
7116
+ {
7117
+ if (stream == null) return;
7118
+ stream.Dispose(); stream = null;
7119
+ }
7120
+ }
7121
+ }
7122
+ }