# Phase 9: Timing Profiles - Research

**Researched:** 2026-04-13
**Domain:** Timing profile persistence, built-in presets, MCP tool registration, profile resolution
**Confidence:** HIGH

## Summary

Phase 9 adds named timing profiles to the existing profile infrastructure built in Phase 8. The implementation mirrors the screenshot profile pattern almost exactly -- same JSON file (under the `"timing"` key), same slugification, same overwrite semantics, same `capture_from_current` pattern. The primary new elements are: (1) a `TimingProfile` type storing timing parameters and diagnostic flags, (2) four hardcoded built-in presets, (3) a `parameter_summary` generator for human-readable descriptions, and (4) a timing profile resolver that merges profile values into `start_capture` params.

No new libraries are needed. The existing `ProfileManager` class can be extended (or a parallel `TimingProfileManager` created) to handle timing profiles in the same JSON file. The `profile-types.ts` module gains a `TimingProfile` interface. The `profile-resolver.ts` module gains a `resolveTimingProfile` function. Three new MCP tools are registered following the established pattern.

**Primary recommendation:** Extend the existing `ProfileManager` to handle timing profiles (adding timing-specific CRUD methods), or create a parallel `TimingProfileManager` that shares the same file I/O. The built-in presets should be defined in a separate `timing-presets.ts` module for clarity. The `resolveTimingProfile` function should be a pure function alongside `resolveScreenshotProfile`.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- **D-01:** Timing profiles stored in the same JSON file as screenshot profiles, under the `"timing"` top-level key. File path from `SCREEN_TIMELAPSE_PROFILES_PATH` env var, defaulting to `.screen-timelapse/profiles.json`.
- **D-02:** Save with existing name overwrites silently (same as screenshot profiles).
- **D-03:** Profile names case-insensitive, slugified for storage key, display name preserved in `display_name` field.
- **D-04:** A timing profile stores: `name`, `display_name`, `interval_ms` (min 100), `max_frames` (min 1, max 50), `duration_ms` (optional, min 100), `jpeg_quality` (1-100, default 80), `delta_highlight` (boolean, default false), `compress_idle` (boolean, default false), `gif_export` (boolean, default false), `description` (optional), `created_at`, `updated_at`.
- **D-05:** Diagnostic flags included because they control how a capture runs. Optional, default false.
- **D-06:** All timing parameters are optional. Partial presets valid (e.g., just `interval_ms` and `gif_export: true`).
- **D-07:** Three new MCP tools: `save_timing_profile`, `list_timing_profiles`, `delete_timing_profile`.
- **D-08:** Tool params use same snake_case names as `start_capture`. Zero translation.
- **D-09:** Validation ranges match `start_capture`: interval_ms >= 100, max_frames 1-50, jpeg_quality 1-100, duration_ms >= 100. Zero-param profile valid.
- **D-10:** `start_capture` gains optional `timing_profile` param. Named profile provides timing/diagnostic param base; inline params override.
- **D-11:** Both `screenshot_profile` and `timing_profile` can be used together. Resolution: timing profile base -> screenshot profile base -> inline params override.
- **D-12:** Missing timing profile returns structured error with available profile names.
- **D-13:** Four built-in presets: `quick-glance`, `steady-watch`, `debug-flicker`, `slow-monitor`. Hardcoded, not persisted. Listed with `builtin: true` tag.
- **D-14:** User profiles shadow built-in names (user version takes priority).
- **D-15:** Built-in presets make tool immediately useful without setup.
- **D-16:** `capture_from_current` mode: `source_session` auto-populates timing params from session config.
- **D-17:** `parameter_summary` string per profile in list output (e.g., "every 500ms, max 6 frames").

### Claude's Discretion
- Internal JSON structure and field ordering
- Error message wording
- Whether built-in presets are defined in a separate module or inline
- `list_timing_profiles` sort order
- Whether to create a shared `ProfileManager` base class or keep screenshot/timing profile logic in separate modules

### Deferred Ideas (OUT OF SCOPE)
- Profile composition/inheritance (extending one profile from another)
- Profile validation against active monitor capabilities
</user_constraints>

