Skip to content

finding: dev metadata reload does not re-ingest connector items — reconcileDeclaredConnectors reads a registry the artifact reload never refreshes for connectors #7742

Description

@huangyiirene

Symptom

Shared unproven half behind three PARTIALs (connector-declarative-boot, connector-degraded-recovery, connector-spec-path-no-escape): a connector edit on disk fires a metadata reload that does not reconcile the change — no reconcile, no teardown, no re-materialize. Not scored a fail, because the default os dev restarts the serve child on recompile so the user-visible loop still applies the change; the production trigger (a Studio package publish) was not drivable in the run. Flagged for triage.

Root cause

reconcileDeclaredConnectors (packages/services/service-automation/src/plugin.ts, ~line 1059) reads ql.registry.listItems('connector'), but the artifact reload re-ingests object definitions and not connector items — so on a connector-only edit the registry the reconcile reads is stale and the reconcile is a no-op. The connector materialize/teardown never re-runs off the reload event.

Stale-premise check: present on origin/main (reconcileDeclaredConnectors still reads listItems('connector'), and the reload path does not re-ingest connectors).

Reproduction

  1. os dev a stack with a declared connector, but drive a metadata reload without a serve-child restart (or reload via a Studio package publish).
  2. Edit the connector definition on disk and trigger the reload.

Expected: the connector is torn down and re-materialized from the new definition. Actual: the reload re-ingests objects only; the connector reconcile is a no-op and the old connector stays live.

Source

Extracted from the QA run #7690 (framework 92f26f7, console 09987b680).

Activity

  1. claude commented on Aug 11, 2026

    @claude
    Contributor

    Findings triage (grade): promoted — finding removed, pm:queue stays (resolves the filed dual pm-state).

    Why queue: the mechanism is confirmed on origin/main @ 8f1851e — reconcileDeclaredConnectors (packages/services/service-automation/src/plugin.ts:1059) reads ql?.registry?.listItems?.('connector') (~:1071), and the artifact reload re-ingests object definitions only, so a connector-only edit reconciles against a stale registry (a no-op). The dev loop masks it because the serve child restarts, but the named production trigger (a Studio package publish) walks straight into it. A confirmed no-op reconcile with a named landing is a defect, not an observation.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 11, 2026
  3. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    CollaboratorAuthor

    Claim: PM loop round 2
    Session: session_01JVfWSQN9RDDGwdVSmMc29x
    Branch: claude/issue-7742-connector-reload-reconcile
    Worktree: cloud session's own clone (mode:cloud)
    Domain: domain:services
    File surface: packages/services/service-automation/src/plugin.ts (reconcileDeclaredConnectors ~:1059, registry read ~:1071) and the reload-event wiring it needs. ⚠️ If the true fix lands in the artifact-reload path inside packages/metadata* (making the reload re-ingest connector items), that is domain:metadata territory — STOP-and-report fork in the dispatch, per the cross-seat protocol.
    Container & model: M (reload/reconcile wiring with a cross-lane fork), mode:cloud, model: opus
    Serial constraints cleared: ⚠️ same-package sibling #7747 (notify acted accounting, root cause not yet located) is deferred and serialized behind this card — not dispatched this round. Same-day churn on service-automation (#7699, #7595, #7490 — all merged) touches builtin/ and run surfaces, not reconcileDeclaredConnectors; triage confirmed the mechanism on 8f1851e at 13:11Z. No in-flight PR touches service-automation.


    Generated by Claude Code

  4. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    CollaboratorAuthor
    {
      "issue": 7742,
      "status": "in_progress",
      "premise_still_valid": true,
      "premise_evidence": "Measured on origin/main @ b54aaab, not read off the issue. New test drives the REAL reload shape (a real SchemaRegistry that is never mutated across the reload + MetadataPlugin's actual `{ changed, metadata }` payload) and the reconcile is a total no-op: edited connector -> `closed` [] (expected ['billing']); deleted connector -> `closed` [] (expected ['shipping']); added connector -> not registered. Mechanism confirmed exactly as filed: `reconcileDeclaredConnectors` read `ql.registry.listItems('connector')` (plugin.ts ~:1059/:1071); ObjectQL's own `metadata:reloaded` handler re-ingests the payload's OBJECT definitions (`ingestReloadedObjects`) and nothing re-ingests connector items, so the reconcile compared the boot snapshot against itself. Also measured: every pre-existing reload test (connector-materialization.test.ts) hand-mutates a fake registry (`state.declared = next`) before firing the hook, which is why the path looked covered.",
      "branch": "claude/issue-7742-connector-reload-reconcile",
      "pr": 7847,
      "pr_url": "https://github.com/objectstack-ai/objectstack/pull/7847",
      "pr_state": "draft",
      "cross_lane_fork": {
        "triggered": false,
        "detail": "No metadata-side change was needed and none was made. `packages/metadata*` is untouched. The services-side shape works: the plugin re-reads its OWN input from the two sources a reload refreshes, leaving global reload semantics alone."
      },
      "change_summary": {
        "files": [
          "packages/services/service-automation/src/plugin.ts (+220/-30)",
          "packages/services/service-automation/src/connector-reload-reingest.test.ts (new, 8 tests)",
          ".changeset/connector-reload-reingest.md (patch)"
        ],
        "design": "New `readDeclaredConnectorItems(ctx, { post })` is the single declaration source for BOTH the reconcile and the descriptor audit. It folds two reload-refreshed sources over the existing registry read, one per trigger: (1) the artifact on the `metadata:reloaded` payload — the dev/HMR trigger, and the only place an edited or deleted definition exists — kept as plugin state so a degraded-retry firing later does not fall back to the boot snapshot and rebuild the pre-edit instance; (2) `protocol.getMetaItems({type:'connector'})` — the flattened /meta view the flow re-sync already reads, which layers the `sys_metadata` rows a Studio publish promotes to active over the registry (verified in protocol.ts `mergePackageAwareOverlay`: the overlay wins over the registry entry it shadows). The payload fold is a REPLACEMENT SCOPED to the packages the artifact speaks for (manifest id + `_packageId` on its items), not a union: a union can never observe a deletion, an unscoped replacement would tear down another package's connector.",
        "boot_path_unchanged": "The protocol read is `post`-only (every reconcile that is not the fatal boot one). At boot the registry was just built and is current by construction; a sys_metadata query that can fail does not belong on the fail-loudly boot path.",
        "fail_safe_rules": "Absent/failing protocol read -> no answer, nothing torn down (same contract as the flow re-sync's `null`). Empty served view while the registry HAS entries -> no answer (the view is a superset of the registry by construction). Payload with no `metadata.connectors` ARRAY (a publish's bare `{changed}`, or an artifact with no `connectors:` key) -> previous snapshot untouched; only an artifact carrying an EMPTY array is the honest 'none left'. Unchanged entries still hash to the same signature, so reloads do not churn live connections.",
        "refactor": "`readFlowDefsFromProtocol`'s body is now the shared `readMetaItemsFromProtocol(ctx, type)`; the flow warn message is byte-identical (`[Automation] flow read from protocol failed: getMetaItems('flow')`), so its pin in thrown-cause-diagnostics.test.ts still holds."
      },
      "tests": {
        "new_file": "packages/services/service-automation/src/connector-reload-reingest.test.ts",
        "shape": "Real `SchemaRegistry` (from @objectstack/objectql), deliberately NEVER mutated across the reload — the registry going stale is the fact under test. Assertions are on the reconcile's OBSERVABLE effect (the old instance's `close()`, a re-materialization carrying the new `providerConfig`, the live connector list), never on internal call counts.",
        "cases": [
          "edited connector on a dev artifact reload -> torn down + re-materialized from the EDITED definition",
          "unedited connector -> left alone (no reconnect churn)",
          "connector deleted from the reloaded artifact -> torn down",
          "connector added by the reloaded artifact -> materialized",
          "another package's connector -> survives an unrelated app's reload",
          "Studio publish (no artifact on the payload) -> re-materializes off the published /meta view while the registry stays stale",
          "empty or throwing published view -> never tears down live connectors",
          "payload with no connector collection / no payload at all -> changes nothing"
        ],
        "reverse_verification": "PASSED. With plugin.ts reverted to origin/main and the tests kept, 4 failed / 3 passed: the edit, delete, add and publish cases fail exactly the way the QA run observed (no-op reconcile — `closed` empty, nothing re-materialized), while the never-tear-down guards pass on both sides (they must)."
      },
      "gates": {
        "service_automation_suite": "PASS — 928 tests / 76 files",
        "build_closure": "PASS — turbo build --filter @objectstack/service-automation... 20/20",
        "typecheck": "PASS — tsc --noEmit: no errors in touched files (3 pre-existing errors in the untouched nested-region-parity.test.ts, present on origin/main)",
        "check_docs_audit_scope": "PASS",
        "downstream_suites": "PASS — connector-mcp 23, connector-rest 16 (the two packages that boot AutomationServicePlugin)",
        "changeset": "PRESENT — .changeset/connector-reload-reingest.md (patch, @objectstack/service-automation)",
        "engine_double_gate": "N/A — the new fakes are a registry and a protocol read surface; no test fake with update/delete verbs was added, so no assertEngineUpdate/DeleteDispatch routing is owed.",
        "lint_farm": "NOT RUN locally, per dispatch."
      },
      "card_clauses": {
        "notify_acted_untouched": true,
        "worked_from_origin_main_not_card_line_numbers": true,
        "worktree": "/home/user/objectstack-7742 (dedicated per-task worktree; primary checkout returned to main)"
      },
      "flakes_observed": [
        "engine.test.ts > 'should execute unconditional branches in parallel' failed once in one full-suite run, passed in isolation and on the next two full runs (928/928). A wall-clock parallelism assertion, untouched by this diff — reporting it rather than hiding it."
      ],
      "open_questions": [
        "Deletion detection on the DEV path needs the artifact's package coordinate. It comes from `metadata.manifest.id` / `metadata.id` or the `_packageId` MetadataPlugin stamps on the items; when an artifact carries neither, the fold degrades to name-wise replacement — edits and additions still land, a DELETED connector waits for the restart it always waited for. Chosen deliberately over guessing a wider removal scope, which would tear down other packages' connectors. Flagging it as a known partial rather than a silent cap.",
        "The Studio-publish half is verified through my own read seam (a protocol service serving the published view) plus a code read of protocol.ts's overlay precedence — NOT through a live publish against a real sys_metadata. A full integration test would need a real engine + storage + publishPackageDrafts; out of proportion for this card, but it means the publish end is argued, not measured end-to-end. Say the word if the PM wants that integration test added before ready-flip.",
        "`auditDeclaredConnectors` now reads the same fresh declaration (it was equally stale). That was in-scope coherence, not creep — but it does mean the kernel:ready audit now performs one protocol read where it previously read the registry only."
      ],
      "next": "PM owns CI convergence, the ready-flip and landing. Not idle-polling CI."
    }

    Generated by Claude Code

  5. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    CollaboratorAuthor

    ACCEPT — PR #7847 (services seat review, session session_01JVfWSQN9RDDGwdVSmMc29x).

    What shipped: the reconcile's INPUT is now fresh. readDeclaredConnectorItems folds two reload-refreshed sources over the boot registry read — the metadata:reloaded payload's artifact (dev/HMR trigger, scoped replacement by the packages the artifact owns) and protocol.getMetaItems({type:'connector'}) (the /meta overlay view a Studio publish promotes, post-boot only) — so both named triggers now reconcile against the current world. The descriptor audit reads the same declaration (in-scope coherence). Fail-safe rules are the right shape: absent/failing/suspect-empty answers never tear down a live connector; only an artifact carrying an explicit empty array is "none left".

    Review notes:

    Marking ready and arming auto-merge now. #7747 (same package) unblocks for round 3 once this lands.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions