dsh-codex-style-computer-use 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -32,15 +32,14 @@ using System.Windows.Forms; // Clipboard (watch out: bare Point stays our own st
32
32
 
33
33
  namespace DshComputerUse.Worker
34
34
  {
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.
35
+ // A live cycle observes physical holds, but only actual actuation can start a handoff.
36
+ // Once yielded, three quiet seconds with no held input returns control to the agent.
38
37
  public sealed class HumanInputState
39
38
  {
40
39
  public const int SustainedMs = 2000, IdleMs = 3000;
41
40
  readonly object sync = new object();
42
41
  readonly HashSet<int> held = new HashSet<int>();
43
- bool enabled, seen;
42
+ bool enabled, acting, seen;
44
43
  string phase = "idle", lastKind = "none";
45
44
  long first, last, revision, burst;
46
45
  public string Phase { get { lock (sync) { return phase; } } }
@@ -67,8 +66,18 @@ namespace DshComputerUse.Worker
67
66
  {
68
67
  if (enabled == on) return;
69
68
  enabled = on;
70
- if (!on) { phase = "idle"; return; }
71
- if (held.Count > 0 || (seen && now - last < IdleMs)) Start(now);
69
+ if (!on) { phase = "idle"; acting = false; }
70
+ }
71
+ }
72
+ public void SetActing(bool on, long now)
73
+ {
74
+ lock (sync)
75
+ {
76
+ acting = on;
77
+ // Do not replay earlier ordinary activity. A key/button still physically
78
+ // held does need ownership before we can inject a conflicting chord.
79
+ if (on && enabled && phase == "idle" && held.Count > 0)
80
+ { last = now; revision++; Start(now); }
72
81
  }
73
82
  }
74
83
  void Start(long now) { phase = "yielding"; first = now; if (!seen) last = now; burst++; }
@@ -79,8 +88,9 @@ namespace DshComputerUse.Worker
79
88
  bool continued = held.Count > 0 || (seen && now - last < IdleMs);
80
89
  lastKind = key == 0 ? "pointer-or-wheel" : key <= 6 ? "button" : "keyboard";
81
90
  if (key != 0) { if (down) held.Add(key); if (up) held.Remove(key); }
82
- seen = true; last = now; revision++;
83
- if (!enabled) return;
91
+ seen = true; last = now;
92
+ if (!enabled || (phase == "idle" && !acting)) return;
93
+ revision++;
84
94
  if (phase == "idle" || !continued) Start(now);
85
95
  if (now - first >= SustainedMs) phase = "waiting";
86
96
  }
@@ -103,6 +113,35 @@ namespace DshComputerUse.Worker
103
113
  }
104
114
  }
105
115
 
116
+ // Sum only physical displacement, rebasing every injected cursor event. Agent
117
+ // motion cannot turn a 1-pixel hand tremor into a takeover. Back-and-forth jitter
118
+ // cancels rather than accumulating path length; a deliberate 16-pixel move yields.
119
+ public sealed class PointerHandoffTolerance
120
+ {
121
+ public const int DistancePx = 16, BurstGapMs = 250;
122
+ readonly object sync = new object();
123
+ bool seen;
124
+ int x, y;
125
+ long dx, dy, lastPhysical;
126
+ public void Reset(int px, int py)
127
+ { lock (sync) { seen = true; x = px; y = py; dx = dy = 0; lastPhysical = 0; } }
128
+ public bool Record(int px, int py, bool physical, bool acting, bool yielded, long now)
129
+ {
130
+ lock (sync)
131
+ {
132
+ long mx = seen ? (long)px - x : 0, my = seen ? (long)py - y : 0;
133
+ x = px; y = py; seen = true;
134
+ if (!physical) return false;
135
+ if (!acting && !yielded) { dx = dy = 0; lastPhysical = now; return false; }
136
+ if (mx == 0 && my == 0) return false;
137
+ if (yielded) return true;
138
+ if (now - lastPhysical > BurstGapMs) dx = dy = 0;
139
+ lastPhysical = now; dx += mx; dy += my;
140
+ return dx * dx + dy * dy >= DistancePx * DistancePx;
141
+ }
142
+ }
143
+ }
144
+
106
145
  // A temporary pause is an explicit, single-use claim. No clock grants permission.
107
146
  public sealed class RecoveryPermit
108
147
  {
@@ -474,40 +513,45 @@ namespace DshComputerUse.Worker
474
513
  [DllImport("user32.dll")] static extern bool UpdateLayeredWindow(IntPtr h, IntPtr dst, ref POINTL pptDst, ref SIZEL psize,
475
514
  IntPtr src, ref POINTL pptSrc, int colorKey, ref BLENDFUNCTION blend, int flags);
476
515
  [DllImport("user32.dll")] static extern int GetSystemMetrics(int i);
516
+ [DllImport("user32.dll")] static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);
477
517
 
478
518
  static Thread _thread;
479
- static volatile int _lastTouchMs; // 0 = never; else Environment.TickCount of the last actuation
480
- static volatile int _holdMs = 900;
519
+ static volatile bool _acting;
520
+ static volatile bool _preparing;
521
+ static volatile int _preparationStart;
522
+ static int _preparationSerial, _presentedPreparation;
523
+ public const int PreparationMs = 1500;
481
524
  static volatile bool _enabled = true;
482
525
  static volatile string _label = "AI 正在操控此电脑 · 监听中";
483
526
 
484
- /// Mark "the agent is acting right now" — the glow stays lit for holdMs afterwards.
527
+ /// Legacy indicator requests announce monitoring; only real input can paint deep blue.
485
528
  public static void Touch(int holdMs)
486
529
  {
487
530
  if (!_enabled) return;
488
- _holdMs = holdMs < 200 ? 200 : holdMs;
489
- _lastTouchMs = Environment.TickCount;
531
+ _thinking = true;
490
532
  EnsureStarted();
491
533
  }
492
- public static void Off() { _lastTouchMs = 0; _thinking = false; }
534
+ public static void Off() { _acting = false; _preparing = false; _thinking = false; }
535
+ public static bool Acting { get { return _acting; } }
536
+ public static bool Preparing { get { return _preparing; } }
537
+ public static bool PreparationVisible { get { return _preparing && Volatile.Read(ref _presentedPreparation) == Volatile.Read(ref _preparationSerial); } }
538
+ public static void ActuationBegin() { _acting = true; EnsureStarted(); }
539
+ public static void ActuationEnd() { _acting = false; }
540
+ public static void Prepare(bool on)
541
+ {
542
+ if (!on) { _preparing = false; return; }
543
+ _preparationStart = 0;
544
+ Interlocked.Increment(ref _preparationSerial);
545
+ _preparing = true; _enabled = true; EnsureStarted();
546
+ }
493
547
 
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.
548
+ // Nested injection sequences stay observable across drag/key/typing gaps.
549
+ // The enclosing actual action scope controls ownership and the deep-blue cue.
501
550
  static int _injecting = 0; // refcount; Interlocked supplies the barriers
502
551
  public static void InputBegin() { Interlocked.Increment(ref _injecting); }
503
552
  public static void InputEnd() { if (Interlocked.Decrement(ref _injecting) < 0) _injecting = 0; }
504
553
  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; }
554
+ public static void KeepActing() { /* Actual operation scope owns the cue, with no trailing hold. */ }
511
555
 
512
556
  // Emergency stop must LOOK like one. The normal fade is a slow ease (~1.6 s) which,
513
557
  // during a panic, reads as "nothing happened" — so this snaps the overlay to zero on
@@ -515,7 +559,7 @@ namespace DshComputerUse.Worker
515
559
  static volatile bool _killNow = false;
516
560
  public static void Kill()
517
561
  {
518
- _lastTouchMs = 0; _thinking = false; _flashStart = 0;
562
+ _acting = false; _preparing = false; _thinking = false; _flashStart = 0;
519
563
  _killNow = true;
520
564
  }
521
565
 
@@ -567,7 +611,7 @@ namespace DshComputerUse.Worker
567
611
  public static int DeriveWant(bool flashOn, int committed)
568
612
  {
569
613
  if (Panic.Exited) return 0;
570
- return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (flashOn ? 2 : committed)));
614
+ return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (_preparing ? 7 : (Acting ? 1 : (flashOn ? 2 : committed)))));
571
615
  }
572
616
  /// The title states the same fact as the light, derived the same way (an ended session must
573
617
  /// not advertise "已暂停 · Ctrl+Alt+R 继续" in the window list).
@@ -575,7 +619,7 @@ namespace DshComputerUse.Worker
575
619
  {
576
620
  if (Panic.Exited) return "AI 已停止操控 · 未监听";
577
621
  return _askMode ? "AI 正在提问 · 在 DSH 聊天里回答 · 无倒计时(Ctrl+Alt+R 撤销)"
578
- : (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : label);
622
+ : (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : _preparing ? "即将开始操作,请暂停键鼠 · 1.5 秒准备" : label);
579
623
  }
580
624
  static volatile string _shownTitle = null;
581
625
  // What the indicator is ACTUALLY rendering right now, readable from outside via `probe`.
@@ -678,6 +722,7 @@ namespace DshComputerUse.Worker
678
722
  WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TOPMOST,
679
723
  "STATIC", _label, WS_POPUP, vx, vy, vw, vh, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
680
724
  if (hwnd == IntPtr.Zero) return;
725
+ TryExcludeOverlayFromCapture(hwnd);
681
726
  _hwnd = hwnd;
682
727
 
683
728
  IntPtr screenDc = GetDC(IntPtr.Zero);
@@ -703,37 +748,16 @@ namespace DshComputerUse.Worker
703
748
  int paintedFlash = 0;
704
749
  int alpha = 0;
705
750
  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;
751
+ int committed = 0;
709
752
  while (true)
710
753
  {
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));
754
+ // Ownership and deep blue use the same actual action scope. There is no
755
+ // delayed onset or lingering tail during reading, thinking or preparation.
756
+ bool acting = _enabled && Acting;
715
757
  bool thinking = ThinkingActive;
716
758
  bool flashOn = _flashStart != 0 && unchecked(Environment.TickCount - _flashStart) < _flashMs;
717
759
 
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
- }
760
+ committed = acting ? 1 : (thinking ? 3 : 0);
737
761
 
738
762
  // ONE derivation (Glow.DeriveWant): the exit flag outranks ask/stop/flash/committed,
739
763
  // so no later state change on another thread can paint a light on an ended session.
@@ -760,14 +784,14 @@ namespace DshComputerUse.Worker
760
784
  _killNow = false;
761
785
  alpha = 0;
762
786
  if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
763
- curBitmap = 0; committed = 0; pending = -1; paintedFlash = 0;
787
+ curBitmap = 0; committed = 0; paintedFlash = 0;
764
788
  MSG mk; while (PeekMessage(out mk, hwnd, 0, 0, PM_REMOVE)) { }
765
789
  Thread.Sleep(12);
766
790
  continue;
767
791
  }
768
792
  // want: 0 idle | 1 acting (deep blue) | 3 thinking (pale blue) | 2 flash (yellow)
769
793
  // 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)));
794
+ int needBitmap = want == 7 ? 6 : want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : (want == 1 ? 3 : (want == 4 || want == 5 ? 4 : 1)));
771
795
  // A REPAINT MUST REACH THE SCREEN EVEN WHEN ALPHA DOES NOT MOVE.
772
796
  //
773
797
  // The present below used to be gated on `diff != 0` alone, while the flash snaps
@@ -778,7 +802,8 @@ namespace DshComputerUse.Worker
778
802
  // RED frame when the border happened to be at full alpha already — whether the
779
803
  // brake was visible was luck. Present when EITHER the bitmap or the alpha changed.
780
804
  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 */ }
805
+ if (needBitmap == 6) { PaintPreparation(bits, vw, vh, PreparationVisible ? unchecked(Environment.TickCount - _preparationStart) : 0); curBitmap = 6; repaint = true; }
806
+ 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
807
  else if (needBitmap == 5 && curBitmap != 5) { PaintHumanYield(bits, vw, vh); curBitmap = 5; repaint = true; }
783
808
  else if (needBitmap == 4 && curBitmap != 4) { PaintStop(bits, vw, vh); curBitmap = 4; repaint = true; }
784
809
  else if (needBitmap == 3 && curBitmap != 3) { PaintBlue(bits, vw, vh, true); curBitmap = 3; repaint = true; }
@@ -790,7 +815,7 @@ namespace DshComputerUse.Worker
790
815
  int target;
791
816
  bool flashFrame = false;
792
817
  bool breathFrame = false; // breathing states are ABSOLUTE, never eased
793
- if (want == 0) target = 0;
818
+ if (want == 0) { target = 0; breathFrame = true; }
794
819
  else if (want == 2)
795
820
  {
796
821
  // The capture cue runs its OWN envelope (wind-up, burst, short fade): an absolute
@@ -798,7 +823,8 @@ namespace DshComputerUse.Worker
798
823
  target = FlashAlpha(unchecked(Environment.TickCount - _flashStart));
799
824
  flashFrame = true;
800
825
  }
801
- else if (want == 1) target = FULL;
826
+ else if (want == 1) { target = FULL; breathFrame = true; }
827
+ else if (want == 7) { target = 255; breathFrame = true; }
802
828
  else if (want == 4) target = FULL; // steady red, no breathing
803
829
  else if (want == 6)
804
830
  {
@@ -860,7 +886,11 @@ namespace DshComputerUse.Worker
860
886
  SetWindowPos(hwnd, new IntPtr(-1), vx, vy, vw, vh, 0x0004 | 0x0010 | 0x0040);
861
887
  visible = true;
862
888
  }
863
- UpdateLayeredWindow(hwnd, screenDc, ref dst, ref size, memDc, ref src, 0, ref bf, ULW_ALPHA);
889
+ if (UpdateLayeredWindow(hwnd, screenDc, ref dst, ref size, memDc, ref src, 0, ref bf, ULW_ALPHA) && want == 7 && !PreparationVisible)
890
+ {
891
+ _preparationStart = Environment.TickCount;
892
+ Volatile.Write(ref _presentedPreparation, Volatile.Read(ref _preparationSerial));
893
+ }
864
894
  }
865
895
  else if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
866
896
  }
@@ -870,146 +900,154 @@ namespace DshComputerUse.Worker
870
900
  }
871
901
  }
872
902
 
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)
903
+ // Thin, square screen-edge rails leave the application itself completely clear.
904
+ // Each pixel belongs to exactly one inset ring: corners meet without gaps or
905
+ // overlapping hot spots, and the inward bloom ends after eight physical pixels.
906
+ static void PaintEdgeRail(System.Drawing.Graphics g, Rectangle bounds, Color tint)
886
907
  {
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++)
908
+ int[] alpha = { 244, 216, 94, 53, 30, 16, 8, 3 };
909
+ var smoothing = g.SmoothingMode;
910
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
911
+ try
912
+ {
913
+ for (int i = 0; i < alpha.Length; i++)
908
914
  {
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))
915
+ int x = bounds.Left + i, y = bounds.Top + i;
916
+ int width = bounds.Width - 2 * i, height = bounds.Height - 2 * i;
917
+ if (width <= 0 || height <= 0) break;
918
+ Color start = Color.FromArgb(alpha[i], Math.Min(255, tint.R + 18), Math.Min(255, tint.G + 14), Math.Min(255, tint.B + 8));
919
+ Color end = Color.FromArgb(alpha[i], tint.R, tint.G, tint.B);
920
+ using (var brush = new System.Drawing.Drawing2D.LinearGradientBrush(
921
+ new RectangleF(bounds.X, bounds.Y, Math.Max(1, bounds.Width), Math.Max(1, bounds.Height)), start, end, 32f))
915
922
  {
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();
923
+ g.FillRectangle(brush, x, y, width, 1);
924
+ if (height > 1) g.FillRectangle(brush, x, y + height - 1, width, 1);
925
+ if (height > 2)
926
+ {
927
+ g.FillRectangle(brush, x, y + 1, 1, height - 2);
928
+ if (width > 1) g.FillRectangle(brush, x + width - 1, y + 1, 1, height - 2);
929
+ }
920
930
  }
921
931
  }
922
- PaintEscBadge(g, intense ? 238 : 148);
932
+ }
933
+ finally { g.SmoothingMode = smoothing; }
934
+ }
935
+
936
+ static Rectangle OverlayDisplay(int w, int h)
937
+ {
938
+ var display = new Rectangle(0, 0, w, h);
939
+ if (w == Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN) && h == Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN))
940
+ {
941
+ display = Screen.PrimaryScreen.Bounds;
942
+ display.Offset(-Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), -Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN));
943
+ }
944
+ return display;
945
+ }
946
+
947
+ // WDA_EXCLUDEFROMCAPTURE must not be sent on older Windows, where its fallback
948
+ // can black out the covered image. Unsupported APIs simply leave the overlay visible.
949
+ static void TryExcludeOverlayFromCapture(IntPtr hwnd)
950
+ {
951
+ try
952
+ {
953
+ object build = Microsoft.Win32.Registry.GetValue(
954
+ @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "CurrentBuildNumber", null);
955
+ int number;
956
+ if (int.TryParse(Convert.ToString(build), out number) && number >= 19041)
957
+ SetWindowDisplayAffinity(hwnd, 0x00000011);
958
+ }
959
+ catch { /* display preference only; never changes control or capture behavior */ }
960
+ }
961
+
962
+ static void PaintBlue(IntPtr bits, int w, int h, bool intense)
963
+ {
964
+ WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
965
+ PaintEdgeRail(g, new Rectangle(0, 0, w, h), intense
966
+ ? Color.FromArgb(49, 116, 248) : Color.FromArgb(36, 206, 216));
923
967
  });
924
968
  }
925
969
 
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
970
  public static int HumanYieldAlpha(int tick)
930
971
  { return 48 + (int)Math.Round(30 * (1 - Math.Cos((unchecked((uint)tick) % 2800) * 2 * Math.PI / 2800.0))); }
931
972
  static void PaintHumanYield(IntPtr bits, int w, int h)
932
973
  {
933
974
  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);
975
+ Color tint = Color.FromArgb(238, 190, 102);
976
+ PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
977
+ PaintStatusChip(g, w, h, "已让出键鼠 · 静默 3 秒后接续", tint);
944
978
  });
945
979
  }
946
980
 
947
- static void PaintEscBadge(System.Drawing.Graphics g, int opacity)
981
+ static void PaintPreparation(IntPtr bits, int w, int h, int elapsed)
948
982
  {
949
- PaintBadge(g, "监听中 · Ctrl+Esc 暂停 · Ctrl+Alt+Q 退出", opacity, false);
983
+ WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
984
+ Color tint = Color.FromArgb(166, 139, 250);
985
+ PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
986
+ Rectangle display = OverlayDisplay(w, h);
987
+ float scale = Math.Max(1f, Math.Min(1.25f, display.Width / 1920f));
988
+ float bw = Math.Min(408 * scale, display.Width - 24), bh = 64 * scale;
989
+ float x = display.Left + (display.Width - bw) / 2, y = display.Top + 10 * scale;
990
+ float progress = Math.Max(0f, Math.Min(1f, elapsed / (float)PreparationMs));
991
+ using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
992
+ using (var fill = new System.Drawing.Drawing2D.LinearGradientBrush(new RectangleF(x, y, bw, bh),
993
+ Color.FromArgb(224, 23, 27, 39), Color.FromArgb(224, 35, 31, 53), 0f))
994
+ using (var outline = new Pen(Color.FromArgb(110, tint), 1f))
995
+ using (var title = new Font("Microsoft YaHei UI", 12.5f * scale, FontStyle.Bold))
996
+ using (var detail = new Font("Microsoft YaHei UI", 9f * scale))
997
+ using (var white = new SolidBrush(Color.FromArgb(255, 246, 247, 253)))
998
+ using (var soft = new SolidBrush(Color.FromArgb(238, 183, 188, 207)))
999
+ using (var ring = new Pen(Color.FromArgb(70, tint), 2 * scale))
1000
+ using (var accent = new Pen(Color.FromArgb(245, tint), 2 * scale))
1001
+ using (var track = new SolidBrush(Color.FromArgb(52, tint)))
1002
+ using (var bar = new System.Drawing.Drawing2D.LinearGradientBrush(
1003
+ new RectangleF(x + 48 * scale, y + 56 * scale, bw - 64 * scale, 2 * scale),
1004
+ Color.FromArgb(220, 167, 139, 250), Color.FromArgb(235, 96, 165, 250), 0f))
1005
+ {
1006
+ AddRound(shape, x, y, bw, bh, 14 * scale); g.FillPath(fill, shape); g.DrawPath(outline, shape);
1007
+ g.DrawEllipse(ring, x + 16 * scale, y + 20 * scale, 20 * scale, 20 * scale);
1008
+ if (progress > 0) g.DrawArc(accent, x + 16 * scale, y + 20 * scale, 20 * scale, 20 * scale, -90, 360 * progress);
1009
+ g.DrawString("即将开始操作,请暂停键鼠", title, white, x + 48 * scale, y + 9 * scale);
1010
+ g.DrawString("Ctrl+Esc 暂停", detail, soft, x + 48 * scale, y + 34 * scale);
1011
+ string remaining = (Math.Max(0, PreparationMs - elapsed) / 1000.0).ToString("0.0") + " 秒后开始";
1012
+ SizeF size = g.MeasureString(remaining, detail);
1013
+ g.DrawString(remaining, detail, soft, x + bw - 16 * scale - size.Width, y + 34 * scale);
1014
+ g.FillRectangle(track, x + 48 * scale, y + 56 * scale, bw - 64 * scale, 2 * scale);
1015
+ if (progress > 0) g.FillRectangle(bar, x + 48 * scale, y + 56 * scale, (bw - 64 * scale) * progress, 2 * scale);
1016
+ }
1017
+ });
950
1018
  }
951
1019
 
952
- static void PaintBadge(System.Drawing.Graphics g, string txt, int opacity, bool red)
1020
+ static void PaintStatusChip(System.Drawing.Graphics g, int w, int h, string text, Color tint)
953
1021
  {
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))
1022
+ Rectangle display = OverlayDisplay(w, h);
1023
+ float scale = Math.Max(1f, Math.Min(1.25f, display.Width / 1920f));
1024
+ using (var font = new Font("Microsoft YaHei UI", 9f * scale))
959
1025
  {
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);
1026
+ SizeF measured = g.MeasureString(text, font);
1027
+ float bw = Math.Min(measured.Width + 38 * scale, display.Width - 24), bh = 28 * scale;
1028
+ float x = display.Left + (display.Width - bw) / 2, y = display.Top + 8 * scale;
1029
+ using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
1030
+ using (var fill = new SolidBrush(Color.FromArgb(222, 22, 26, 36)))
1031
+ using (var outline = new Pen(Color.FromArgb(90, tint), 1f))
1032
+ using (var accent = new SolidBrush(Color.FromArgb(255, tint)))
1033
+ using (var ink = new SolidBrush(Color.FromArgb(250, 235, 239, 248)))
1034
+ {
1035
+ AddRound(shape, x, y, bw, bh, 12 * scale); g.FillPath(fill, shape); g.DrawPath(outline, shape);
1036
+ g.FillEllipse(accent, x + 12 * scale, y + 11 * scale, 6 * scale, 6 * scale);
1037
+ g.DrawString(text, font, ink, x + 24 * scale, y + (bh - measured.Height) / 2);
1038
+ }
979
1039
  }
980
1040
  }
981
1041
 
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
1042
  static void PaintStop(IntPtr bits, int w, int h)
985
1043
  {
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);
1044
+ WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
1045
+ Color tint = Color.FromArgb(240, 84, 102);
1046
+ PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
1047
+ PaintStatusChip(g, w, h, "操作已暂停 · Ctrl+Alt+R 继续", tint);
1008
1048
  });
1009
1049
  }
1010
1050
 
1011
- /// Yellow "a screenshot was just taken" rectangle around the captured region —
1012
- /// same stepped-gradient treatment, no strokes, no hard edges.
1013
1051
  static void PaintFlash(IntPtr bits, int w, int h)
1014
1052
  {
1015
1053
  int fx = _fx, fy = _fy, fw = _fw, fh = _fh;
@@ -1019,32 +1057,8 @@ namespace DshComputerUse.Worker
1019
1057
  if (fx + fw > w) fw = w - fx;
1020
1058
  if (fy + fh > h) fh = h - fy;
1021
1059
  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
- }
1060
+ WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
1061
+ PaintEdgeRail(g, new Rectangle(fx, fy, fw, fh), Color.FromArgb(247, 193, 62));
1048
1062
  });
1049
1063
  }
1050
1064
 
@@ -1056,6 +1070,7 @@ namespace DshComputerUse.Worker
1056
1070
  using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp))
1057
1071
  {
1058
1072
  g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
1073
+ g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
1059
1074
  g.Clear(System.Drawing.Color.Transparent);
1060
1075
  draw(g);
1061
1076
  }
@@ -1232,6 +1247,7 @@ namespace DshComputerUse.Worker
1232
1247
  // separate statements, so a Q between the check and the write re-lit a dead cycle.
1233
1248
  lock (SyncLock)
1234
1249
  {
1250
+ Program.InvalidateActiveInput();
1235
1251
  _temporary = null;
1236
1252
  _engaged = false;
1237
1253
  _stopped = false;
@@ -1258,6 +1274,7 @@ namespace DshComputerUse.Worker
1258
1274
  {
1259
1275
  if (_exited) return false;
1260
1276
  if (!CycleLit) Program.ResetKickoffFocus();
1277
+ _controlsReleased = false;
1261
1278
  CycleLit = true; // an agent cycle is live: cyan is on (R1)
1262
1279
  Arm(); // "Computer use 一旦启用,立即挂打断监听"
1263
1280
  // A BRAKE OUTRANKS "THINKING" (cold-start bug, 2026-09-13): if a stop is already
@@ -1281,6 +1298,19 @@ namespace DshComputerUse.Worker
1281
1298
  return true;
1282
1299
  }
1283
1300
  }
1301
+ public static bool TryEndCycle()
1302
+ {
1303
+ SyncExitedFromFile(); SyncFromFile();
1304
+ lock (SyncLock)
1305
+ {
1306
+ if (_engaged || _stopped || PauseRequested) return false;
1307
+ _controlsReleased = true;
1308
+ CycleLit = false;
1309
+ Disarm(); Glow.Kill();
1310
+ Program.ReleaseOwnedInputs(); Program.ReleaseDesktopOwnership();
1311
+ return true;
1312
+ }
1313
+ }
1284
1314
 
1285
1315
  /// Was the record written after this process started? Only then is it news rather than an
1286
1316
  /// inherited fact. (The record carries its own `at=` line precisely so this is answerable
@@ -1355,7 +1385,7 @@ namespace DshComputerUse.Worker
1355
1385
  }
1356
1386
  // OUTSIDE the lock: Notify writes to stdout, and a full pipe must never hold the lock the
1357
1387
  // exit and the cycle-opener share.
1358
- if (adopted) ReleaseStuck(); // adopted exit == detected exit, cleanup included
1388
+ if (adopted) { ReleaseStuck(); Program.ReleaseDesktopOwnership(); }
1359
1389
  if (announce) Notify("exit", ExitSummary());
1360
1390
  }
1361
1391
 
@@ -1751,6 +1781,20 @@ namespace DshComputerUse.Worker
1751
1781
  } while (!ControlQueue.IsEmpty && Interlocked.CompareExchange(ref _controlWorkQueued, 1, 0) == 0);
1752
1782
  });
1753
1783
  }
1784
+ static void QueueDesktopControl(Action action, Action skipped = null)
1785
+ {
1786
+ QueueControl(delegate {
1787
+ // Serialize with graceful release: a queued physical hotkey may not
1788
+ // reopen a completed cycle or steer an exit into its continuing chat.
1789
+ lock (SyncLock)
1790
+ {
1791
+ if (!ControlListenersEnabled || !Program.TryClaimDesktop("physical control"))
1792
+ { if (skipped != null) skipped(); return; }
1793
+ try { action(); }
1794
+ finally { if (!CycleLit) Program.ReleaseDesktopOwnership(); }
1795
+ }
1796
+ });
1797
+ }
1754
1798
  public static string ControlHotkey(uint vk, bool down, bool human, bool ctrl, bool alt, bool shift, bool win, bool live)
1755
1799
  {
1756
1800
  if (!down || !human) return "";
@@ -1760,8 +1804,7 @@ namespace DshComputerUse.Worker
1760
1804
  return "";
1761
1805
  }
1762
1806
  static bool _suppressPauseEsc;
1763
- static bool _havePhysicalPoint;
1764
- static POINT _physicalPoint;
1807
+ static readonly PointerHandoffTolerance PointerMotion = new PointerHandoffTolerance();
1765
1808
  public static bool PhysicalEvent(IntPtr extra, uint flags, uint injectedMask)
1766
1809
  { return extra != OwnMagic && (flags & injectedMask) == 0; }
1767
1810
 