## Standard Stack

No new libraries needed. This phase uses only what is already installed.

### Core (existing)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Node.js `fs/promises` | built-in | JSON file read/write (shared with screenshot profiles) | Already used by ProfileManager [VERIFIED: src/profiles/profile-manager.ts] |
| zod | ^3.25.0 | Tool input schemas, param validation | Already used by all MCP tools [VERIFIED: src/types.ts] |
| @modelcontextprotocol/sdk | ^1.29.0 | `registerTool` for new MCP tools | Already used in server.ts [VERIFIED: src/server.ts] |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Extending ProfileManager | New TimingProfileManager class | Separate class avoids bloating ProfileManager but duplicates file I/O. Either approach works; discretion item. |
| Inline built-in presets | Separate `timing-presets.ts` module | Separate module keeps server.ts clean and makes presets testable independently. Recommended. |

## Architecture Patterns

### Recommended Project Structure
```
src/
  profiles/
    profile-types.ts          # Add TimingProfile interface, ProfilesFileData update
    profile-manager.ts         # Add timing profile CRUD methods (or new class)
    profile-resolver.ts        # Add resolveTimingProfile function
    timing-presets.ts          # NEW: Built-in timing presets + parameter_summary generator
  server.ts                    # Add 3 new tool registrations + modify start_capture with timing_profile param
```

[VERIFIED: src/profiles/ directory exists with profile-types.ts, profile-manager.ts, profile-resolver.ts from Phase 8]

### Pattern 1: TimingProfile Type
**What:** Interface mirroring CaptureConfig timing fields, stored under `"timing"` key in the shared JSON file.

```typescript
// Source: Derived from CaptureConfig in src/types.ts + CONTEXT.md D-04
export interface TimingProfile {
  slug: string;
  displayName: string;
  intervalMs?: number;        // min 100
  maxFrames?: number;         // 1-50
  durationMs?: number;        // min 100
  jpegQuality?: number;       // 1-100
  deltaHighlight?: boolean;   // default false
  compressIdle?: boolean;     // default false
  gifExport?: boolean;        // default false
  description?: string;
  createdAt: string;          // ISO 8601
  updatedAt: string;          // ISO 8601
  builtin?: boolean;          // true for hardcoded presets (read-only)
}
```

**Key difference from ScreenshotProfile:** All domain fields are optional (D-06). A profile with just `description` or just one flag is valid. The `builtin` field is only set on presets returned by list -- never persisted.

### Pattern 2: Extending ProfileManager for Timing Profiles
**What:** Add timing-specific CRUD to the existing ProfileManager class. It already stores `timingData` and preserves it during persist.

```typescript
// Source: Existing ProfileManager in src/profiles/profile-manager.ts
// The class already has: this.timingData: Record<string, unknown> = {}
// Upgrade to: this.timingProfiles: Map<string, TimingProfile> | null = null

// Add methods parallel to screenshot profile methods:
async saveTiming(input: SaveTimingProfileInput): Promise<TimingProfile>;
async getTiming(slug: string): Promise<TimingProfile | undefined>;
async deleteTiming(slug: string): Promise<boolean>;
async listTiming(): Promise<TimingProfile[]>;  // Merges user + builtin
async saveTimingFromSession(name: string, sessionId: string, sessionManager: ...): Promise<TimingProfile>;
```

**Why extend vs. new class:** The ProfileManager already owns the file, handles lazy loading, and preserves the timing key. Adding timing methods avoids duplicating all the file I/O, load, and persist logic. The methods are parallel and don't interfere with screenshot methods.

[VERIFIED: ProfileManager.timingData exists on line 32 of profile-manager.ts as `Record<string, unknown>` -- designed for Phase 9 upgrade]

### Pattern 3: Built-in Presets
**What:** Hardcoded timing profiles available without any prior save call (D-13).

```typescript
// Source: CONTEXT.md D-13
export const BUILTIN_TIMING_PRESETS: Record<string, TimingProfile> = {
  "quick-glance": {
    slug: "quick-glance",
    displayName: "Quick Glance",
    intervalMs: 500,
    maxFrames: 6,
    description: "Fast 3-second snapshot burst for checking if something is happening",
    builtin: true,
    createdAt: "2026-01-01T00:00:00.000Z",
    updatedAt: "2026-01-01T00:00:00.000Z",
  },
  "steady-watch": {
    slug: "steady-watch",
    displayName: "Steady Watch",
    intervalMs: 2000,
    maxFrames: 20,
    durationMs: 40000,
    description: "40-second watch at 2s intervals for observing gradual changes",
    builtin: true,
    createdAt: "2026-01-01T00:00:00.000Z",
    updatedAt: "2026-01-01T00:00:00.000Z",
  },
  "debug-flicker": {
    slug: "debug-flicker",
    displayName: "Debug Flicker",
    intervalMs: 200,
    maxFrames: 30,
    deltaHighlight: true,
    description: "Rapid capture with delta highlighting for diagnosing visual flicker",
    builtin: true,
    createdAt: "2026-01-01T00:00:00.000Z",
    updatedAt: "2026-01-01T00:00:00.000Z",
  },
  "slow-monitor": {
    slug: "slow-monitor",
    displayName: "Slow Monitor",
    intervalMs: 5000,
    maxFrames: 50,
    durationMs: 300000,
    compressIdle: true,
    description: "5-minute idle-compressed monitor for long-running processes",
    builtin: true,
    createdAt: "2026-01-01T00:00:00.000Z",
    updatedAt: "2026-01-01T00:00:00.000Z",
  },
};
```

**Shadow semantics (D-14):** When resolving a timing profile name, check user profiles first. If not found, check built-ins. When listing, merge both sets -- user profiles override builtins with same slug.

### Pattern 4: Parameter Summary Generator (D-17)
**What:** Human-readable one-liner describing a timing profile's configuration.

```typescript
export function generateParameterSummary(profile: TimingProfile): string {
  const parts: string[] = [];
  if (profile.intervalMs !== undefined) {
    parts.push(profile.intervalMs >= 1000
      ? `every ${profile.intervalMs / 1000}s`
      : `every ${profile.intervalMs}ms`);
  }
  if (profile.maxFrames !== undefined) {
    parts.push(`max ${profile.maxFrames} frames`);
  }
  if (profile.durationMs !== undefined) {
    const seconds = profile.durationMs / 1000;
    parts.push(seconds >= 60 ? `for ${seconds / 60}min` : `for ${seconds}s`);
  }
  const flags: string[] = [];
  if (profile.deltaHighlight) flags.push("delta");
  if (profile.compressIdle) flags.push("idle-compressed");
  if (profile.gifExport) flags.push("gif");
  if (flags.length > 0) parts.push(flags.join(", "));
  return parts.join(", ") || "no timing params set";
}
```

### Pattern 5: Timing Profile Resolution
**What:** Pure function merging timing profile values with inline start_capture params.

```typescript
// Source: Follows resolveScreenshotProfile pattern in src/profiles/profile-resolver.ts
export interface ResolvedTimingParams {
  interval_ms?: number;
  max_frames?: number;
  duration_ms?: number;
  jpeg_quality?: number;
  delta_highlight?: boolean;
  compress_idle?: boolean;
  gif_export?: boolean;
}

export function resolveTimingProfile(
  profile: TimingProfile,
  inlineParams: Partial<ResolvedTimingParams>,
): ResolvedTimingParams {
  return {
    interval_ms: inlineParams.interval_ms ?? profile.intervalMs,
    max_frames: inlineParams.max_frames ?? profile.maxFrames,
    duration_ms: inlineParams.duration_ms ?? profile.durationMs,
    jpeg_quality: inlineParams.jpeg_quality ?? profile.jpegQuality,
    delta_highlight: inlineParams.delta_highlight ?? profile.deltaHighlight,
    compress_idle: inlineParams.compress_idle ?? profile.compressIdle,
    gif_export: inlineParams.gif_export ?? profile.gifExport,
  };
}
```

