Skip to content

[P3][bug] Orphaned live-contract watch still active on the shared test key since 2026-09-04; smoke cleanup is skipped whenever id extraction fails #126

Description

@karlwaldman

Confirmed live against https://api.oilpriceapi.com on 2026-09-13 with the shared test key (read-only GET /v1/subscriptions), and in code on origin/main.

What is live

GET /v1/subscriptions on the shared test key returns one watch nobody owns:

{"id":"b84b24a0-2b28-4eac-835e-db92bab5c0cb","name":"mcp-live-contract-1788524538194","codes":["BRENT_CRUDE_USD"],"interval_seconds":3600,"status":"active","source":"api","tool_name":"opa_create_price_subscription","last_evaluated_at":"2026-09-13T19:32:06Z","next_run_at":"2026-09-13T20:32:06Z","created_at":"2026-09-04T12:22:19Z"}

It has been snapshotted hourly for nine days (a WatchEvent per interval via WatchSnapshotWorker). From the API code, watch evaluation does not consume request quota, so the cost is event rows and noise in any events poll on this key, and it occupies one watch slot on whatever tier the key is on.

Why it leaked (from git history)

  • 36e8261 (Cover every MCP tool with a generated live contract matrix #80, merged 2026-09-04) added subscriptionLifecycle() in scripts/live-smoke.mjs reading id = created.body?.subscription?.id. Production nests the record under data, so id was undefined, the assert(id, ...) threw, and the finally guard if (id) await deleteAndVerify(...) skipped cleanup for a watch the API had created.
  • b81f8e9 (Reconcile MCP handlers with live production envelopes #82) changed the read to created.body?.subscription?.id ?? created.body?.data?.subscription?.id at 08:26 -04:00 (12:26Z), four minutes after this watch was created (12:22:19Z).

The id read is fixed. The structural flaw is not: cleanup still depends on parsing the create response. If the envelope moves again, or the assertion after create fails before id is set, the next run leaks another watch the same way.

Suggested fix

  1. Delete b84b24a0-2b28-4eac-835e-db92bab5c0cb from the test account (not done here: it is not a resource this session created).
  2. In finally, when id is unset but create returned 2xx, fall back to listing /v1/subscriptions and deleting records whose name equals the generated mcp-live-contract-<ts>. Same for the price-alert lifecycle.
  3. Optionally sweep mcp-live-contract-* records older than an hour at the start of each run, so any past leak self-heals.

Found while writing the Node SDK subscriptions lifecycle smoke (OilpriceAPI/oilpriceapi-node#78).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions