ops(registry): backfill stale member_profiles.agents types + crawler reclassify-on-disagreement#3541
Merged
EmmaLouise2018 merged 2 commits intoApr 30, 2026
Conversation
…reclassify-on-disagreement Refs #3538. PR #3498 added `resolveAgentTypes()` server-side, but it only runs on writes (POST/PUT to /api/me/member-profile). Rows saved before #3498 never get re-evaluated. The crawler's type-update path at `crawler.ts:580` only wrote back when the stored type was missing — once any non-unknown value was set, the row was frozen. This is the cleanup for Problem 1 in #3538. ## Crawler type-update policy (crawler.ts) Old: write back only when no stored type and inferred is non-unknown. New: - Promote when stored is missing OR stored is 'unknown' AND inferred is non-unknown. Same intent as before, broadened to cover the 'unknown' case that was previously frozen. - Log a warning on disagreement (stored non-unknown != inferred non-unknown). Do NOT auto-flip — single probes can be wrong; auto-flipping would corrupt good rows on a transient bad probe. Operator runs the backfill explicitly to reconcile. ## Backfill script (server/scripts/backfill-member-agent-types.ts) Walks every `member_profiles` row, calls `resolveAgentTypes()` on its `agents[]`, writes back any agent whose stored type disagrees with the snapshot's inferred type. Idempotent. Has a `--dry-run` mode. ``` npx tsx server/scripts/backfill-member-agent-types.ts --dry-run npx tsx server/scripts/backfill-member-agent-types.ts ``` ## Export `resolveAgentTypes` is now exported from `member-profiles.ts` so the script can reuse it. The backfill is the same logic as the write path; pushing the abstraction up rather than duplicating it. ## Test plan - New: `server/tests/unit/crawler-type-update-policy.test.ts` — pins the promote/disagreement matrix. 5/5 pass. - `npx tsc --noEmit -p server/tsconfig.json` — clean. ## Operator note Run `--dry-run` first on staging to see the diff, then again on prod. Bidcliq and Swivel ('buying' but actually sales) are the known cases.
This was referenced Apr 29, 2026
Merged
Merged
Contributor
|
Issue #3550 proposes a Generated by Claude Code |
EmmaLouise2018
added a commit
that referenced
this pull request
Apr 30, 2026
#3541 + 457_agent_verification_badges_per_version both landed on main while this PR was open, so 457 is now occupied. Migration runner explicitly errors on duplicate version numbers (server/src/db/migrate.ts:80) — would crash on next deploy. Renames the migration file and the doc cross-reference in the helper module's header comment. No test/code references the version number, so no other call sites change.
EmmaLouise2018
added a commit
that referenced
this pull request
Apr 30, 2026
…r-agent timing #3541 has merged, so resolveAgentTypes is statically imported from member-profiles.ts (the dynamic-import workaround is gone). Adds a load-bearing silent-corruption guard: a transient probe failure on an agent that already had a snapshot row never overwrites that row back to NULL — decideWrite() routes probe_failed/dns_failed to "preserve". Adds per-agent elapsed_ms + slowest-N report so operators see the slow tail. Fails loud on missing DATABASE_URL. Test coverage: success / probe-failed (timeout) / DNS-failed / discovery_error routing / silent-corruption guard / dry-run / per-agent timing — 18/18 pass.
bokelley
pushed a commit
that referenced
this pull request
May 1, 2026
…e transitions (closes #3550) (#3567) * feat(registry): add type_reclassification_log migration Append-only audit trail for agent type transitions. Captures every flip from the three writer paths (backfill_script, crawler_promote, member_write) so future audits answer with a row, not a stdout-grep. No FK to agents — historical record survives agent deletion. Refs #3550, #3538, #3541. * feat(registry): add insertTypeReclassification DB helper Single-call insert helper for the type_reclassification_log table. Idempotent only at the row level — deduplication is the caller's responsibility. On insert failure we log and swallow: the audit log is observability, not a write barrier, and a failed log insert must not roll back the caller's primary intent. Refs #3550. * feat(registry): crawler disagreement path writes audit log row When the type-update policy decides 'disagreement' (stored non-unknown differs from inferred non-unknown), also write a type_reclassification_log row with source='crawler_promote' and notes={decision: 'logged_only_no_promote'}. The crawler still does not auto-flip — the disagreement event itself is what the audit log captures. Operator runs the backfill to flip explicitly. See #3538. Refs #3550. * feat(registry): member-write resolveAgentTypes flips emit audit log rows After resolveAgentTypes runs at any of the three call sites (POST create, PUT bulk-update, admin update), diff against the pre-resolve agent array and write a type_reclassification_log row per flipped agent. source is 'member_write', member_id is the workos_organization_id (or profile id on the admin path). Pulls the diff/log logic into a new exported helper (logResolvedTypeChanges) so the three sites stay tight. Refs #3550. * feat(registry): backfill script writes audit log rows in real mode Every real-mode flip from backfill-member-agent-types.ts now writes a type_reclassification_log row with source='backfill_script' and a generated run_id (backfill-<unix-ms>). Dry-run skips the audit log — no writes, no audit rows. run_id is also echoed in the script's summary so an operator can answer "what did the 2026-04-29 backfill change?" with a single SELECT. Refs #3550. * test(registry): pin type_reclassification_log helper + crawler audit hook - type-reclassification-log-db.test.ts: 6 tests covering canonical insert shape, null-padding for omitted optionals, JSONB serialization, explicit null oldType (first-classification), error swallow (audit log must never block caller), and per-source acceptance. - crawler-type-update-policy.test.ts: extends with 3 tests pinning that the disagreement branch writes source='crawler_promote' to the audit log, while the agree/promote branches do not. Plus changeset. Refs #3550. * docs(registry): pin resolveAgentTypes returns-new-array invariant in docstring Pre-review nit: logResolvedTypeChanges captures `before` arrays by reference at three call sites and diffs against the resolved array. Whole audit-log diff depends on resolveAgentTypes returning a new array, never mutating in place. Pinning the contract in the docstring so a future refactor that switches to in-place mutation surfaces the constraint at the source rather than silently zeroing out audit log entries. Closes #3550. * fix(registry): renumber type_reclassification_log migration 457 -> 459 #3541 + 457_agent_verification_badges_per_version both landed on main while this PR was open, so 457 is now occupied. Migration runner explicitly errors on duplicate version numbers (server/src/db/migrate.ts:80) — would crash on next deploy. Renames the migration file and the doc cross-reference in the helper module's header comment. No test/code references the version number, so no other call sites change.
EmmaLouise2018
added a commit
that referenced
this pull request
May 1, 2026
…eout Closes #3551 (Problem 2 quick-win for #3538). 77% of agents in the public registry currently render type='unknown'. Before designing the full retry-with-backoff system, ship a one-shot script that re-probes every currently-unknown agent with an extended 30s timeout. The output tells us whether unknown is mostly transient or mostly dead — informs the full Problem 2 PR's scope. ## Script server/scripts/reprobe-unknown-agents.ts: - Selects every agent with snapshot.inferred_type='unknown' or missing snapshot - Re-probes with 30s timeout (vs current 10s in crawler.ts:548) - Reuses the crawler's probe + write helpers — no bypass - Calls resolveAgentTypes from member-profiles.ts (#3541) so member_profiles.agents[] picks up new types - Reports {still_unknown, newly_classified by type, probe_failed, dns_failed} - Idempotent. --dry-run mode for staging validation before real runs. ## Test server/tests/unit/reprobe-unknown-agents.test.ts pins the report-shape contract across success/failure/dns-fail cases. ## Operator note Same protocol as the #3541 backfill: dry-run on staging, capture stdout, eyeball the diff, then real run. Repeat on prod. Operator paste-back of the final report into a PR comment so the full Problem 2 PR can scope from real data. ## Out of scope - Full retry-with-backoff loop with last_probe_attempt_at tracking - Mark-as-dead semantics for permanently unreachable agents Both depend on this script's output to scope correctly.
EmmaLouise2018
added a commit
that referenced
this pull request
May 1, 2026
…r-agent timing #3541 has merged, so resolveAgentTypes is statically imported from member-profiles.ts (the dynamic-import workaround is gone). Adds a load-bearing silent-corruption guard: a transient probe failure on an agent that already had a snapshot row never overwrites that row back to NULL — decideWrite() routes probe_failed/dns_failed to "preserve". Adds per-agent elapsed_ms + slowest-N report so operators see the slow tail. Fails loud on missing DATABASE_URL. Test coverage: success / probe-failed (timeout) / DNS-failed / discovery_error routing / silent-corruption guard / dry-run / per-agent timing — 18/18 pass.
EmmaLouise2018
added a commit
that referenced
this pull request
May 1, 2026
…eout (#3558) * feat(registry): one-shot re-probe of unknown agents with extended timeout Closes #3551 (Problem 2 quick-win for #3538). 77% of agents in the public registry currently render type='unknown'. Before designing the full retry-with-backoff system, ship a one-shot script that re-probes every currently-unknown agent with an extended 30s timeout. The output tells us whether unknown is mostly transient or mostly dead — informs the full Problem 2 PR's scope. ## Script server/scripts/reprobe-unknown-agents.ts: - Selects every agent with snapshot.inferred_type='unknown' or missing snapshot - Re-probes with 30s timeout (vs current 10s in crawler.ts:548) - Reuses the crawler's probe + write helpers — no bypass - Calls resolveAgentTypes from member-profiles.ts (#3541) so member_profiles.agents[] picks up new types - Reports {still_unknown, newly_classified by type, probe_failed, dns_failed} - Idempotent. --dry-run mode for staging validation before real runs. ## Test server/tests/unit/reprobe-unknown-agents.test.ts pins the report-shape contract across success/failure/dns-fail cases. ## Operator note Same protocol as the #3541 backfill: dry-run on staging, capture stdout, eyeball the diff, then real run. Repeat on prod. Operator paste-back of the final report into a PR comment so the full Problem 2 PR can scope from real data. ## Out of scope - Full retry-with-backoff loop with last_probe_attempt_at tracking - Mark-as-dead semantics for permanently unreachable agents Both depend on this script's output to scope correctly. * fix(registry): reprobe — static import + silent-corruption guard + per-agent timing #3541 has merged, so resolveAgentTypes is statically imported from member-profiles.ts (the dynamic-import workaround is gone). Adds a load-bearing silent-corruption guard: a transient probe failure on an agent that already had a snapshot row never overwrites that row back to NULL — decideWrite() routes probe_failed/dns_failed to "preserve". Adds per-agent elapsed_ms + slowest-N report so operators see the slow tail. Fails loud on missing DATABASE_URL. Test coverage: success / probe-failed (timeout) / DNS-failed / discovery_error routing / silent-corruption guard / dry-run / per-agent timing — 18/18 pass. * fix(test): vi.mock member-profiles to bypass WorkOS init in reprobe test Previous shim was a no-op due to ESM import hoisting — imports run before module-body statements, so member-profiles.ts (transitively imported via the script's static import of resolveAgentTypes) constructed WorkOS before WORKOS_API_KEY got set. Passed locally where the env was already set; failed in CI where it wasn't. Mock member-profiles directly so WorkOS init never triggers. The script's behavior around resolveAgentTypes is exercised through the mock — unit tests pin the behavior, not the implementation. 18/18 reprobe tests pass.
This was referenced May 1, 2026
EmmaLouise2018
added a commit
that referenced
this pull request
May 1, 2026
Changesets are append-only history. The previous commit on this branch deleted this file when removing the script — that's wrong. The original changeset describes what landed in #3541 and stays as historical record; the new migration changeset (backfill-member-agent-types-migration.md) describes the follow-up.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #3538.
PR #3498 added
resolveAgentTypes()server-side, but it only runs on writes (POST/PUT to/api/me/member-profile). Rows saved before #3498 never get re-evaluated. The crawler's type-update path atcrawler.ts:580only wrote back when the stored type was missing — once any non-unknown value was set, the row was frozen, so Bidcliq and Swivel (registered as'buying'while being sales agents) cannot self-correct.This is the cleanup for Problem 1 in #3538.
Crawler type-update policy (
server/src/crawler.ts)Old: write back only when no stored type and inferred is non-unknown.
New:
'unknown'AND inferred is non-unknown. Same intent as before, broadened to cover the'unknown'case that was previously frozen.Backfill script (
server/scripts/backfill-member-agent-types.ts)Walks every
member_profilesrow, callsresolveAgentTypes()on itsagents[], writes back any agent whose stored type disagrees with the snapshot's inferred type. Idempotent. Has a--dry-runmode.Operator runbook
Owner: member-tools / registry oncall. Runs from a workstation with
DATABASE_URLset to the target environment (staging first, then prod). The script is read/write — same machine that runs migrations.Expected diff size today:
'buying'→'sales'. Both are member-registered, both haveagent_capabilities_snapshotrows that infersales. Two flips minimum.Where output goes:
2026-04-29-staging-backfill.log).Procedure:
Pre-run checklist:
agent_url, no> 25flips, every flip explainable).Export
resolveAgentTypesis now exported fromserver/src/routes/member-profiles.tsso the script can reuse it. Pushing the abstraction up rather than duplicating it across the script + the write path.Test plan
server/tests/unit/crawler-type-update-policy.test.ts— pins the promote/disagreement matrix. 5/5 pass.npx tsc --noEmit -p server/tsconfig.json— clean.--dry-runon staging per the runbook above.Stack ordering
Recommended merge order from #3538: 3540 → 3542 → 3543 → 3541. Backfill ships last so it's the most-deliberate step — runs against the wire-corrected codebase from #3540 and the docs-explained surface from #3542 / #3543.
Out of scope
unknownagents (Problem 2 quick-win).registering-an-agent.mdxpage — docs(registry): registering-an-agent — four crawl paths + AAO-membership value story #3543.