**Combined resolution (D-11):** When both `screenshot_profile` and `timing_profile` are provided:
1. Load timing profile -> apply as base for timing/diagnostic params
2. Load screenshot profile -> apply as base for target params
3. Inline params override everything

The order between screenshot and timing profiles doesn't conflict because they cover non-overlapping parameter spaces (target vs. timing).

### Anti-Patterns to Avoid
- **Duplicating file I/O:** Don't create a second file reader. Extend ProfileManager to handle both profile types in the same load/persist cycle.
- **Validating ranges in the profile type:** Profile storage should accept any valid numbers. Range validation belongs in the zod schema at the tool boundary (D-09).
- **Persisting built-in presets:** Built-ins are hardcoded constants. Never write them to the JSON file. Only list them in output.
- **Treating boolean defaults as required:** `delta_highlight`, `compress_idle`, `gif_export` default to false but are stored as explicit booleans in profiles. Don't omit them -- store the explicit value so profiles are self-documenting.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Schema validation | Manual range checks | Zod schemas with `.min()/.max()` | Consistent with all existing tools [VERIFIED: StartCaptureInputSchema in types.ts] |
| Name slugification | New slug function | Existing `slugify()` from profile-types.ts | Already built and tested in Phase 8 [VERIFIED: src/profiles/profile-types.ts] |
| File persistence | New file I/O module | Existing ProfileManager load/persist | Already handles dual-key JSON structure [VERIFIED: src/profiles/profile-manager.ts] |
| Profile resolution merge | Inline object spread in handler | Pure function like `resolveTimingProfile` | Matches existing `resolveScreenshotProfile` pattern; testable [VERIFIED: src/profiles/profile-resolver.ts] |

**Key insight:** Phase 9 is intentionally parallel to Phase 8. Reuse everything possible from the screenshot profile infrastructure. The novel elements are: built-in presets, parameter_summary generation, and the combined resolution of both profile types in start_capture.

## Common Pitfalls

### Pitfall 1: Nullish Coalescing with Boolean False
**What goes wrong:** `inlineParams.delta_highlight ?? profile.deltaHighlight` correctly handles `undefined` but agents might pass explicit `false` to override a profile's `true`. Nullish coalescing preserves explicit `false` (since `false` is not `null`/`undefined`), so this is correct. But if using `||` instead, explicit `false` would be lost.
**Why it happens:** Easy to accidentally use `||` instead of `??` for boolean fields.
**How to avoid:** Always use `??` in the resolver. Never use `||` for optional boolean merging. This is already correct in `resolveScreenshotProfile` -- follow the same pattern.
**Warning signs:** Agent sets `delta_highlight: false` inline but capture still uses the profile's `true` value.

### Pitfall 2: Built-in Preset Shadowing in Delete
**What goes wrong:** Agent tries to delete a built-in preset. It's not in the user profiles map, so delete returns "not found". But the preset still appears in list output (it's hardcoded).
**Why it happens:** Built-ins can't be deleted.
**How to avoid:** `delete_timing_profile` should check if the name matches a built-in and return a specific error message: "Cannot delete built-in preset. Save a user profile with the same name to override it."
**Warning signs:** Agent sees a profile in list, tries to delete it, gets not-found error.

### Pitfall 3: ProfileManager timingData Upgrade
**What goes wrong:** Phase 8's `ProfileManager` stores `timingData` as `Record<string, unknown>`. Phase 9 needs to upgrade this to a typed `Map<string, TimingProfile>`. If the upgrade doesn't handle existing files that have a populated `timing` key (from manual editing or a Phase 9 partial run), it could lose data.
**Why it happens:** Type mismatch between Phase 8's placeholder and Phase 9's actual structure.
**How to avoid:** Replace `timingData: Record<string, unknown>` with a proper `timingProfiles: Map<string, TimingProfile>` that loads from `data.timing` in the same way screenshots load from `data.screenshot`. The persist method writes both.
**Warning signs:** Timing profiles disappear after a screenshot profile save/delete operation.

### Pitfall 4: Default Values in Partial Profiles
**What goes wrong:** A timing profile with just `interval_ms: 200` is saved. When used with `start_capture`, the agent expects `max_frames` to use `start_capture`'s default (20). But if the resolver returns `undefined` for `max_frames`, it bypasses the zod default.
**Why it happens:** Zod `.default()` only applies when the field is not present in the parsed input. If the resolver explicitly sets `max_frames: undefined`, it might override the default.
**How to avoid:** In the resolver, only include fields that have actual values. Filter out `undefined` entries before spreading into the args object. Or use a merge strategy that skips undefined values.
**Warning signs:** `start_capture` with a partial timing profile fails validation for required fields.

### Pitfall 5: Combined Profile Resolution Order
**What goes wrong:** Both `screenshot_profile` and `timing_profile` are specified. The screenshot profile resolver runs and produces a full args object. Then the timing profile resolver runs and overwrites timing fields -- but the screenshot resolver already set them to `start_capture` defaults (not `undefined`).
**Why it happens:** Resolution order matters. If screenshot resolution is applied first and fills in defaults, timing profile values get masked.
**How to avoid:** Apply timing profile first (for timing params only), then screenshot profile (for target params only), then inline overrides. Since the two profiles cover non-overlapping param spaces, they don't conflict -- but only if each resolver touches only its own domain.
**Warning signs:** Timing profile values are ignored when both profiles are specified.

## Code Examples

### SaveTimingProfileInput
```typescript
// Parallels SaveProfileInput from profile-manager.ts
export interface SaveTimingProfileInput {
  name: string;
  intervalMs?: number;
  maxFrames?: number;
  durationMs?: number;
  jpegQuality?: number;
  deltaHighlight?: boolean;
  compressIdle?: boolean;
  gifExport?: boolean;
  description?: string;
}
```

### save_timing_profile Tool Registration
```typescript
// Source: Follows save_screenshot_profile pattern in server.ts line 672
server.registerTool(
  "save_timing_profile",
  {
    title: "Save Timing Profile",
    description: "Save or update a named timing profile with capture timing parameters and diagnostic flags.",
    inputSchema: {
      name: z.string().min(1).describe("Profile name (case-insensitive, slugified for storage)"),
      interval_ms: z.number().min(100).optional().describe("Milliseconds between captures"),
      max_frames: z.number().min(1).max(50).optional().describe("Maximum frames to capture"),
      duration_ms: z.number().min(100).optional().describe("Total capture duration in ms"),
      jpeg_quality: z.number().min(1).max(100).optional().describe("JPEG quality (1-100)"),
      delta_highlight: z.boolean().optional().describe("Enable delta highlighting"),
      compress_idle: z.boolean().optional().describe("Collapse identical frames"),
      gif_export: z.boolean().optional().describe("Export as animated GIF"),
      description: z.string().optional().describe("Human-readable description"),
      source_session: z.string().uuid().optional().describe("Copy timing config from this session"),
    },
  },
  async (args) => { /* handler */ },
);
```

### start_capture Timing Profile Integration
```typescript
// Add to start_capture inputSchema (alongside existing screenshot_profile):
timing_profile: z
  .string()
  .optional()
  .describe("Named timing profile to use as base timing/diagnostic config. Inline params override."),

// In handler, after screenshot profile resolution:
if (args.timing_profile) {
  const profileManager = getProfileManager();
  const slug = slugify(args.timing_profile);
  // Check user profiles first, then built-ins (D-14 shadow semantics)
  let timingProfile = await profileManager.getTiming(slug);
  if (!timingProfile) {
    timingProfile = BUILTIN_TIMING_PRESETS[slug];
  }
  if (!timingProfile) {
    // Structured error with available names (D-12)
    const userProfiles = await profileManager.listTiming();
    const builtinNames = Object.keys(BUILTIN_TIMING_PRESETS);
    return errorResponse(slug, [...userProfiles.map(p => p.slug), ...builtinNames]);
  }
  const resolved = resolveTimingProfile(timingProfile, {
    interval_ms: args.interval_ms,
    max_frames: args.max_frames,
    // ... etc
  });
  resolvedArgs = { ...resolvedArgs, ...stripUndefined(resolved) };
}
```

