Skip to content

OptionsWatcher can lose a config write that lands milliseconds after the previous one #1747

Description

@kriszyp

Summary

Two set_component_file writes to a component's config.yaml arriving in quick succession (observed: 9ms apart) can result in the second write never being observed by the component's OptionsWatcher — the live config keeps the first write's values until some later event touches the file.

Evidence

Observed while developing an integration test for the static plugin (branch kris/static-cache-headers): an alternating pass/fail pattern where every config update issued immediately after a fast-succeeding previous update was lost. Harper log excerpt showing the paired writes:

2026-07-09T23:48:09.802Z [main/0] [info] [operation]: { operation: 'set_component_file', project: 'static-cache-headers', file: 'config.yaml' }
2026-07-09T23:48:09.811Z [main/0] [info] [operation]: { operation: 'set_component_file', project: 'static-cache-headers', file: 'config.yaml' }

The first write was applied (served headers changed); the second never was, across a 15s polling window.

Analysis

components/OptionsWatcher.ts#handleChange itself is sound — every chokidar event triggers a fresh readFile, so last-write-wins on contents. The loss is at the chokidar/inotify layer: a write landing while the watch is being re-established (e.g. after an atomic replace/rename from the prior write) emits no event, and nothing self-heals afterward because no later event arrives.

Impact

Low-frequency but user-visible: two quick config saves (studio, deploy tooling, scripts) can leave the component running the older config with no error. Same seam presumably affects any watched config, not just static.

Suggested direction

  • Debounced re-read after the last observed event (cheap self-heal), or a slow periodic mtime check as a safety net, in OptionsWatcher.
  • Alternatively have set_component_file nudge the watcher (it knows the write happened).

— found while testing #1746's static-plugin follow-up; reported by Claude (Opus 4.8) with Kris

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions