newmark-agent 0.6.4 → 0.6.8

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.
Files changed (76) hide show
  1. package/config.example.json +13 -3
  2. package/dist/cli-commands.d.ts +1 -1
  3. package/dist/cli-commands.js +54 -3
  4. package/dist/cli-discovery.js +3 -0
  5. package/dist/conversation-utility-host.bundle.cjs +4472 -1767
  6. package/dist/conversation-utility-host.js +94 -7
  7. package/dist/core/agent.d.ts +126 -12
  8. package/dist/core/agent.js +785 -172
  9. package/dist/core/agentKernelRunner.d.ts +7 -1
  10. package/dist/core/agentKernelRunner.js +160 -20
  11. package/dist/core/autoRouter.d.ts +49 -44
  12. package/dist/core/autoRouter.js +117 -302
  13. package/dist/core/computerUseSession.js +1 -1
  14. package/dist/core/config.js +27 -11
  15. package/dist/core/continuation/contracts.d.ts +1 -1
  16. package/dist/core/continuation/store.d.ts +91 -1
  17. package/dist/core/continuation/store.js +291 -61
  18. package/dist/core/conversationKernel.d.ts +49 -7
  19. package/dist/core/conversationKernel.js +303 -51
  20. package/dist/core/conversationStateDocument.d.ts +18 -0
  21. package/dist/core/conversationStateDocument.js +88 -0
  22. package/dist/core/conversationVisualBudget.d.ts +74 -0
  23. package/dist/core/conversationVisualBudget.js +153 -0
  24. package/dist/core/electronUtilityAgentClient.d.ts +3 -2
  25. package/dist/core/electronUtilityAgentClient.js +32 -8
  26. package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
  27. package/dist/core/electronUtilityRuntimePool.js +93 -24
  28. package/dist/core/fileWriteObservation.d.ts +41 -0
  29. package/dist/core/fileWriteObservation.js +454 -0
  30. package/dist/core/hostRuntimeHooks.d.ts +23 -0
  31. package/dist/core/hostRuntimeHooks.js +17 -0
  32. package/dist/core/jevDecision.d.ts +144 -0
  33. package/dist/core/jevDecision.js +226 -0
  34. package/dist/core/memoryProbe.d.ts +24 -0
  35. package/dist/core/memoryProbe.js +104 -0
  36. package/dist/core/mobilePairing.d.ts +7 -1
  37. package/dist/core/mobilePairing.js +9 -1
  38. package/dist/core/performanceDiagnostics.d.ts +1 -1
  39. package/dist/core/routeDecisionValidator.d.ts +86 -0
  40. package/dist/core/routeDecisionValidator.js +249 -0
  41. package/dist/core/routeEligibility.d.ts +52 -0
  42. package/dist/core/routeEligibility.js +108 -0
  43. package/dist/core/runtimeMemoryBudget.d.ts +6 -0
  44. package/dist/core/runtimeMemoryBudget.js +12 -0
  45. package/dist/core/toolPolicy.d.ts +1 -1
  46. package/dist/core/toolPolicy.js +1 -1
  47. package/dist/core/utilityAgentProtocol.d.ts +8 -10
  48. package/dist/core/utilityHostToolRouter.js +2 -0
  49. package/dist/core/visualDownscale.d.ts +6 -0
  50. package/dist/core/visualDownscale.js +137 -0
  51. package/dist/core/wslAgentClient.d.ts +1 -2
  52. package/dist/core/wslAgentClient.js +0 -8
  53. package/dist/core/wslAgentProtocol.d.ts +1 -10
  54. package/dist/core/wslAgentRuntimePool.d.ts +1 -3
  55. package/dist/core/wslAgentRuntimePool.js +17 -22
  56. package/dist/llm/provider.d.ts +3 -1
  57. package/dist/llm/provider.js +53 -9
  58. package/dist/main.js +107 -30
  59. package/dist/preload.js +3 -1
  60. package/dist/server.js +30 -18
  61. package/dist/tools/computerUse.d.ts +6 -0
  62. package/dist/tools/computerUse.js +406 -21
  63. package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
  64. package/dist/tools/computerUsePowerShellHost.js +105 -31
  65. package/dist/tools/index.d.ts +3 -4
  66. package/dist/tools/index.js +146 -66
  67. package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
  68. package/dist/tui/src/i18n.js +12 -1
  69. package/dist/tui/src/render.js +9 -2
  70. package/dist/tui/src/settings-schema.js +19 -0
  71. package/dist/tui/src/state.js +69 -1
  72. package/dist/ui/index.html +651 -233
  73. package/dist/ui/lucide-sprite.svg +0 -8
  74. package/dist/wsl-agent-host.bundle.cjs +4302 -1767
  75. package/dist/wsl-agent-host.js +0 -3
  76. package/package.json +39 -10
@@ -68,6 +68,7 @@ const DEFAULT_CAPTURE_MAX_HEIGHT = 960;
68
68
  const MIN_CAPTURE_MAX_WIDTH = 320;
69
69
  const MIN_CAPTURE_MAX_HEIGHT = 240;
70
70
  const MAX_CAPTURE_DIMENSION = 2048;
71
+ const MAX_SPARSE_WAIT_MS = 5000;
71
72
  let lastScreenshotCleanupAt = 0;
72
73
  function ephemeralScreenshotPath(kind, directory = tempScreenshotDir(), createdAt = Date.now(), ownerPid = process.pid, suffix = crypto.randomBytes(4).toString('hex')) {
73
74
  const pid = Math.max(1, Math.floor(Number(ownerPid) || process.pid));
@@ -210,6 +211,172 @@ function jpegCaptureScript(outPath, boundsScript, requestedMaxWidth, requestedMa
210
211
  'Write-Output (@{ ok=$true; left=$x; top=$y; width=$w; height=$h; capture_max_width=$maxWidth; capture_max_height=$maxHeight; image_width=$imageWidth; image_height=$imageHeight; image_bytes=$fileSize; image_quality=$qualityUsed; image_available=$imageAvailable; image_mime="image/jpeg" } | ConvertTo-Json -Compress)',
211
212
  ].join('\r\n');
212
213
  }
214
+ function boundedSparseWait(value) {
215
+ const requested = Number(value);
216
+ if (!Number.isFinite(requested) || requested <= 0)
217
+ return 0;
218
+ return Math.min(MAX_SPARSE_WAIT_MS, Math.floor(requested));
219
+ }
220
+ function sparseMeasurementSampleBudget(waitMs) {
221
+ const boundedWait = Math.max(1, Math.floor(Number(waitMs) || 1));
222
+ // A live PowerShell process handshake does not mean its first GDI capture,
223
+ // script execution and IPC response are warm. Reserve completion time on
224
+ // every call so the lane's own wait-elapsed result beats the outer fallback.
225
+ const completionMarginMs = Math.min(Math.max(0, boundedWait - 1), Math.min(1500, Math.max(500, Math.floor(boundedWait * 0.3))));
226
+ return Math.max(1, boundedWait - completionMarginMs);
227
+ }
228
+ function sparseMeasurementScript(waitMs) {
229
+ const boundedWait = Math.min(MAX_SPARSE_WAIT_MS, Math.max(1, Math.floor(waitMs)));
230
+ return [
231
+ '$ErrorActionPreference = "Stop"',
232
+ '$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen',
233
+ '$x=[int]$bounds.Left; $y=[int]$bounds.Top; $w=[int]$bounds.Width; $h=[int]$bounds.Height',
234
+ 'if ($w -le 0 -or $h -le 0) { throw "Virtual screen geometry is unavailable." }',
235
+ '$gridWidth=32; $gridHeight=18; $pollMs=200; $cellThreshold=24; $strongThreshold=64',
236
+ '$watch = [System.Diagnostics.Stopwatch]::StartNew(); $sampleCount=0; $changedCells=0; $triggered=$false; $geometryChanged=$false',
237
+ '$thumbnail = [NewmarkSparseCapture]::CaptureScaled($x,$y,$w,$h,$gridWidth,$gridHeight)',
238
+ '$baseline = [byte[]]::new($gridWidth * $gridHeight)',
239
+ 'try {',
240
+ ' for ($row=0; $row -lt $gridHeight; $row++) { for ($column=0; $column -lt $gridWidth; $column++) { $index=$row*$gridWidth+$column; $baseline[$index]=$thumbnail.GetLuma($column,$row) } }',
241
+ ' $sampleCount=1',
242
+ ` while ($watch.ElapsedMilliseconds -lt ${boundedWait}) {`,
243
+ ` $remaining=[Math]::Max(1, ${boundedWait} - [int]$watch.ElapsedMilliseconds); Start-Sleep -Milliseconds ([Math]::Min($pollMs,$remaining))`,
244
+ ' $nextBounds=[System.Windows.Forms.SystemInformation]::VirtualScreen',
245
+ ' if ($nextBounds.Left -ne $x -or $nextBounds.Top -ne $y -or $nextBounds.Width -ne $w -or $nextBounds.Height -ne $h) { $geometryChanged=$true; break }',
246
+ ' $nextThumbnail=[NewmarkSparseCapture]::CaptureScaled($x,$y,$w,$h,$gridWidth,$gridHeight); $thumbnail.Dispose(); $thumbnail=$nextThumbnail',
247
+ ' $changedCells=0; $strongCells=0',
248
+ ' for ($row=0; $row -lt $gridHeight; $row++) { for ($column=0; $column -lt $gridWidth; $column++) { $index=$row*$gridWidth+$column; $luma=[int]$thumbnail.GetLuma($column,$row); $delta=[Math]::Abs($luma-$baseline[$index]); if ($delta -ge $cellThreshold) { $changedCells++ }; if ($delta -ge $strongThreshold) { $strongCells++ } } }',
249
+ ' $sampleCount++',
250
+ ' if ($changedCells -ge 4 -or $strongCells -ge 2) { $triggered=$true; break }',
251
+ ' }',
252
+ ].join('\r\n') + [
253
+ '} finally { $watch.Stop(); $thumbnail.Dispose() }',
254
+ '$reason = if ($geometryChanged) { "screen_geometry_changed" } elseif ($triggered) { "coarse_change" } else { "wait_elapsed" }',
255
+ 'Write-Output (@{ ok=$true; triggered=$triggered; geometry_changed=$geometryChanged; sample_count=$sampleCount; changed_cells=$changedCells; elapsed_ms=[int]$watch.ElapsedMilliseconds; reason=$reason } | ConvertTo-Json -Compress)',
256
+ ].join('\r\n');
257
+ }
258
+ function sparseAbortError(signal) {
259
+ if (signal.reason instanceof Error)
260
+ return signal.reason;
261
+ return new Error(signal.reason ? String(signal.reason) : 'Computer Use sparse observation was cancelled.');
262
+ }
263
+ async function waitForSparseChange(waitMs, signal) {
264
+ if (signal?.aborted)
265
+ throw sparseAbortError(signal);
266
+ return await new Promise((resolve, reject) => {
267
+ let settled = false;
268
+ let phase = 'preparing';
269
+ const deadlineAt = Date.now() + waitMs;
270
+ let deadlineTimer;
271
+ const cleanup = () => {
272
+ if (deadlineTimer)
273
+ clearTimeout(deadlineTimer);
274
+ signal?.removeEventListener('abort', onAbort);
275
+ };
276
+ const onAbort = () => {
277
+ if (settled)
278
+ return;
279
+ settled = true;
280
+ cleanup();
281
+ (0, computerUsePowerShellHost_1.stopComputerUsePowerShellLane)('sparse');
282
+ reject(sparseAbortError(signal));
283
+ };
284
+ deadlineTimer = setTimeout(() => {
285
+ if (settled)
286
+ return;
287
+ settled = true;
288
+ cleanup();
289
+ // A normal wait deadline during helper startup must not kill the shared
290
+ // lane. Its initialization can finish in the background and serve the
291
+ // next observation. Once a sample has actually been submitted, however,
292
+ // stop a worker that failed to return before its bounded sampling window.
293
+ if (phase === 'sampling')
294
+ (0, computerUsePowerShellHost_1.stopComputerUsePowerShellLane)('sparse');
295
+ const error = phase === 'preparing'
296
+ ? 'Sparse helper did not become ready before the observation deadline; its startup continues for the next observation.'
297
+ : 'Sparse sample did not return before the observation deadline; continuing with a fresh full observation.';
298
+ resolve({ ok: false, triggered: false, geometryChanged: false, sampleCount: null, changedCells: null, elapsedMs: waitMs, reason: 'wait_elapsed', error });
299
+ }, waitMs);
300
+ signal?.addEventListener('abort', onAbort, { once: true });
301
+ if (signal?.aborted) {
302
+ onAbort();
303
+ return;
304
+ }
305
+ const parseResult = (result) => {
306
+ if (settled)
307
+ return;
308
+ settled = true;
309
+ cleanup();
310
+ if (!result.ok) {
311
+ resolve({ ok: false, triggered: false, geometryChanged: false, sampleCount: null, changedCells: null, elapsedMs: result.elapsedMs, reason: 'sampling_failed', error: result.output.slice(0, 500) });
312
+ return;
313
+ }
314
+ try {
315
+ const parsed = JSON.parse(result.output);
316
+ resolve({
317
+ ok: parsed.ok === true,
318
+ triggered: parsed.triggered === true,
319
+ geometryChanged: parsed.geometry_changed === true,
320
+ sampleCount: Math.max(0, Number(parsed.sample_count) || 0),
321
+ changedCells: Math.max(0, Number(parsed.changed_cells) || 0),
322
+ elapsedMs: Math.max(0, Number(parsed.elapsed_ms) || result.elapsedMs),
323
+ reason: String(parsed.reason || 'sampling_failed'),
324
+ ...(parsed.ok === true ? {} : { error: 'Sparse measurement returned an invalid result.' }),
325
+ });
326
+ }
327
+ catch {
328
+ resolve({ ok: false, triggered: false, geometryChanged: false, sampleCount: null, changedCells: null, elapsedMs: result.elapsedMs, reason: 'sampling_failed', error: 'Sparse measurement result was not valid JSON.' });
329
+ }
330
+ };
331
+ const prepareAndSample = async () => {
332
+ const preparation = await (0, computerUsePowerShellHost_1.prepareComputerUsePowerShellLane)('sparse', waitMs);
333
+ if (settled)
334
+ return;
335
+ if (!preparation.ready) {
336
+ settled = true;
337
+ cleanup();
338
+ resolve({
339
+ ok: false,
340
+ triggered: false,
341
+ geometryChanged: false,
342
+ sampleCount: null,
343
+ changedCells: null,
344
+ elapsedMs: Math.max(preparation.elapsedMs, Date.now() - (deadlineAt - waitMs)),
345
+ reason: 'wait_elapsed',
346
+ error: 'Sparse helper initialization exceeded the observation deadline; the lane remains available to finish startup.',
347
+ });
348
+ return;
349
+ }
350
+ const remainingMs = Math.max(0, deadlineAt - Date.now());
351
+ if (remainingMs < 100) {
352
+ settled = true;
353
+ cleanup();
354
+ resolve({ ok: false, triggered: false, geometryChanged: false, sampleCount: null, changedCells: null, elapsedMs: waitMs, reason: 'wait_elapsed', error: 'Sparse helper became ready without enough time left to capture a bounded sample.' });
355
+ return;
356
+ }
357
+ const sampleBudgetMs = sparseMeasurementSampleBudget(remainingMs);
358
+ phase = 'sampling';
359
+ try {
360
+ const result = await (0, computerUsePowerShellHost_1.runPersistentPowerShell)(sparseMeasurementScript(sampleBudgetMs), remainingMs, 'sparse');
361
+ parseResult(result);
362
+ }
363
+ catch (error) {
364
+ if (settled)
365
+ return;
366
+ settled = true;
367
+ cleanup();
368
+ resolve({ ok: false, triggered: false, geometryChanged: false, sampleCount: null, changedCells: null, elapsedMs: Date.now() - (deadlineAt - waitMs), reason: 'sampling_failed', error: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500) });
369
+ }
370
+ };
371
+ void prepareAndSample().catch(error => {
372
+ if (settled)
373
+ return;
374
+ settled = true;
375
+ cleanup();
376
+ resolve({ ok: false, triggered: false, geometryChanged: false, sampleCount: null, changedCells: null, elapsedMs: Date.now() - (deadlineAt - waitMs), reason: 'sampling_failed', error: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500) });
377
+ });
378
+ });
379
+ }
213
380
  function gradientPalette(_input) {
214
381
  return ['#000000', '#ffffff', '#000000', '#ffffff'];
215
382
  }
@@ -438,11 +605,15 @@ function sceneGeneration(apps, elements) {
438
605
  async function observeUiAutomation(maxChars, rootHandle, timeoutMs = 30000, lane = 'uia') {
439
606
  const limit = Math.min(Math.max(Math.floor(maxChars || 30000), 1000), 200000);
440
607
  const handleHex = String(rootHandle || '').trim().replace(/^0x/i, '').replace(/[^0-9a-f]/gi, '');
608
+ if (!/[1-9a-f]/i.test(handleHex)) {
609
+ return { elements: [], visited: 0, error: 'Window-scoped UI Automation requires a non-zero native window handle.', elapsedMs: 0 };
610
+ }
611
+ const hostProcessId = Math.max(1, Math.floor(process.pid));
441
612
  const script = [
442
- handleHex
443
- ? `$nativeHandle = [System.IntPtr]::new([Convert]::ToInt64(${psQuote(handleHex)}, 16)); $root = [System.Windows.Automation.AutomationElement]::FromHandle($nativeHandle)`
444
- : '$root = [System.Windows.Automation.AutomationElement]::RootElement',
613
+ `$nativeHandle = [System.IntPtr]::new([Convert]::ToInt64(${psQuote(handleHex)}, 16)); $root = [System.Windows.Automation.AutomationElement]::FromHandle($nativeHandle)`,
445
614
  'if ($null -eq $root) { throw "UI Automation root is unavailable." }',
615
+ '$rootProcessId = 0; try { $rootProcessId = [int]$root.Current.ProcessId } catch {}',
616
+ `if ($rootProcessId -eq ${hostProcessId}) { throw "UI Automation of the Newmark host window is skipped to avoid Electron self-accessibility hangs." }`,
446
617
  '$cache = New-Object System.Windows.Automation.CacheRequest',
447
618
  '$cache.TreeScope = [System.Windows.Automation.TreeScope]::Element',
448
619
  '$cache.TreeFilter = [System.Windows.Automation.Automation]::ControlViewCondition',
@@ -496,6 +667,9 @@ async function observeUiAutomation(maxChars, rootHandle, timeoutMs = 30000, lane
496
667
  return { elements: parseJsonArray(result.output), visited: 0, error: 'UI Automation returned invalid JSON.', elapsedMs: result.elapsedMs };
497
668
  }
498
669
  }
670
+ function skippedUiAutomation(error) {
671
+ return { elements: [], visited: 0, ...(error ? { error } : {}), elapsedMs: 0 };
672
+ }
499
673
  async function observeAppWindows(maxChars, timeoutMs = 30000, lane = 'windows') {
500
674
  const limit = Math.min(Math.max(Math.floor(maxChars || 30000), 1000), 200000);
501
675
  const script = [
@@ -597,7 +771,162 @@ async function selectAppWindow(target, handle) {
597
771
  return { apps, error: `No visible application window matched: ${target || handle}.`, elapsedMs: observed.elapsedMs };
598
772
  return { app, apps, elapsedMs: observed.elapsedMs };
599
773
  }
600
- async function cropScreenshot(workspacePath, ownerId, allowEphemeralVisionImage, app, includeRawUi = false, captureMaxWidth, captureMaxHeight) {
774
+ const MAX_WAIT_FOR_TIMEOUT_MS = 30_000;
775
+ const DEFAULT_WAIT_FOR_TIMEOUT_MS = 5_000;
776
+ const WAIT_FOR_POLL_INTERVAL_MS = 250;
777
+ function boundedWaitForTimeout(value) {
778
+ const requested = Number(value);
779
+ if (!Number.isFinite(requested) || requested <= 0)
780
+ return DEFAULT_WAIT_FOR_TIMEOUT_MS;
781
+ return Math.min(MAX_WAIT_FOR_TIMEOUT_MS, Math.max(WAIT_FOR_POLL_INTERVAL_MS, Math.floor(requested)));
782
+ }
783
+ async function waitForUi(options, ownerId) {
784
+ const startedAt = Date.now();
785
+ const timeoutMs = boundedWaitForTimeout(options.timeoutMs);
786
+ const deadline = startedAt + timeoutMs;
787
+ const workspacePath = options.workspacePath;
788
+ const cache = observations.get(observationKey(workspacePath, ownerId));
789
+ const requestedText = String(options.text || '').trim();
790
+ const normalizedQuery = normalizeText(requestedText);
791
+ const targetId = String(options.targetId || '').trim();
792
+ const appTarget = String(options.appTarget || '').trim();
793
+ const requestedHandle = String(options.windowHandle || '').trim();
794
+ const cachedTarget = targetId ? cache?.objects.find(object => object.target_id === targetId) : undefined;
795
+ let app;
796
+ let lastWindowError;
797
+ let attempts = 0;
798
+ let nextWindowPollAt = 0;
799
+ if (!normalizedQuery && !targetId && !appTarget && !requestedHandle) {
800
+ return { ok: false, action: 'wait_for', error: 'Provide text, target_id, app_target, or window_handle to wait for.' };
801
+ }
802
+ if (targetId && !cachedTarget) {
803
+ return { ok: false, action: 'wait_for', error: `target_id not found in latest observation: ${targetId}. Call computer_use observe first.` };
804
+ }
805
+ const directHandle = requestedHandle || (!appTarget ? String(cache?.windowHandle || '') : '');
806
+ if (directHandle) {
807
+ const direct = await observeAppWindowByHandle(directHandle);
808
+ attempts += 1;
809
+ if (!direct.app) {
810
+ return { ok: false, action: 'wait_for', status: 'window_unavailable', error: direct.error || 'The selected window is no longer available.', elapsed_ms: Date.now() - startedAt, attempts };
811
+ }
812
+ app = direct.app;
813
+ }
814
+ if (!app && !appTarget) {
815
+ return { ok: false, action: 'wait_for', error: 'Call computer_use observe/app_observe first, or provide app_target/window_handle.' };
816
+ }
817
+ if (targetId && cache?.windowHandle && app && cache.windowHandle.toLowerCase() !== app.handle.toLowerCase()) {
818
+ return { ok: false, action: 'wait_for', error: `target_id=${targetId} belongs to a different window than ${app.handle}. Observe that window before waiting on the target.` };
819
+ }
820
+ while (Date.now() <= deadline) {
821
+ if (options.signal?.aborted) {
822
+ return { ok: false, action: 'wait_for', status: 'cancelled', error: 'wait_for was cancelled.', elapsed_ms: Date.now() - startedAt, attempts };
823
+ }
824
+ if (!app && appTarget) {
825
+ if (Date.now() >= nextWindowPollAt) {
826
+ const observed = await observeAppWindows(30000, 750, 'windows_advisory');
827
+ attempts += 1;
828
+ lastWindowError = observed.error;
829
+ app = observed.apps.find(candidate => appMatches(candidate, appTarget));
830
+ nextWindowPollAt = Date.now() + 1000;
831
+ }
832
+ if (!app) {
833
+ const remaining = deadline - Date.now();
834
+ if (remaining <= 0)
835
+ break;
836
+ await new Promise(resolve => setTimeout(resolve, Math.min(Math.max(1, nextWindowPollAt - Date.now()), remaining)));
837
+ continue;
838
+ }
839
+ }
840
+ if (targetId && cache?.windowHandle && app && cache.windowHandle.toLowerCase() !== app.handle.toLowerCase()) {
841
+ return { ok: false, action: 'wait_for', error: `target_id=${targetId} belongs to a different window than ${app.handle}. Observe that window before waiting on the target.`, elapsed_ms: Date.now() - startedAt, attempts };
842
+ }
843
+ if (app && !normalizedQuery && !targetId) {
844
+ return {
845
+ ok: true,
846
+ action: 'wait_for',
847
+ status: 'matched',
848
+ found: true,
849
+ condition: { app_target: appTarget || undefined, window_handle: app.handle },
850
+ app,
851
+ elapsed_ms: Date.now() - startedAt,
852
+ attempts,
853
+ };
854
+ }
855
+ if (app) {
856
+ const ui = await observeUiAutomation(Number(options.maxChars || 30000), app.handle, 750, 'uia_advisory');
857
+ attempts += 1;
858
+ if (ui.error) {
859
+ return { ok: false, action: 'wait_for', status: 'uia_unavailable', error: ui.error, app, elapsed_ms: Date.now() - startedAt, attempts };
860
+ }
861
+ const objects = semanticObjects(ui.elements, app.bbox.width, app.bbox.height, workspacePath, ownerId);
862
+ const match = objects.find(object => {
863
+ if (targetId && object.target_id !== targetId)
864
+ return false;
865
+ if (!normalizedQuery)
866
+ return true;
867
+ return normalizeText(object.name).includes(normalizedQuery) || normalizeText(object.automation_id).includes(normalizedQuery);
868
+ });
869
+ if (match) {
870
+ const generation = sceneGeneration([app], ui.elements);
871
+ observations.set(observationKey(workspacePath, ownerId), {
872
+ workspacePath,
873
+ objects,
874
+ windowHandle: app.handle,
875
+ width: Math.max(1, app.bbox.width),
876
+ height: Math.max(1, app.bbox.height),
877
+ at: new Date().toISOString(),
878
+ sceneGeneration: generation,
879
+ });
880
+ return {
881
+ ok: true,
882
+ action: 'wait_for',
883
+ status: 'matched',
884
+ found: true,
885
+ condition: {
886
+ ...(requestedText ? { text: requestedText } : {}),
887
+ ...(targetId ? { target_id: targetId } : {}),
888
+ ...(appTarget ? { app_target: appTarget } : {}),
889
+ },
890
+ app,
891
+ scene_generation: generation,
892
+ element: {
893
+ target_id: match.target_id,
894
+ label: match.label,
895
+ role: match.role,
896
+ control_type: match.control_type,
897
+ automation_id: match.automation_id,
898
+ bbox: match.bbox,
899
+ center: match.center,
900
+ },
901
+ elapsed_ms: Date.now() - startedAt,
902
+ attempts,
903
+ };
904
+ }
905
+ }
906
+ const remaining = deadline - Date.now();
907
+ if (remaining <= 0)
908
+ break;
909
+ await new Promise(resolve => setTimeout(resolve, Math.min(WAIT_FOR_POLL_INTERVAL_MS, remaining)));
910
+ }
911
+ return {
912
+ ok: true,
913
+ action: 'wait_for',
914
+ status: 'timed_out',
915
+ found: false,
916
+ condition: {
917
+ ...(requestedText ? { text: requestedText } : {}),
918
+ ...(targetId ? { target_id: targetId } : {}),
919
+ ...(appTarget ? { app_target: appTarget } : {}),
920
+ ...(requestedHandle ? { window_handle: requestedHandle } : {}),
921
+ },
922
+ ...(app ? { app } : {}),
923
+ ...(lastWindowError ? { warning: lastWindowError } : {}),
924
+ timeout_ms: timeoutMs,
925
+ elapsed_ms: Date.now() - startedAt,
926
+ attempts,
927
+ };
928
+ }
929
+ async function cropScreenshot(workspacePath, ownerId, allowEphemeralVisionImage, app, includeRawUi = false, captureMaxWidth, captureMaxHeight, includeUiTree = true) {
601
930
  const totalStarted = Date.now();
602
931
  const outPath = ephemeralScreenshotPath('app');
603
932
  let retainedForVision = false;
@@ -608,7 +937,9 @@ async function cropScreenshot(workspacePath, ownerId, allowEphemeralVisionImage,
608
937
  ], captureMaxWidth, captureMaxHeight);
609
938
  const [result, ui] = await Promise.all([
610
939
  runPowerShell(script, 30000),
611
- observeUiAutomation(30000, app.handle, 750, 'uia_advisory'),
940
+ includeUiTree
941
+ ? observeUiAutomation(30000, app.handle, 750, 'uia_advisory')
942
+ : Promise.resolve(skippedUiAutomation()),
612
943
  ]);
613
944
  if (!result.ok)
614
945
  return captureFailure('app_observe', app);
@@ -631,7 +962,7 @@ async function cropScreenshot(workspacePath, ownerId, allowEphemeralVisionImage,
631
962
  }
632
963
  const cacheKey = observationKey(workspacePath, ownerId);
633
964
  const generation = sceneGeneration([app], elements);
634
- observations.set(cacheKey, { workspacePath, objects, width: Number(parsed.width || b.width || 1), height: Number(parsed.height || b.height || 1), at: new Date().toISOString(), sceneGeneration: generation });
965
+ observations.set(cacheKey, { workspacePath, objects, windowHandle: app.handle, width: Number(parsed.width || b.width || 1), height: Number(parsed.height || b.height || 1), at: new Date().toISOString(), sceneGeneration: generation });
635
966
  const payload = {
636
967
  ok: true,
637
968
  action: 'app_observe',
@@ -640,8 +971,10 @@ async function cropScreenshot(workspacePath, ownerId, allowEphemeralVisionImage,
640
971
  screenshot_retention: allowEphemeralVisionImage ? 'ephemeral-delete-after-vision-input' : 'ephemeral-deleted-before-tool-return',
641
972
  coordinate_system: 'virtual-screen-pixels',
642
973
  perception: {
643
- mode: 'native-application-screenshot-plus-windows-ui-automation',
974
+ mode: includeUiTree ? 'native-application-screenshot-plus-windows-ui-automation' : 'native-application-screenshot-only',
644
975
  application_scope: 'single-visible-window',
976
+ ui_tree_status: includeUiTree ? (ui.error ? 'unavailable' : 'captured') : 'skipped',
977
+ ui_tree_skipped: !includeUiTree,
645
978
  element_count: elements.length,
646
979
  visited_count: ui.visited,
647
980
  scene_generation: generation,
@@ -652,6 +985,7 @@ async function cropScreenshot(workspacePath, ownerId, allowEphemeralVisionImage,
652
985
  telemetry: {
653
986
  capture_ms: result.elapsedMs,
654
987
  uia_ms: ui.elapsedMs,
988
+ ui_tree_skipped: !includeUiTree,
655
989
  total_ms: Date.now() - totalStarted,
656
990
  element_count: elements.length,
657
991
  visited_count: ui.visited,
@@ -901,8 +1235,28 @@ function resolveTarget(targetId, workspacePath, ownerId) {
901
1235
  return { x: 0, y: 0, error: `target_id not found in latest observation: ${id}.` };
902
1236
  return { x: target.center.x, y: target.center.y, target };
903
1237
  }
904
- async function screenshot(workspacePath, ownerId, allowEphemeralVisionImage, includeRawUi = false, captureMaxWidth, captureMaxHeight) {
1238
+ async function screenshot(workspacePath, ownerId, allowEphemeralVisionImage, includeRawUi = false, captureMaxWidth, captureMaxHeight, sparseWaitMs, signal, includeUiTree = true) {
905
1239
  const totalStarted = Date.now();
1240
+ const sparseDurationMs = boundedSparseWait(sparseWaitMs);
1241
+ const sparseStarted = Date.now();
1242
+ const sparse = sparseDurationMs > 0 ? await waitForSparseChange(sparseDurationMs, signal) : undefined;
1243
+ const sparseWallMs = sparse ? Date.now() - sparseStarted : 0;
1244
+ if (signal?.aborted)
1245
+ throw sparseAbortError(signal);
1246
+ const sparseDetails = sparse ? {
1247
+ method: 'coarse-grid-luminance-change-wakeup',
1248
+ grid_width: 32,
1249
+ grid_height: 18,
1250
+ sample_count: sparse.sampleCount,
1251
+ changed_cells: sparse.changedCells,
1252
+ triggered: sparse.triggered,
1253
+ reason: sparse.reason,
1254
+ elapsed_ms: sparse.elapsedMs,
1255
+ wall_elapsed_ms: sparseWallMs,
1256
+ fresh_full_observation_follows: true,
1257
+ interpretation: 'Wake signal only. A timeout or no detected change is not evidence that the screen stayed unchanged; small, brief, or color-only changes can be missed.',
1258
+ ...(sparse.error ? { warning: sparse.error } : {}),
1259
+ } : undefined;
906
1260
  const outPath = ephemeralScreenshotPath('observe');
907
1261
  let retainedForVision = false;
908
1262
  try {
@@ -913,23 +1267,29 @@ async function screenshot(workspacePath, ownerId, allowEphemeralVisionImage, inc
913
1267
  const foreground = await foregroundWindowHandle();
914
1268
  const [result, ui, apps] = await Promise.all([
915
1269
  runPowerShell(script, 30000),
916
- observeUiAutomation(30000, foreground.handle, 750, 'uia_advisory'),
917
- observeAppWindows(30000, 750, 'windows_advisory'),
1270
+ includeUiTree && foreground.handle
1271
+ ? observeUiAutomation(30000, foreground.handle, 750, 'uia_advisory')
1272
+ : Promise.resolve(skippedUiAutomation(includeUiTree ? 'Foreground window handle is unavailable; UI Automation tree capture was skipped.' : undefined)),
1273
+ includeUiTree
1274
+ ? observeAppWindows(30000, 750, 'windows_advisory')
1275
+ : Promise.resolve({ apps: [], error: undefined, elapsedMs: 0 }),
918
1276
  ]);
1277
+ if (signal?.aborted)
1278
+ throw sparseAbortError(signal);
919
1279
  if (!result.ok)
920
- return captureFailure('observe');
1280
+ return { ...captureFailure('observe'), ...(sparseDetails ? { sparse_measurement: sparseDetails } : {}) };
921
1281
  let parsed = {};
922
1282
  try {
923
1283
  parsed = JSON.parse(result.output);
924
1284
  }
925
1285
  catch {
926
- return captureFailure('observe');
1286
+ return { ...captureFailure('observe'), ...(sparseDetails ? { sparse_measurement: sparseDetails } : {}) };
927
1287
  }
928
1288
  const screenWidth = Number(parsed.width || 1);
929
1289
  const screenHeight = Number(parsed.height || 1);
930
1290
  const objects = semanticObjects(ui.elements, screenWidth, screenHeight, workspacePath, ownerId);
931
1291
  const generation = sceneGeneration(apps.apps, ui.elements);
932
- observations.set(observationKey(workspacePath, ownerId), { workspacePath, objects, width: screenWidth, height: screenHeight, at: new Date().toISOString(), sceneGeneration: generation });
1292
+ observations.set(observationKey(workspacePath, ownerId), { workspacePath, objects, windowHandle: foreground.handle || undefined, width: screenWidth, height: screenHeight, at: new Date().toISOString(), sceneGeneration: generation });
933
1293
  const payload = {
934
1294
  ok: true,
935
1295
  action: 'observe',
@@ -937,28 +1297,47 @@ async function screenshot(workspacePath, ownerId, allowEphemeralVisionImage, inc
937
1297
  screenshot_retention: allowEphemeralVisionImage ? 'ephemeral-delete-after-vision-input' : 'ephemeral-deleted-before-tool-return',
938
1298
  coordinate_system: 'virtual-screen-pixels',
939
1299
  perception: {
940
- mode: 'native-screenshot-plus-windows-ui-automation',
1300
+ mode: includeUiTree ? 'native-screenshot-plus-windows-ui-automation' : 'native-screenshot-only',
1301
+ ui_tree_status: includeUiTree ? (ui.error ? 'unavailable' : 'captured') : 'skipped',
1302
+ ui_tree_skipped: !includeUiTree,
941
1303
  vision_assist: {
942
- when_model_supports_vision: 'The Agent sends this screenshot as image input together with the UI Automation element tree, so visual recognition and control bounding boxes can be used in the same decision step.',
943
- fallback_without_vision: 'Use the UI Automation element tree, bbox, center coordinates, and semantic target IDs only; no screenshot file path is retained or exposed.',
1304
+ when_model_supports_vision: includeUiTree
1305
+ ? 'The Agent sends this screenshot as image input together with the UI Automation element tree, so visual recognition and control bounding boxes can be used in the same decision step.'
1306
+ : 'The Agent receives this screenshot as image input without UI Automation traversal or semantic target IDs.',
1307
+ fallback_without_vision: includeUiTree
1308
+ ? 'Use the UI Automation element tree, bbox, center coordinates, and semantic target IDs only; no screenshot file path is retained or exposed.'
1309
+ : 'This result has no semantic target IDs; request include_ui_tree=true for a UI Automation element tree.',
944
1310
  },
945
1311
  element_count: ui.elements.length,
946
1312
  visited_count: ui.visited,
947
1313
  scene_generation: generation,
948
1314
  scene_summary: buildSceneSummary(objects, screenWidth, screenHeight),
949
1315
  objects: compactSemanticObjects(objects),
950
- object_usage: 'Prefer target_id from scene_summary.high_priority_objects or objects for click/move/scroll when available; it resolves to the latest observed center coordinate and keeps actions tied to a semantic UI object.',
1316
+ ...(sparseDetails ? { sparse_measurement: sparseDetails } : {}),
1317
+ object_usage: includeUiTree
1318
+ ? 'Prefer target_id from scene_summary.high_priority_objects or objects for click/move/scroll when available; it resolves to the latest observed center coordinate and keeps actions tied to a semantic UI object.'
1319
+ : 'This screenshot-only observation has no semantic target IDs. Use visual coordinates or request another observe with include_ui_tree=true before using target_id.',
951
1320
  warning: foreground.error || ui.error || apps.error || undefined,
952
- note: 'Inspired by capture-parse-decide-act and MCP-style Computer Control agents: screenshot capture plus native Windows UI Automation text/control bounding boxes, semantic target objects, stable target IDs, high-priority object summaries, normalized bboxes, allowed actions, and risk hints. It does not copy third-party code.',
1321
+ note: includeUiTree
1322
+ ? 'Native screenshot plus a bounded, foreground-window-scoped Windows UI Automation tree. No third-party server or code is loaded.'
1323
+ : 'Fast native screenshot path; UI Automation traversal was intentionally skipped. No third-party server or code is loaded.',
953
1324
  },
954
1325
  telemetry: {
955
1326
  capture_ms: result.elapsedMs,
956
1327
  uia_ms: ui.elapsedMs,
1328
+ ui_tree_skipped: !includeUiTree,
957
1329
  app_list_ms: apps.elapsedMs,
958
1330
  foreground_ms: foreground.elapsedMs,
959
1331
  total_ms: Date.now() - totalStarted,
960
1332
  element_count: ui.elements.length,
961
1333
  visited_count: ui.visited,
1334
+ ...(sparse ? {
1335
+ sparse_sampling_ms: sparse.elapsedMs,
1336
+ sparse_wall_ms: sparseWallMs,
1337
+ sparse_configured_wait_ms: sparseDurationMs,
1338
+ sparse_sample_count: sparse.sampleCount,
1339
+ sparse_triggered: sparse.triggered,
1340
+ } : {}),
962
1341
  },
963
1342
  ...parsed,
964
1343
  };
@@ -1093,7 +1472,9 @@ async function foregroundSceneGeneration() {
1093
1472
  const handle = handleResult.handle;
1094
1473
  const [apps, ui] = await Promise.all([
1095
1474
  observeAppWindows(30000),
1096
- observeUiAutomation(30000, handle),
1475
+ handle
1476
+ ? observeUiAutomation(30000, handle)
1477
+ : Promise.resolve(skippedUiAutomation('Foreground window handle is unavailable; UI Automation tree capture was skipped.')),
1097
1478
  ]);
1098
1479
  return {
1099
1480
  generation: sceneGeneration(apps.apps, ui.elements),
@@ -1207,6 +1588,7 @@ function stringifyComputerUseResult(result) {
1207
1588
  exports.computerUseInternals = {
1208
1589
  cleanupStaleScreenshots,
1209
1590
  ephemeralScreenshotPath,
1591
+ sparseMeasurementSampleBudget,
1210
1592
  stringifyComputerUseResult,
1211
1593
  };
1212
1594
  async function runComputerUse(options) {
@@ -1230,7 +1612,7 @@ async function runComputerUse(options) {
1230
1612
  result = { ok: true, action: 'takeover_stop', takeover: false };
1231
1613
  }
1232
1614
  else if (action === 'observe') {
1233
- result = await screenshot(options.workspacePath, ownerId, !!options.allowEphemeralVisionImage, !!options.includeRawUi, options.captureMaxWidth, options.captureMaxHeight);
1615
+ result = await screenshot(options.workspacePath, ownerId, !!options.allowEphemeralVisionImage, !!options.includeRawUi, options.captureMaxWidth, options.captureMaxHeight, options.sparseWaitMs, options.signal, options.includeUiTree !== false);
1234
1616
  }
1235
1617
  else if (action === 'app_list') {
1236
1618
  const first = await observeAppWindows(Number(options.maxChars || 60000));
@@ -1247,7 +1629,7 @@ async function runComputerUse(options) {
1247
1629
  else if (action === 'app_observe') {
1248
1630
  const selected = await selectAppWindow(options.appTarget, options.windowHandle);
1249
1631
  result = selected.app
1250
- ? await cropScreenshot(options.workspacePath, ownerId, !!options.allowEphemeralVisionImage, selected.app, !!options.includeRawUi, options.captureMaxWidth, options.captureMaxHeight)
1632
+ ? await cropScreenshot(options.workspacePath, ownerId, !!options.allowEphemeralVisionImage, selected.app, !!options.includeRawUi, options.captureMaxWidth, options.captureMaxHeight, options.includeUiTree !== false)
1251
1633
  : { ok: false, action, error: selected.error, applications: selected.apps.slice(0, 20) };
1252
1634
  }
1253
1635
  else if (action === 'app_activate') {
@@ -1257,6 +1639,9 @@ async function runComputerUse(options) {
1257
1639
  else if (action === 'wait') {
1258
1640
  result = await wait(Number(options.durationMs || 1000));
1259
1641
  }
1642
+ else if (action === 'wait_for') {
1643
+ result = await waitForUi(options, ownerId);
1644
+ }
1260
1645
  else if (action === 'sequence') {
1261
1646
  result = await executeSequence(options, ownerId);
1262
1647
  }
@@ -1,4 +1,4 @@
1
- type HelperLane = 'action' | 'uia' | 'windows' | 'uia_advisory' | 'windows_advisory';
1
+ type HelperLane = 'action' | 'uia' | 'windows' | 'uia_advisory' | 'windows_advisory' | 'sparse';
2
2
  export interface PowerShellResult {
3
3
  ok: boolean;
4
4
  output: string;
@@ -6,6 +6,11 @@ export interface PowerShellResult {
6
6
  }
7
7
  export declare function runPersistentPowerShell(script: string, timeoutMs?: number, lane?: HelperLane): Promise<PowerShellResult>;
8
8
  export declare function computerUsePowerShellLaneReady(lane: HelperLane): boolean;
9
+ export declare function prepareComputerUsePowerShellLane(lane: HelperLane, timeoutMs: number): Promise<{
10
+ ready: boolean;
11
+ elapsedMs: number;
12
+ }>;
13
+ export declare function stopComputerUsePowerShellLane(lane: HelperLane): void;
9
14
  export declare function stopComputerUsePowerShellHost(): void;
10
15
  export {};
11
16
  //# sourceMappingURL=computerUsePowerShellHost.d.ts.map