@@ -1773,15 +1816,18 @@ namespace DshComputerUse.Worker
1773
1816
  {
1774
1817
  var m = (Native.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(Native.MSLLHOOKSTRUCT));
1775
1818
  if (m.dwExtraInfo != OwnMagic) Interlocked.Increment(ref _externalInput);
1776
- if (PhysicalEvent(m.dwExtraInfo, m.flags, 3))
1819
+ bool physical = PhysicalEvent(m.dwExtraInfo, m.flags, 3);
1820
+ int message = wParam.ToInt32();
1821
+ // Significant idle motion invalidates observations, but does not yield.
1822
+ bool movement = message == 0x200 && PointerMotion.Record(m.pt.X, m.pt.Y, physical,
1823
+ true, Handoff.Active, HumanClock.ElapsedMilliseconds);
1824
+ if (physical)
1777
1825
  {
1778
- int message = wParam.ToInt32(), key = 0;
1826
+ int key = 0;
1779
1827
  bool down = false, up = false, activity = true;
1780
1828
  switch (message)
1781
1829
  {
1782
- case 0x200:
1783
- activity = !_havePhysicalPoint || m.pt.X != _physicalPoint.X || m.pt.Y != _physicalPoint.Y;
1784
- _physicalPoint = m.pt; _havePhysicalPoint = true; break;
1830
+ case 0x200: activity = movement; break;
1785
1831
  case 0x201: key = 1; down = true; break;
1786
1832
  case 0x202: key = 1; up = true; break;
1787
1833
  case 0x204: key = 2; down = true; break;
@@ -1820,8 +1866,8 @@ namespace DshComputerUse.Worker
1820
1866
  { if (up) _suppressPauseEsc = false; return new IntPtr(1); }
1821
1867
  bool ctrl = _humanCtrl && (Native.GetAsyncKeyState(Native.VK_CONTROL) & 0x8000) != 0;
1822
1868
  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);
1869
+ string hotkey = ControlListenersEnabled ? ControlHotkey(k.vkCode, down, human, ctrl, alt,
1870
+ Handoff.Held(0x10), Handoff.Held(0x5B) || Handoff.Held(0x5C), CycleLit && !_exited) : "";
1825
1871
  if (hotkey == "pause")
1826
1872
  {
1827
1873
  _suppressPauseEsc = true;
@@ -1829,19 +1875,19 @@ namespace DshComputerUse.Worker
1829
1875
  // The hook marks ownership immediately; do not perform SendInput or
1830
1876
  // filesystem work on this OS hook thread.
1831
1877
  string evidence = ChordEvidence(k, ctrl, alt);
1832
- QueueControl(delegate {
1878
+ QueueDesktopControl(delegate {
1833
1879
  try { Engage("Ctrl+Esc chord detected", false, "hook", evidence); }
1834
1880
  finally { Interlocked.Decrement(ref _pendingPauses); }
1835
- });
1881
+ }, delegate { Interlocked.Decrement(ref _pendingPauses); });
1836
1882
  return new IntPtr(1); // Ctrl+Esc must not also open the Windows Start menu.
1837
1883
  }
1838
1884
  if (hotkey == "exit")
1839
1885
  {
1840
1886
  string evidence = ChordEvidence(k, ctrl, alt);
1841
- QueueControl(delegate { Exit("Ctrl+Alt+Q chord detected", "hook", evidence); });
1887
+ QueueDesktopControl(delegate { Exit("Ctrl+Alt+Q chord detected", "hook", evidence); });
1842
1888
  }
1843
1889
  else if (hotkey == "resume")
1844
- QueueControl(delegate { Clear(); });
1890
+ QueueDesktopControl(delegate { Clear(); });
1845
1891
  }
1846
1892
  }
1847
1893
  }
@@ -1886,6 +1932,7 @@ namespace DshComputerUse.Worker
1886
1932
  // SendInputChecked (which now refuses after an exit): releasing what is stuck is exactly
1887
1933
  // the cleanup that must still be allowed.
1888
1934
  ReleaseStuck();
1935
+ Program.ReleaseDesktopOwnership();
1889
1936
  Program.AuditRecord("EXIT", _exitSource + "\t" + why + "\t" + _exitEvidence);
1890
1937
  Notify("exit", ExitSummary());
1891
1938
  }
@@ -1963,12 +2010,12 @@ namespace DshComputerUse.Worker
1963
2010
  bool r = (Native.GetAsyncKeyState(Native.VK_R) & 1) != 0;
1964
2011
  bool agentQuiet = _agentActAt == 0 || unchecked(Environment.TickCount - _agentActAt) > 400;
1965
2012
  if (warm < 3) warm++;
1966
- else if (_humanCtrl && _humanAlt && asyncCtrl && asyncAlt && agentQuiet)
2013
+ else if (ControlListenersEnabled && _humanCtrl && _humanAlt && asyncCtrl && asyncAlt && agentQuiet)
1967
2014
  {
1968
2015
  // R FIRST: Ctrl+Alt+R is the human's explicit re-arm, and a stale Q bit must
1969
2016
  // never beat it — pressing the key that brings computer use BACK used to be
1970
2017
  // able to END it instead.
1971
- if (r && !prevR) QueueControl(delegate { Clear(); });
2018
+ if (r && !prevR) QueueDesktopControl(delegate { Clear(); });
1972
2019
  else if (q && !prevQ)
1973
2020
  {
1974
2021
  Native.KBDLLHOOKSTRUCT kk = new Native.KBDLLHOOKSTRUCT();
@@ -1977,7 +2024,7 @@ namespace DshComputerUse.Worker
1977
2024
  "q-latch + hook-tracked modifiers + measured async key state " +
1978
2025
  "(no keydown was seen by the hook); " +
1979
2026
  ChordEvidence(kk, asyncCtrl, asyncAlt);
1980
- QueueControl(delegate { Exit("Ctrl+Alt+Q chord detected", "poll", evidence); });
2027
+ QueueDesktopControl(delegate { Exit("Ctrl+Alt+Q chord detected", "poll", evidence); });
1981
2028
  }
1982
2029
  }
1983
2030
  prevQ = q; prevR = r;
@@ -1993,6 +2040,8 @@ namespace DshComputerUse.Worker
1993
2040
  // durable manual brake and never creates a STOP record.
1994
2041
  static int _agentActAt;
1995
2042
  static volatile bool _armed = false;
2043
+ static volatile bool _controlsReleased = true;
2044
+ public static bool ControlListenersEnabled { get { return !_controlsReleased; } }
1996
2045
  public static volatile bool CycleLit;
1997
2046
  public static bool Armed { get { return _armed; } }
1998
2047
  public static bool Exited { get { return _exited; } }
@@ -2000,15 +2049,33 @@ namespace DshComputerUse.Worker
2000
2049
  static readonly HumanInputState Handoff = new HumanInputState();
2001
2050
  static readonly object HandoffPublishLock = new object();
2002
2051
  static string _publishedHandoff = "idle";
2052
+ static long _physicalInputEpoch, _publishedInputEpoch;
2003
2053
  static int _handoffWorkQueued;
2004
2054
  public static bool HumanActive { get { return Handoff.Active; } }
2005
2055
  public static long HumanRevision { get { return Handoff.Revision; } }
2006
2056
  public static bool HumanHolds(int vk) { return Handoff.Held(vk); }
2007
- public static object CooperationState() { return Handoff.Snapshot(HumanClock.ElapsedMilliseconds); }
2057
+ public static long InputEpoch { get { return Interlocked.Read(ref _physicalInputEpoch); } }
2058
+ public static object CooperationState()
2059
+ {
2060
+ var state = (Dictionary<string, object>)Handoff.Snapshot(HumanClock.ElapsedMilliseconds);
2061
+ state["inputEpoch"] = InputEpoch; state["mouseTolerancePx"] = PointerHandoffTolerance.DistancePx;
2062
+ return state;
2063
+ }
2008
2064
  static void ResetMonitorRun() { /* Manual recovery must not erase physical input history. */ }
2009
2065
  public static void Arm() { _armed = true; Handoff.Enable(CycleLit && !_exited, HumanClock.ElapsedMilliseconds); }
2010
2066
  public static void Disarm() { _armed = false; Handoff.Enable(false, HumanClock.ElapsedMilliseconds); PublishHandoff(); }
2011
2067
  public static void AgentActed() { _agentActAt = Environment.TickCount; }
2068
+ public static void BeginActuation()
2069
+ {
2070
+ POINT point; if (Native.GetCursorPos(out point)) PointerMotion.Reset(point.X, point.Y);
2071
+ Glow.ActuationBegin();
2072
+ Handoff.SetActing(true, HumanClock.ElapsedMilliseconds);
2073
+ }
2074
+ public static void EndActuation()
2075
+ {
2076
+ Handoff.SetActing(false, HumanClock.ElapsedMilliseconds);
2077
+ Glow.ActuationEnd();
2078
+ }
2012
2079
  static bool IsBareModifier(uint vk)
2013
2080
  {
2014
2081
  return vk == Native.VK_SHIFT || vk == Native.VK_CONTROL || vk == Native.VK_MENU ||
@@ -2017,6 +2084,7 @@ namespace DshComputerUse.Worker
2017
2084
  }
2018
2085
  public static void RecordHumanInput(int key, bool down, bool up)
2019
2086
  {
2087
+ Interlocked.Increment(ref _physicalInputEpoch);
2020
2088
  long now = HumanClock.ElapsedMilliseconds;
2021
2089
  bool was = Handoff.Active;
2022
2090
  Handoff.Enable(_armed && CycleLit && !_exited, now);
@@ -2032,15 +2100,20 @@ namespace DshComputerUse.Worker
2032
2100
  lock (HandoffPublishLock)
2033
2101
  {
2034
2102
  string phase = Handoff.Phase;
2035
- if (phase == _publishedHandoff) return;
2103
+ long epoch = InputEpoch;
2104
+ if (phase == _publishedHandoff && epoch == _publishedInputEpoch) return;
2105
+ bool changed = phase != _publishedHandoff;
2036
2106
  _publishedHandoff = phase;
2107
+ _publishedInputEpoch = epoch;
2108
+ if (!ControlListenersEnabled && !CycleLit && phase == "idle") return;
2037
2109
  if (phase != "idle") Glow.HumanYield();
2038
- Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + HumanRevision + "; kind=" + Handoff.LastKind);
2110
+ if (changed) Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + HumanRevision + "; kind=" + Handoff.LastKind);
2039
2111
  Notify("handoff", phase);
2040
2112
  }
2041
2113
  }
2042
2114
  static void MonitorHuman()
2043
2115
  {
2116
+ if (!ControlListenersEnabled) return;
2044
2117
  SyncExitedFromFile();
2045
2118
  long now = HumanClock.ElapsedMilliseconds;
2046
2119
  Handoff.Enable(_armed && CycleLit && !_exited, now);
@@ -2822,8 +2895,8 @@ namespace DshComputerUse.Worker
2822
2895
  static readonly object OutLock = new object(); // responses AND unsolicited events share stdout
2823
2896
  static readonly DateTime Epoch = new DateTime(1970, 1, 1);
2824
2897
 
2825
- // Position correction is diagnostic only. Physical input immediately yields;
2826
- // no distance, reversal, wheel-count or screen-corner rule latches a pause.
2898
+ // Position correction is diagnostic only. Deliberate physical takeover during
2899
+ // actual input yields; incidental mouse jitter and idle use do not pause the task.
2827
2900
  static int _positionCorrections;
2828
2901
  static string _dpiNote = "unset";
2829
2902
  static readonly HashSet<string> ObsOps = new HashSet<string>
@@ -2841,16 +2914,44 @@ namespace DshComputerUse.Worker
2841
2914
  "activate", "windowOp", "calibrate", "selectRange", "uiaAct"
2842
2915
  };
2843
2916
  static readonly object InputLock = new object();
2917
+ static readonly DesktopOwnershipLease DesktopLease = new DesktopOwnershipLease(DesktopOwnershipLease.DefaultPath());
2918
+ static bool _releaseDesktopPending;
2919
+ public static bool DesktopOwned { get { return DesktopLease.Owned; } }
2920
+ public static bool TryClaimDesktop(string owner)
2921
+ { string occupiedBy; return DesktopLease.TryAcquire(owner, out occupiedBy); }
2922
+ static void EnsureDesktopOwnership(string op)
2923
+ {
2924
+ if (op == "ping" || op == "echoargs" || op == "keyClass" || op == "stuckPlan" || op == "probe") return;
2925
+ string occupiedBy;
2926
+ if (!DesktopLease.TryAcquire("native cycle: " + op, out occupiedBy))
2927
+ {
2928
+ 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.");
2929
+ error.Data["outcome"] = "not-dispatched"; throw error;
2930
+ }
2931
+ }
2932
+ public static void ReleaseDesktopOwnership()
2933
+ {
2934
+ lock (InputLock)
2935
+ {
2936
+ _releaseDesktopPending = true;
2937
+ if (!_operationLive && OwnedUps.Count == 0) { DesktopLease.Dispose(); _releaseDesktopPending = false; }
2938
+ }
2939
+ }
2844
2940
  static readonly Dictionary<string, INPUT> OwnedUps = new Dictionary<string, INPUT>();
2845
2941
  static readonly ConcurrentDictionary<int, bool> CancelledRequests = new ConcurrentDictionary<int, bool>();
2846
2942
  static long _cancelGeneration;
2943
+ public static void InvalidateActiveInput() { Interlocked.Increment(ref _cancelGeneration); }
2847
2944
  static long _operationCancelGeneration, _operationHumanRevision;
2848
2945
  static volatile bool _operationLive, _operationDispatched;
2946
+ static bool _operationActing;
2947
+ static string _lastAnnouncedSeries;
2948
+ static long _announcedHumanRevision, _announcedInputEpoch, _preparedInputEpoch;
2849
2949
  static volatile int _activeRequestId, _lastCompletedRequestId;
2850
2950
  // The seam is internal to the compiled program; no tool can replace it.
2851
2951
  internal static Func<INPUT[], uint> InputSender = delegate(INPUT[] inputs) {
2852
2952
  return Native.SendInput((uint)inputs.Length, inputs, InputCbSize);
2853
2953
  };
2954
+ internal static Func<IntPtr> PreparationForeground = delegate { return Native.GetForegroundWindow(); };
2854
2955
 
2855
2956
  static Exception InputInterrupted(string code, string message)
2856
2957
  {
@@ -2866,11 +2967,74 @@ namespace DshComputerUse.Worker
2866
2967
  if (_operationLive && _operationCancelGeneration != Interlocked.Read(ref _cancelGeneration))
2867
2968
  throw InputInterrupted("INPUT_CANCELLED", "the owning tool was cancelled; no further input will be sent.");
2868
2969
  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.");
2970
+ 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
2971
  }
2871
2972
  static void MarkMutation()
2872
2973
  {
2873
- Panic.Check("actuation"); CheckHumanInput(); _operationDispatched = true;
2974
+ Panic.Check("actuation"); BeginActuation(); CheckHumanInput(); _operationDispatched = true;
2975
+ }
2976
+ static void BeginActuation()
2977
+ {
2978
+ if (_operationActing) return;
2979
+ if (_operationLive && _preparedInputEpoch != Panic.InputEpoch)
2980
+ 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.");
2981
+ _operationActing = true;
2982
+ Panic.BeginActuation();
2983
+ CheckHumanInput();
2984
+ if (_operationLive && _preparedInputEpoch != Panic.InputEpoch)
2985
+ throw InputInterrupted("HUMAN_REOBSERVE", "The desktop changed at the input boundary. No action was dispatched. Read the current target and continue the pending task.");
2986
+ }
2987
+ static void EndActuationForReadback()
2988
+ {
2989
+ lock (InputLock)
2990
+ {
2991
+ // Nested selection/chord helpers may still own a modifier or button.
2992
+ // Keep their sequence protected until every accepted down is released.
2993
+ if (OwnedUps.Count != 0) return;
2994
+ _operationActing = false;
2995
+ Panic.EndActuation();
2996
+ }
2997
+ }
2998
+ static void PrepareActuation(string op, Dictionary<string, object> request)
2999
+ {
3000
+ object raw;
3001
+ var args = request.TryGetValue("args", out raw) ? raw as Dictionary<string, object> : null;
3002
+ string series = args == null ? "" : GetS(args, "__inputSeries", "");
3003
+ if (!string.IsNullOrEmpty(series) && series == _lastAnnouncedSeries &&
3004
+ _announcedHumanRevision == Panic.HumanRevision)
3005
+ {
3006
+ if (_announcedInputEpoch != Panic.InputEpoch)
3007
+ 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.");
3008
+ _preparedInputEpoch = Panic.InputEpoch; return;
3009
+ }
3010
+ Panic.Check(op); CheckHumanInput();
3011
+ long inputEpoch = Panic.InputEpoch;
3012
+ IntPtr foreground = PreparationForeground();
3013
+ Glow.Prepare(true);
3014
+ try
3015
+ {
3016
+ var startup = Stopwatch.StartNew();
3017
+ while (!Glow.PreparationVisible)
3018
+ {
3019
+ Panic.Check(op); CheckHumanInput();
3020
+ if (startup.ElapsedMilliseconds >= 5000)
3021
+ throw InputInterrupted("PREPARATION_UNAVAILABLE", "The preparation notice could not be displayed; no input was dispatched.");
3022
+ Thread.Sleep(10);
3023
+ }
3024
+ var timer = Stopwatch.StartNew();
3025
+ while (timer.ElapsedMilliseconds < Glow.PreparationMs)
3026
+ {
3027
+ Panic.Check(op); CheckHumanInput();
3028
+ int remaining = Glow.PreparationMs - (int)timer.ElapsedMilliseconds;
3029
+ if (remaining > 0) Thread.Sleep(Math.Min(25, remaining));
3030
+ }
3031
+ Panic.Check(op); CheckHumanInput();
3032
+ if (inputEpoch != Panic.InputEpoch || foreground != PreparationForeground())
3033
+ 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.");
3034
+ _lastAnnouncedSeries = series; _announcedHumanRevision = Panic.HumanRevision;
3035
+ _announcedInputEpoch = _preparedInputEpoch = inputEpoch;
3036
+ }
3037
+ finally { Glow.Prepare(false); }
2874
3038
  }
2875
3039
  public static bool CancelInput(int requestId)
2876
3040
  {
@@ -2912,6 +3076,7 @@ namespace DshComputerUse.Worker
2912
3076
  // cross-process STOP may hold SyncLock while releasing owned input.
2913
3077
  if (Panic.Engaged || Panic.Stopped || Panic.Exited)
2914
3078
  throw InputInterrupted("ABORTED", "manual pause or exit owns the desktop; no further input will be sent.");
3079
+ BeginActuation();
2915
3080
  if (inputs.Length == 1)
2916
3081
  {
2917
3082
  bool isUp; INPUT release; int physical;
@@ -3218,7 +3383,11 @@ namespace DshComputerUse.Worker
3218
3383
  {
3219
3384
  var request = Json.Deserialize<Dictionary<string, object>>(line);
3220
3385
  string op = GetS(request, "op", "");
3221
- bool input = ActOps.Contains(op) || op == "shiftClick" || op == "selftest";
3386
+ object rawArgs;
3387
+ var args = request.TryGetValue("args", out rawArgs) ? rawArgs as Dictionary<string, object> : null;
3388
+ bool input = ActOps.Contains(op) || op == "shiftClick" || op == "selftest" ||
3389
+ (op == "focus" && args != null && GetB(args, "refocus", false));
3390
+ EnsureDesktopOwnership(op);
3222
3391
  int requestId = GetI(request, "id", 0);
3223
3392
  bool ignored;
3224
3393
  _activeRequestId = requestId;
@@ -3240,8 +3409,13 @@ namespace DshComputerUse.Worker
3240
3409
  }
3241
3410
  finally
3242
3411
  {
3243
- if (input) ReleaseOwnedInputs();
3412
+ if (input)
3413
+ {
3414
+ try { ReleaseOwnedInputs(); }
3415
+ finally { _operationActing = false; Panic.EndActuation(); Glow.Prepare(false); }
3416
+ }
3244
3417
  _operationLive = false; _activeRequestId = 0;
3418
+ if (_releaseDesktopPending) ReleaseDesktopOwnership();
3245
3419
  _lastCompletedRequestId = Math.Max(_lastCompletedRequestId, requestId);
3246
3420
  CancelledRequests.TryRemove(requestId, out ignored);
3247
3421
  }
@@ -3257,6 +3431,7 @@ namespace DshComputerUse.Worker
3257
3431
  Dictionary<string, object> a = (req.TryGetValue("args", out av) && av is Dictionary<string, object>) ? (Dictionary<string, object>)av : new Dictionary<string, object>();
3258
3432
  bool cycleOp = GetB(a, "__cycle", false) && op != null &&
3259
3433
  op != "abort" && op != "ping" && op != "echoargs" &&
3434
+ op != "probe" && op != "keyClass" && op != "stuckPlan" &&
3260
3435
  op != "indicator" && op != "calm" && op != "panic" && op != "resume" && op != "recover" &&
3261
3436
  // ... AND THE CYCLE HAS TO BE ALIVE TO BE LIT (2026-09-13). "Lit by any call" is how
3262
3437
  // a DEAD cycle came back as a cyan breathing border: after Ctrl+Alt+Q ended the
@@ -3281,6 +3456,10 @@ namespace DshComputerUse.Worker
3281
3456
  // and is therefore treated as the observation it really is.
3282
3457
  Panic.CheckOp((op == "click" || op == "move" || op == "type" || op == "key") && GetB(a, "dryRun", false) ? "probe" : op);
3283
3458
 
3459
+ if ((ActOps.Contains(op) || op == "shiftClick" || op == "selftest" ||
3460
+ (op == "focus" && GetB(a, "refocus", false))) && !GetB(a, "dryRun", false))
3461
+ PrepareActuation(op, req);
3462
+
3284
3463
  switch (op)
3285
3464
  {
3286
3465
  // THE QUESTION PATH IS TWO OPS NOW (merge-design.md §2.3): `beginAsk` engages the
@@ -3399,8 +3578,9 @@ namespace DshComputerUse.Worker
3399
3578
  pr["glow"] = Dict(
3400
3579
  "want", Glow.Want,
3401
3580
  // 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.
3581
+ // by the actual action scope, so both are reported as observable facts.
3403
3582
  "injecting", Glow.Injecting,
3583
+ "acting", Glow.Acting, "preparing", Glow.Preparing, "preparationMs", Glow.PreparationMs,
3404
3584
  "title", Glow.ShownTitle == null ? "" : Glow.ShownTitle);
3405
3585
  // EVENTS THE OS ACTUALLY INSERTED (not the plan): 0 after a plain pause with
3406
3586
  // nothing held is the observable form of "no ghost right-click / Alt menu".
@@ -3417,9 +3597,9 @@ namespace DshComputerUse.Worker
3417
3597
  // like the emergency brake. Called when the agent's turn ends, so a lingering blue
3418
3598
  // border can never claim the machine is still being driven after control stopped.
3419
3599
  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));
3600
+ if (!Panic.TryEndCycle())
3601
+ return Ok(Dict("released", false, "reason", "A manual pause or question still owns this cycle; its brake and ownership were preserved."));
3602
+ return Ok(Dict("released", true, "indicator", "off", "hard", true));
3423
3603
  case "focus": return Ok(FocusReport(a, GetB(a, "refocus", false)));
3424
3604
  case "frameSig": return Ok(FrameSigOp(a));
3425
3605
  // `source=agent` (not "unknown"): this op exists ONLY for the agent's own `stop`, and a
@@ -3706,6 +3886,7 @@ namespace DshComputerUse.Worker
3706
3886
  // ACTING light on it, and clear the mark in a finally so the blue can never outlive the
3707
3887
  // input it is describing.
3708
3888
  CheckHumanInput();
3889
+ BeginActuation();
3709
3890
  Glow.InputBegin();
3710
3891
  try
3711
3892
  {
@@ -3921,6 +4102,7 @@ namespace DshComputerUse.Worker
3921
4102
  SendMouse(down, 0, 0, 0); Thread.Sleep(30); SendMouse(up, 0, 0, 0);
3922
4103
  if (i < clicks - 1) Thread.Sleep(gap);
3923
4104
  }
4105
+ EndActuationForReadback();
3924
4106
  double changedPct = -1;
3925
4107
  if (verify && before != null)
3926
4108
  {
@@ -4047,16 +4229,8 @@ namespace DshComputerUse.Worker
4047
4229
  {
4048
4230
  _lastRefocus = EnsureTargetForeground();
4049
4231
  }
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);
4232
+ // Validation and focus readback do not own input. The first real mutation
4233
+ // or SendInput opens the actual scope and its deep-blue cue.
4060
4234
  }
