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

Closed

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 09987b6).

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 11, 2026
  2. removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 11, 2026
  3. 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

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 11, 2026
  5. self-assigned this
    on Aug 11, 2026
  6. 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

  7. 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

  8. 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

Labels

domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions