# Phase 6: Integration and Hardening - Research

**Researched:** 2026-04-12
**Domain:** DWM capture integration, fallback logic, crash resilience, resource leak prevention
**Confidence:** HIGH

## Summary

Phase 6 wires the Phase 5 native DWM capture addon into the existing WindowTarget and WindowRegionTarget classes. The work is primarily TypeScript-level integration: adding a `captureWindowBest()` helper to window-utils.ts that tries DWM first then falls back to node-screenshots, modifying both target classes to use it, and adding a sharp-based crop path for WindowRegionTarget (since DWM returns PNG buffers, not node-screenshots Image objects).

The native addon (capture.cpp, addon.cpp) and its TypeScript wrapper (dwm-capture.ts) already handle crash isolation (SEH), HWND validation, RAII resource management, and error-to-null conversion. Phase 6 does NOT modify native C++ code. The integration layer simply calls `captureWindowDwm(hwnd)` and checks the result.

**Primary recommendation:** Implement `captureWindowBest(win)` in window-utils.ts as the single integration point. Both WindowTarget and WindowRegionTarget call this function. The function probes DWM availability once (cached), tries DWM capture per-call, and falls back to the existing `win.captureImageSync()` + `toPngSync()` path when DWM returns null.

<user_constraints>

## User Constraints (from CONTEXT.md)

### Locked Decisions
- **D-01:** Modify `WindowTarget.capture()` to try DWM capture first via `captureWindowDwm(hwnd)`, fall back to existing capture when DWM returns null
- **D-02:** Modify `WindowRegionTarget.capture()` similarly -- get full window PNG via DWM, then crop sub-region using sharp
- **D-03:** Add `captureWindowBest(win)` helper to `window-utils.ts` that encapsulates DWM-first-then-fallback logic
- **D-04:** No new CaptureTarget subclass -- DWM is a capture mechanism, not a different target
- **D-05:** No changes to server.ts, CaptureConfig, or MCP tool schemas -- integration is fully internal
- **D-06:** Probe DWM availability once at first capture, cache the result
- **D-07:** If DWM fails for a specific window, fall back to monitor-crop for that capture only -- don't disable DWM globally
- **D-08:** Log which capture backend was used at debug level
- **D-09:** If native addon fails to load (MODULE_NOT_FOUND), `isDwmCaptureAvailable()` returns false, all captures silently use fallback
- **D-10:** Check `IsWindow(hwnd)` before capture (already in Phase 5 native code)
- **D-11:** Minimized windows: throw structured error (existing behavior)
- **D-12:** Cloaked UWP windows: if DWM fails, fallback also fails, let existing error propagate
- **D-13:** SEH wrapper already in place from Phase 5
- **D-14:** TypeScript wrapper already catches all errors and returns null
- **D-15:** No child-process isolation
- **D-16:** All D3D11 resources use ComPtr RAII wrappers (Phase 5)
- **D-17:** Add GDI handle leak test: 100+ captures, check GetGuiResources() before/after, stable within +/- 5
- **D-18:** Staging texture created/destroyed per capture, no persistent GPU resources
- **D-19:** DWM returns PNG buffer; use sharp to decode, crop, re-encode for WindowRegionTarget
- **D-20:** Replace `win.captureImageSync()` in WindowRegionTarget (WM_PRINT flicker source)

### Claude's Discretion
- Whether to cache the sharp import or import at module level (RECOMMENDATION: import at module level, matching established project pattern)
- Exact log message wording for backend selection
- Whether to add a `captureBackend` field to session metadata (nice-to-have)
- Test file organization for the leak test

### Deferred Ideas (OUT OF SCOPE)
None

</user_constraints>

<phase_requirements>

## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| DWM-06 | WindowTarget automatically uses DWM capture when native addon is available | `captureWindowBest()` helper tries DWM first; WindowTarget calls it instead of `win.captureImageSync()` |
| DWM-07 | WindowTarget falls back to monitor-crop when addon unavailable or DWM fails | `captureWindowBest()` returns fallback PNG when `captureWindowDwm()` returns null |
| DWM-08 | WindowRegionTarget also benefits from DWM capture | WindowRegionTarget uses `captureWindowBest()` then sharp `.extract()` for crop |
| DWM-09 | Fallback is transparent -- agents see no difference in MCP tool interface | No changes to server.ts, CaptureConfig, or tool schemas (D-05) |
| DWM-10 | Native addon crash does not terminate MCP server | Already handled by Phase 5 SEH + TypeScript try/catch returning null |
| DWM-11 | Addon handles minimized, cloaked, destroyed windows gracefully | Minimized: error thrown before DWM attempt; cloaked: DWM fails, fallback fails, error propagates; destroyed: IsWindow() check in native code |
| DWM-12 | COM/DirectX resources properly released, no GDI handle leaks | Phase 5 RAII; Phase 6 adds leak validation test with GetGuiResources() |

</phase_requirements>

## Architecture Patterns

### Critical Code Discovery: Current State vs. CONTEXT.md Description

The CONTEXT.md references `captureWindowViaMonitor(win)` as an existing function in window-utils.ts. **This function does not exist.** [VERIFIED: codebase grep] The actual code in both targets uses `win.captureImageSync()` directly -- this is node-screenshots' WM_PRINT-based capture, NOT a monitor-crop approach.

The `captureWindowBest()` helper must therefore implement TWO things:
1. DWM-first path: call `captureWindowDwm(win.id())`
2. Fallback path: call `win.captureImageSync()` then `.toPngSync()` (the current approach)

There is no existing "monitor-crop" function to fall back to. The fallback IS the existing `captureImageSync()` approach from node-screenshots.

### Recommended Integration Structure

```
src/capture/targets/
  window-utils.ts          # Add captureWindowBest(win) + DWM availability cache
  window-target.ts         # Simplify: findWindow() -> captureWindowBest(win)
  window-region-target.ts  # Modify: captureWindowBest(win) -> sharp extract -> PNG
  dwm-capture.ts           # Unchanged (Phase 5 deliverable)
  capture-target.ts        # Unchanged
```

### Pattern 1: captureWindowBest() Helper

**What:** Single function encapsulating DWM-first-then-fallback logic
**When to use:** Called by both WindowTarget and WindowRegionTarget

```typescript
// window-utils.ts additions
import { isDwmCaptureAvailable, captureWindowDwm } from "./dwm-capture.js";
import type { Window } from "node-screenshots";
import { logger } from "../../logger.js";

let dwmAvailable: boolean | null = null; // Cached after first probe (D-06)

export async function captureWindowBest(win: Window): Promise<Buffer> {
  // Probe DWM availability once, cache result
  if (dwmAvailable === null) {
    dwmAvailable = isDwmCaptureAvailable();
    logger.debug(`DWM capture available: ${dwmAvailable}`);
  }

  // Try DWM first if available (D-01, D-07)
  if (dwmAvailable) {
    const hwnd = win.id();
    const pngBuffer = await captureWindowDwm(hwnd);
    if (pngBuffer !== null) {
      logger.debug(`Captured window "${win.title()}" via DWM`);
      return pngBuffer;
    }
    // DWM failed for this window — fall back (D-07: per-window, not global)
    logger.debug(`DWM capture failed for "${win.title()}", falling back`);
  }

  // Fallback: node-screenshots captureImageSync (D-09)
  const image = win.captureImageSync();
  if (image.width === 0 || image.height === 0) {
    throw new Error(`Window capture returned empty image: "${win.title()}"`);
  }
  logger.debug(`Captured window "${win.title()}" via node-screenshots fallback`);
  return image.toPngSync();
}
```

[VERIFIED: codebase code — Window.captureImageSync(), toPngSync(), win.id() all confirmed in existing code]

### Pattern 2: Simplified WindowTarget

```typescript
// window-target.ts — simplified
async capture(): Promise<Buffer> {
  const win = findWindow(this.handle, this.titleMatch);
  if (!win) {
    throw new Error(`Window not found: ...`);
  }
  if (win.isMinimized()) {
    throw new Error(`Window is minimized: "${win.title()}"`);
  }
  return captureWindowBest(win);
}
```

### Pattern 3: WindowRegionTarget with Sharp Crop

```typescript
// window-region-target.ts — DWM + sharp crop
import sharp from "sharp";

async capture(): Promise<Buffer> {
  const win = findWindow(this.windowHandle, this.windowTitle);
  if (!win) { throw new Error(`Window not found: ...`); }
  if (win.isMinimized()) { throw new Error(`Window is minimized: ...`); }

  const fullPng = await captureWindowBest(win);

  // Get dimensions from the PNG to validate crop bounds
  const metadata = await sharp(fullPng).metadata();
  const imgW = metadata.width!;
  const imgH = metadata.height!;

  if (this.regionX >= imgW || this.regionY >= imgH) {
    throw new Error(`Region outside window bounds`);
  }

  const clampedW = Math.min(this.regionWidth, imgW - this.regionX);
  const clampedH = Math.min(this.regionHeight, imgH - this.regionY);

  const cropped = await sharp(fullPng)
    .extract({ left: this.regionX, top: this.regionY, width: clampedW, height: clampedH })
    .png()
    .toBuffer();

  return cropped;
}
```

[VERIFIED: sharp `.extract()` and `.metadata()` confirmed available via runtime check]

### Anti-Patterns to Avoid

- **Re-probing DWM every frame:** Wastes time; probe once and cache (D-06). The addon's `isDwmCaptureAvailable()` already does its own internal caching but the TypeScript layer should also cache to avoid unnecessary NAPI calls.
- **Disabling DWM globally on per-window failure:** Some windows legitimately don't have DWM surfaces (e.g., console windows). Per-window fallback is correct (D-07).
- **Using sharp(fullPng).metadata() then sharp(fullPng).extract() as two separate reads:** This decodes the PNG twice. However, sharp's pipeline is efficient and the alternative (chaining) requires knowing dimensions before extract. The two-call approach is the cleanest pattern. The ~5-10ms cost is acceptable per D-19.
- **Returning Image objects from captureWindowBest:** DWM returns PNG Buffer, not a node-screenshots Image. The function must return Buffer for consistency. This means WindowRegionTarget cannot use `.cropSync()` from node-screenshots -- it must use sharp.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| PNG crop | Manual pixel buffer manipulation | sharp `.extract()` | Handles stride, color space, re-encoding correctly |
| DWM availability detection | Custom Windows API checks in TS | `isDwmCaptureAvailable()` from dwm-capture.ts | Already implemented, handles load failure gracefully |
| Crash isolation | Process-level isolation or domain error handlers | Phase 5 SEH + try/catch layering | Already bulletproof: native SEH catches AV, TS wrapper converts to null |
| GDI handle counting | Manual P/Invoke or child process | Native addon + `GetGuiResources()` in test script | Windows API, must be called from same process |

## Common Pitfalls

### Pitfall 1: Double PNG Decode in WindowRegionTarget
**What goes wrong:** Calling `sharp(buffer).metadata()` then `sharp(buffer).extract()` decodes the PNG twice.
**Why it happens:** Need image dimensions before deciding crop bounds.
**How to avoid:** Accept the double-decode cost (~5-10ms total). Alternatively, chain `.metadata()` callback into `.extract()` but this complicates error handling without meaningful perf gain.
**Warning signs:** N/A -- this is a known acceptable trade-off per D-19.

### Pitfall 2: Forgetting to Handle the "Both Paths Fail" Case
**What goes wrong:** DWM fails (returns null), fallback also fails (e.g., window destroyed between DWM attempt and fallback).
**Why it happens:** Window state changes between the two capture attempts.
**How to avoid:** The fallback path (`win.captureImageSync()`) throws on failure. Let the error propagate -- the scheduler handles it with its skip/retry logic (3 consecutive failures = abort).
**Warning signs:** Silently returning empty buffers instead of throwing.

### Pitfall 3: sharp Import in Module That Didn't Previously Use It
**What goes wrong:** WindowRegionTarget now needs sharp, adding a dependency that wasn't there before.
**Why it happens:** Current code uses `image.cropSync()` from node-screenshots; new code needs `sharp().extract()`.
**How to avoid:** Add `import sharp from "sharp"` at module level, matching the established project pattern used in all processing/ files. [VERIFIED: 12 files use `import sharp from "sharp"` at module level]

### Pitfall 4: win.captureImageSync() Already Throws for Destroyed Windows
**What goes wrong:** Assuming you need to add destroyed-window handling in TypeScript.
**Why it happens:** Over-engineering based on DWM-11 requirement.
**How to avoid:** The existing behavior already handles this. `findWindow()` returns null if the window no longer exists (re-enumerates every call). `win.captureImageSync()` throws if the window handle becomes invalid between find and capture. Both produce structured errors that the scheduler catches. No new code needed for this case.

### Pitfall 5: node-screenshots captureImageSync DOES Use WM_PRINT
**What goes wrong:** Assuming the current code uses "monitor-crop" (as stated in some planning docs).
**Why it happens:** CONTEXT.md describes `captureWindowViaMonitor(win)` but this function does not exist in the codebase.
**How to avoid:** The actual fallback is `win.captureImageSync()` which uses WM_PRINT internally (causes flicker). The DWM upgrade eliminates this for supported windows. For windows where DWM fails, the fallback still flickers -- this is a known limitation, not a bug.

## Code Examples

### sharp extract (crop) API
```typescript
// Source: verified via runtime — sharp(buffer).extract() confirmed available
import sharp from "sharp";

const cropped = await sharp(pngBuffer)
  .extract({ left: 10, top: 20, width: 100, height: 50 })
  .png()
  .toBuffer();
```
[VERIFIED: sharp .extract() exists at runtime]

### sharp metadata API
```typescript
const { width, height } = await sharp(pngBuffer).metadata();
```
[VERIFIED: sharp .metadata() used extensively in project test files]

### DWM capture wrapper (existing Phase 5 code)
```typescript
// From src/capture/targets/dwm-capture.ts — DO NOT MODIFY
import { isDwmCaptureAvailable, captureWindowDwm } from "./dwm-capture.js";

// Returns true if DWM shared surface capture is available
isDwmCaptureAvailable(); // boolean

// Returns PNG Buffer or null on failure
await captureWindowDwm(hwnd); // Promise<Buffer | null>
```
[VERIFIED: actual file contents confirmed]

### Window.id() returns HWND as number
```typescript
// From window-utils.ts existing code
const win = findWindow(handle, title);
const hwnd = win.id(); // number — the Windows HWND value
```
[VERIFIED: used in window-utils.ts line 16, server.ts line 482]

## GDI Handle Leak Test Pattern

The leak test (D-17) should call the native addon directly in a loop and compare handle counts. Pattern:

```typescript
// Test script concept — runs 100+ captures and checks handle stability
import { captureWindowDwm, isDwmCaptureAvailable } from "./dwm-capture.js";

// GetGuiResources must be called via the native addon or a separate FFI
// since it's a Windows API. Options:
// 1. Add a getGdiHandleCount() export to the native addon
// 2. Use a standalone node script with child_process to call PowerShell
// 3. Use node-ffi-napi to call GetGuiResources directly

// Simplest: add to native addon as a test helper
// In addon.cpp: GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS)
```

**Recommendation for planner:** The leak test needs `GetGuiResources()` which is a Windows API. The simplest approach is adding a `getGdiHandleCount()` export to the native addon. This is a one-line function in addon.cpp. Alternatively, use PowerShell via child_process but that adds latency and complexity. [ASSUMED]

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `win.captureImageSync()` (WM_PRINT) | DWM shared surface via native addon | Phase 5-6 (this milestone) | Eliminates flicker, ignores occlusion |
| `image.cropSync()` (node-screenshots) | `sharp().extract()` (sharp) | Phase 6 (this phase) | Works with PNG buffers from DWM, consistent with project patterns |

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | GDI leak test is best implemented by adding `getGdiHandleCount()` to the native addon | GDI Handle Leak Test | Low -- PowerShell fallback exists; test still runs, just slower |
| A2 | sharp double-decode (metadata + extract) costs ~5-10ms | Pitfall 1 | Low -- even if 20ms, acceptable for capture intervals of 500ms+ |

## Environment Availability

| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| sharp | WindowRegionTarget crop | Yes | ^0.34.5 (in package.json) | -- |
| native addon (dwm-capture.node) | DWM capture path | Depends on Phase 5 | -- | node-screenshots fallback |
| node-screenshots | Fallback capture | Yes | ^0.2.8 (in package.json) | -- |

**Missing dependencies with no fallback:**
- None -- the entire point of this phase is graceful fallback when DWM is unavailable.

**Missing dependencies with fallback:**
- Native addon may not be built yet (Phase 5 dependency). Fallback is the existing capture path.

## Open Questions

1. **GetGuiResources() access for leak test**
   - What we know: Need to call `GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS)` from within the Node.js process to count GDI handles.
   - What's unclear: Whether to add this as a native addon export or use an external approach.
   - Recommendation: Add `getGdiHandleCount()` to addon.cpp -- it's a trivial addition and keeps the test self-contained.

2. **Phase 5 completion status**
   - What we know: Phase 5 files exist in the repo (dwm-capture.ts, capture.cpp, addon.cpp). STATE.md shows Phase 5 plans as completed.
   - What's unclear: Whether the native addon has been tested end-to-end and produces valid captures.
   - Recommendation: Phase 6 plan should include a smoke test of `captureWindowDwm()` before integration work begins.

## Sources

### Primary (HIGH confidence)
- Codebase grep of src/capture/targets/ -- all target implementations, window-utils.ts, dwm-capture.ts
- Codebase grep of src/processing/ -- sharp usage patterns (12 files using `import sharp from "sharp"`)
- Runtime verification of sharp `.extract()` method availability
- Phase 5 CONTEXT.md and native source code (capture.cpp, addon.cpp)

### Secondary (MEDIUM confidence)
- Phase 6 CONTEXT.md decisions (D-01 through D-20) -- user-locked decisions

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH -- all libraries already in use, no new dependencies
- Architecture: HIGH -- integration points clearly defined, actual code reviewed, patterns verified
- Pitfalls: HIGH -- discovered code/docs mismatch (captureWindowViaMonitor doesn't exist), all edge cases mapped

**Research date:** 2026-04-12
**Valid until:** 2026-05-12 (stable -- no external dependency changes expected)