4061
4235
 
4062
4236
  static Dictionary<string, object> Indicator(Dictionary<string, object> a)
@@ -4665,6 +4839,7 @@ namespace DshComputerUse.Worker
4665
4839
  KeyboardChord.Run(vks, SendKey, ReleaseKeyForCleanup,
4666
4840
  delegate(ushort vk) { return (Native.GetAsyncKeyState(vk) & 0x8000) != 0; },
4667
4841
  delegate { WaitForKeyHold(Math.Max(20, GetI(a, "holdMs", 60))); });
4842
+ EndActuationForReadback();
4668
4843
  Thread.Sleep(30);
4669
4844
  Dictionary<string, object> keyOut = Dict("combo", combo, "focus", focus);
4670
4845
  if (_lastRefocus != null) keyOut["refocused"] = _lastRefocus;
@@ -5550,8 +5725,8 @@ namespace DshComputerUse.Worker
5550
5725
  {
5551
5726
  CheckHumanInput();
5552
5727
  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);
5728
+ if (!Native.IsWindowVisible(h)) { MarkMutation(); Native.ShowWindow(h, Native.SW_SHOWNOACTIVATE); }
5729
+ if (Native.IsIconic(h)) { MarkMutation(); Native.ShowWindow(h, Native.SW_RESTORE); }
5555
5730
  Thread.Sleep(40);
5556
5731
  CheckHumanInput();
5557
5732
  if (IsForeground(h)) return "already-foreground";
@@ -6018,8 +6193,7 @@ namespace DshComputerUse.Worker
6018
6193
  ", automationId=" + (autoId ?? "-") + ", role=" + (role ?? "-") +
6019
6194
  ", index=" + index + ") among " + seen + " elements");
6020
6195
 
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.
6196
+ // A UIA mutation shares the same actual-action boundary and takeover checks.
6023
6197
  // A selector WITHOUT hwnd searches the whole desktop, so there the element that
6024
6198
  // actually matched decides the victim - and a host control is never driven through
6025
6199
  // its automation pattern either.
@@ -6028,11 +6202,9 @@ namespace DshComputerUse.Worker
6028
6202
  HostGuardPid(foundPid, "uiaAct");
6029
6203
  Panic.Check("uiaAct");
6030
6204
  CheckHumanInput();
6031
- Glow.Touch(GetI(a, "indicatorMs", 1200)); // same reason: see AgentActing
6032
6205
 
6033
6206
  object pat = null;
6034
6207
  string via = null;
6035
- MarkMutation();
6036
6208
  switch (action)
6037
6209
  {
6038
6210
  case "invoke":
@@ -6064,6 +6236,7 @@ namespace DshComputerUse.Worker
6064
6236
  default:
6065
6237
  throw new Exception("action must be invoke|select|expand|collapse|toggle|setValue|focus|scrollIntoView");
6066
6238
  }
6239
+ EndActuationForReadback();
6067
6240
  if (via == null)
6068
6241
  throw new Exception("element does not support '" + action + "': " + ElemDict(found)["name"]);
6069
6242
 
@@ -6373,3 +6546,79 @@ namespace DshComputerUse.Worker
6373
6546
  static void WriteLine(string s) { lock (OutLock) { Console.Out.WriteLine(s); Console.Out.Flush(); } }
6374
6547
  }
6375
6548
  }
6549
+
6550
+ // Integrated verbatim into worker.cs by the native worker build. Tests compile this
6551
+ // production class without starting hooks, overlays or desktop input.
6552
+ namespace DshComputerUse.Worker
6553
+ {
6554
+ public sealed class DesktopOwnershipLease : System.IDisposable
6555
+ {
6556
+ readonly object sync = new object();
6557
+ readonly string path;
6558
+ System.IO.FileStream stream;
6559
+ public DesktopOwnershipLease(string file) { path = file; }
6560
+ public bool Owned { get { lock (sync) { return stream != null; } } }
6561
+ public static string DefaultPath()
6562
+ {
6563
+ string isolated = System.Environment.GetEnvironmentVariable("DSH_COMPUTER_USE_OWNER_FILE");
6564
+ if (!string.IsNullOrEmpty(isolated)) return isolated;
6565
+ return System.IO.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData),
6566
+ "dsh-computer-use", "desktop-" + System.Diagnostics.Process.GetCurrentProcess().SessionId + ".owner");
6567
+ }
6568
+ public bool TryAcquire(string owner, out string occupiedBy)
6569
+ {
6570
+ lock (sync)
6571
+ {
6572
+ occupiedBy = "";
6573
+ if (stream != null) return true;
6574
+ System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path)));
6575
+ System.IO.FileStream opened;
6576
+ try
6577
+ {
6578
+ // A persistent handle is the lock. Never delete the pathname, use a
6579
+ // timer, or steal based on PID: Windows releases it when the process dies.
6580
+ opened = new System.IO.FileStream(path, System.IO.FileMode.OpenOrCreate,
6581
+ System.IO.FileAccess.ReadWrite, System.IO.FileShare.Read);
6582
+ }
6583
+ catch (System.IO.IOException error)
6584
+ {
6585
+ int code = System.Runtime.InteropServices.Marshal.GetHRForException(error) & 0xFFFF;
6586
+ if (code != 32 && code != 33) throw;
6587
+ occupiedBy = ReadOwner();
6588
+ return false;
6589
+ }
6590
+ try
6591
+ {
6592
+ byte[] record = System.Text.Encoding.UTF8.GetBytes("pid=" + System.Diagnostics.Process.GetCurrentProcess().Id +
6593
+ "\nowner=" + (owner ?? "worker").Replace("\r", " ").Replace("\n", " ") + "\n");
6594
+ opened.SetLength(0); opened.Write(record, 0, record.Length); opened.Flush();
6595
+ stream = opened;
6596
+ return true;
6597
+ }
6598
+ catch { opened.Dispose(); throw; }
6599
+ }
6600
+ }
6601
+ string ReadOwner()
6602
+ {
6603
+ try
6604
+ {
6605
+ using (var input = new System.IO.FileStream(path, System.IO.FileMode.Open,
6606
+ System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite))
6607
+ using (var reader = new System.IO.StreamReader(input))
6608
+ {
6609
+ char[] buffer = new char[512];
6610
+ return new string(buffer, 0, reader.Read(buffer, 0, buffer.Length)).Trim();
6611
+ }
6612
+ }
6613
+ catch { return "another live computer-use worker"; }
6614
+ }
6615
+ public void Dispose()
6616
+ {
6617
+ lock (sync)
6618
+ {
6619
+ if (stream == null) return;
6620
+ stream.Dispose(); stream = null;
6621
+ }
6622
+ }
6623
+ }
6624
+ }