### File Format After Phase 9
```json
{
  "screenshot": {
    "vs-code-editor": {
      "slug": "vs-code-editor",
      "displayName": "VS Code Editor",
      "target": "window",
      "windowTitle": "Visual Studio Code",
      "createdAt": "2026-04-13T10:00:00.000Z",
      "updatedAt": "2026-04-13T10:00:00.000Z"
    }
  },
  "timing": {
    "fast-scan": {
      "slug": "fast-scan",
      "displayName": "Fast Scan",
      "intervalMs": 200,
      "maxFrames": 30,
      "deltaHighlight": true,
      "description": "Rapid capture for UI debugging",
      "createdAt": "2026-04-13T11:00:00.000Z",
      "updatedAt": "2026-04-13T11:00:00.000Z"
    }
  }
}
```

## State of the Art

No library changes relevant. All components stable.

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Phase 8 `timingData: Record<string, unknown>` | Typed `Map<string, TimingProfile>` | Phase 9 | ProfileManager upgrade -- replace placeholder with real timing profile support |

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | `stripUndefined()` utility needed to avoid overriding zod defaults with undefined | Common Pitfalls (Pitfall 4) | MEDIUM -- could cause validation failures or unexpected defaults |
| A2 | Built-in preset timestamps use a fixed date (not dynamically generated) | Architecture Patterns (Pattern 3) | LOW -- cosmetic only, timestamps on builtins are informational |

## Open Questions

1. **Should `save_timing_profile` with zero timing params be allowed?**
   - What we know: D-06 explicitly says yes -- "A profile with zero timing parameters is valid (description-only or flags-only profile)."
   - Resolution: Allow it. A profile with just `description: "placeholder"` is valid. The zod schema makes all timing fields optional.

2. **Should `list_timing_profiles` show builtins first or user profiles first?**
   - What we know: Discretion item. No locked decision.
   - Recommendation: Show builtins first (they're reference points), then user profiles alphabetically. Agent can distinguish via `builtin` field.

3. **How to handle `parameter_summary` for zero-param profiles?**
   - Recommendation: Return "no timing params set" or just the description. Edge case but valid per D-06.

## Project Constraints (from CLAUDE.md)

- **Platform:** Windows 11 primary target
- **Protocol:** MCP server spec (tools + resources over stdio)
- **Logging:** All output to stderr only (logger pattern)
- **Dependencies:** Use existing stack only (no new npm packages)
- **Naming:** Snake_case for MCP tool params, camelCase for internal TypeScript

## Sources

### Primary (HIGH confidence)
- `src/profiles/profile-manager.ts` -- Existing ProfileManager class with timing data placeholder [VERIFIED: line 32, `timingData` field]
- `src/profiles/profile-types.ts` -- ScreenshotProfile interface, slugify utility, ProfilesFileData [VERIFIED: complete file read]
- `src/profiles/profile-resolver.ts` -- resolveScreenshotProfile merge pattern [VERIFIED: complete file read]
- `src/types.ts` -- CaptureConfig with timing fields and StartCaptureInputSchema validation ranges [VERIFIED: complete file read]
- `src/server.ts` -- Tool registration pattern, screenshot_profile integration in start_capture [VERIFIED: lines 152-198]
- `09-CONTEXT.md` -- All 17 locked decisions [VERIFIED: complete file read]

### Secondary (MEDIUM confidence)
- `08-RESEARCH.md` -- Phase 8 architecture decisions and patterns that Phase 9 mirrors [VERIFIED: complete file read]
- `08-01-PLAN.md` -- Phase 8 plan structure for reference [VERIFIED: complete file read]

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH -- no new libraries, reuses Phase 8 infrastructure entirely
- Architecture: HIGH -- mirrors established Phase 8 patterns with well-defined extensions
- Pitfalls: HIGH -- boolean merge and default handling are well-understood patterns; built-in preset edge cases are straightforward

**Research date:** 2026-04-13
**Valid until:** 2026-05-13 (stable domain, no moving parts)
