@agent-relay/factory 0.1.76 → 0.1.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/orchestrator/factory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/orchestrator/factory.ts"],"names":[],"mappings":"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"}
@@ -480,6 +480,10 @@ export class FactoryLoop {
480
480
  // owner/repo#number identity. GitHub-native records outrank Linear mirrors.
481
481
  #dependencyIssues = new Map();
482
482
  #terminalDependencyIdentities = new Set();
483
+ /** Sweep-scoped memo of `#dependencyIsTerminalOrMerged`'s mount walk, including
484
+ * the negative answer that `#terminalDependencyIdentities` cannot hold. Cleared
485
+ * with it at the top of every sweep. */
486
+ #dependencyPrProbes = new Map();
483
487
  #dependencyParkNotices = new Map();
484
488
  #dependencyGithubPathsByIdentity;
485
489
  #dependencyLinearTreeLoaded = false;
@@ -833,7 +837,8 @@ export class FactoryLoop {
833
837
  this.#customProbePrResolver = Boolean(ports.probePrResolver);
834
838
  this.#hasProbePrGhRunner = Boolean(ports.probePrGhRunner);
835
839
  this.#probePrGhRunner = ports.probePrGhRunner ?? failClosedGhRunner;
836
- this.#probePrResolver = ports.probePrResolver ?? ((issue) => this.#resolveIssuePr(issue));
840
+ this.#probePrResolver = ports.probePrResolver ??
841
+ ((issue) => this.#resolveIssuePr(issue, { repo: this.#probeRepoForIssue(issue) }));
837
842
  this.#logger = normalizeLogger(ports.logger ?? console);
838
843
  this.#clock = ports.clock ?? realClock;
839
844
  this.#fleetControlPlane = new FleetControlPlaneCircuit({
@@ -2355,7 +2360,11 @@ export class FactoryLoop {
2355
2360
  }
2356
2361
  if (pr.draft) {
2357
2362
  this.#increment('completionSweepDraftPr');
2358
- this.#probePrGhBackoffUntilMs.set(issueStateKey(issueRef(issue)), this.#clock.now() + PROBE_PR_GH_BACKOFF_MS);
2363
+ // Must match the key `#completionPrForIssue` resolves under —
2364
+ // same options, same repo scope — or this backoff is written
2365
+ // under a name nothing reads and the draft PR is re-fetched from
2366
+ // gh on every pass.
2367
+ this.#probePrGhBackoffUntilMs.set(this.#probePrCacheKey(issue, { repo: this.#probeRepoForIssue(issue) }), this.#clock.now() + PROBE_PR_GH_BACKOFF_MS);
2359
2368
  return undefined;
2360
2369
  }
2361
2370
  if (record.decision.implementers.length > 1 && !await this.#allImplementersHaveCompletionPr(record)) {
@@ -2596,6 +2605,7 @@ export class FactoryLoop {
2596
2605
  }
2597
2606
  return this.#resolveIssuePr(issue, {
2598
2607
  titleMarker: FACTORY_E2E_MARKER,
2608
+ repo: this.#probeRepoForIssue(issue),
2599
2609
  });
2600
2610
  }
2601
2611
  async #openPrForIssue(issue) {
@@ -2605,18 +2615,82 @@ export class FactoryLoop {
2605
2615
  return this.#resolveIssuePr(issue, {
2606
2616
  titleMarker: FACTORY_E2E_MARKER,
2607
2617
  openOnly: true,
2618
+ repo: this.#probeRepoForIssue(issue),
2608
2619
  });
2609
2620
  }
2610
- async #resolveIssuePr(issue, opts = {}) {
2621
+ /**
2622
+ * Which repository a probe walk may scope itself to.
2623
+ *
2624
+ * `resolveIssuePrFromMount` has always accepted `opts.repo`, but
2625
+ * `#resolveIssuePr` had no way to express one, so every probe crawled the PR
2626
+ * tree of EVERY configured repository — 21 of them in the live workspace — to
2627
+ * find a PR that can only ever live in one. This is the same routing answer
2628
+ * `#dependencyIsTerminalOrMerged` already uses for its own probe, so the two
2629
+ * probes now scope identically.
2630
+ *
2631
+ * `undefined` means the routing is genuinely ambiguous (a decision spanning
2632
+ * several routes, an issue whose labels match no single repo). The walk then
2633
+ * stays unscoped, exactly as before: narrowing on a guess would silently fail
2634
+ * to find a PR that is really there, which is worse than a slow walk.
2635
+ *
2636
+ * `allowDefault: false` is the whole point of this wrapper. Routing precedence
2637
+ * is byLabel, byProject, keywordRules, default, and `dependencyRepoForIssue`
2638
+ * can see neither the triage decision nor `keywordRules`. Left to its own
2639
+ * fallback it answers `repos.default` for every keyword-routed issue, while
2640
+ * dispatch opened that PR in the keyword-selected repository — so the probe
2641
+ * would walk one repository, confidently, and find nothing. Reporting "no PR"
2642
+ * for an issue that has one is a correctness bug; the unscoped walk it falls
2643
+ * back to instead is merely slow, and the dedupe and cache in this same change
2644
+ * already blunt that cost.
2645
+ */
2646
+ #probeRepoForIssue(issue) {
2647
+ return dependencyRepoForIssue(issue, undefined, this.#config, { allowDefault: false });
2648
+ }
2649
+ /**
2650
+ * The key for BOTH `#probePrResolvedCache` and `#probePrGhBackoffUntilMs`.
2651
+ *
2652
+ * Shared rather than inlined because those two maps are written from more than
2653
+ * one place: `#resolveIssuePr` writes both, and the completion sweep writes a
2654
+ * draft-PR backoff directly. Those writes must agree on the key or the backoff
2655
+ * is set under a name nothing reads, which silently costs a gh call per pass.
2656
+ *
2657
+ * `repo` is part of the key because it narrows which pull requests the
2658
+ * resolution can even see — an issue whose route changes must not be served
2659
+ * the previous repository's PR, or held off gh by the previous repository's
2660
+ * negative backoff, because the completion path probes and CLOSES what this
2661
+ * returns. `*` marks the unscoped walk, a genuinely different resolution that
2662
+ * must not share an entry with any single-repo one.
2663
+ *
2664
+ * Every dimension is a trailing `:`-prefixed segment so the completion
2665
+ * invalidation — which clears `stateKey` plus everything starting
2666
+ * `${stateKey}:` — keeps clearing the whole family as the key grows.
2667
+ */
2668
+ #probePrCacheKey(issue, opts = {}) {
2611
2669
  const issueKey = issueStateKey(issueRef(issue));
2612
- const key = `${opts.openOnly ? `${issueKey}:open` : issueKey}${opts.allowLegacyGithubBranch ? ':legacy' : ''}`;
2670
+ const repoScope = opts.repo ? opts.repo.trim().toLowerCase() : '*';
2671
+ return `${opts.openOnly ? `${issueKey}:open` : issueKey}${opts.allowLegacyGithubBranch ? ':legacy' : ''}:repo=${repoScope}`;
2672
+ }
2673
+ async #resolveIssuePr(issue, opts = {}) {
2674
+ const key = this.#probePrCacheKey(issue, opts);
2613
2675
  const now = this.#clock.now();
2614
2676
  const cached = this.#probePrResolvedCache.get(key);
2615
2677
  if (cached && cached.expiresAtMs > now) {
2616
2678
  return cached.pr;
2617
2679
  }
2618
- const mountPr = await resolveIssuePrFromMount(this.#mount, this.#config, issue, opts, (prefix) => this.#listRelayfileTree(prefix, 'PR probe resolution'));
2680
+ const mountPr = await resolveIssuePrFromMount(this.#mount, this.#config, issue, opts, (prefix) => this.#listRelayfileTree(prefix, 'PR probe resolution'), this.#probeMountWalkProgress('[factory] PR probe mount read progress', issue));
2619
2681
  if (mountPr) {
2682
+ // The mount branch runs first and is the common hit, and until now it was
2683
+ // the one branch that never wrote the cache it reads at the top of this
2684
+ // method. The cache had a reader and no writer on the hot path, so the
2685
+ // full tree walk repeated for every caller, on every sweep, forever.
2686
+ //
2687
+ // Cached on the same terms as the gh branch below — same key, same TTL,
2688
+ // same draft exclusion — because the reason to keep a draft uncached is a
2689
+ // property of the PR (its state is about to flip and the caller wants to
2690
+ // see that promptly), not of which resolver observed it.
2691
+ if (!mountPr.draft) {
2692
+ this.#probePrResolvedCache.set(key, { pr: mountPr, expiresAtMs: now + PROBE_PR_GH_BACKOFF_MS });
2693
+ }
2620
2694
  return mountPr;
2621
2695
  }
2622
2696
  const backoffUntil = this.#probePrGhBackoffUntilMs.get(key) ?? 0;
@@ -3095,6 +3169,7 @@ export class FactoryLoop {
3095
3169
  // current provider snapshots (or merged PR metadata) so a reopened issue
3096
3170
  // cannot remain permanently resolved after an earlier close event.
3097
3171
  this.#terminalDependencyIdentities.clear();
3172
+ this.#dependencyPrProbes.clear();
3098
3173
  this.#dependencyGithubPathsByIdentity = undefined;
3099
3174
  this.#dependencyLinearTreeLoaded = false;
3100
3175
  const issueSource = await this.#issueSource();
@@ -4183,6 +4258,7 @@ export class FactoryLoop {
4183
4258
  openOnly: true,
4184
4259
  failOnLookupError: true,
4185
4260
  allowLegacyGithubBranch: true,
4261
+ repo: this.#probeRepoForIssue(issue),
4186
4262
  });
4187
4263
  }
4188
4264
  async #adoptOrphanedGithubPullRequest(issue, pr, legacyUnownedAgents = []) {
@@ -4582,6 +4658,51 @@ export class FactoryLoop {
4582
4658
  #elapsedSince(startedAtMs) {
4583
4659
  return Math.max(0, this.#clock.now() - startedAtMs);
4584
4660
  }
4661
+ /**
4662
+ * Progress reporting for the mount PR walk.
4663
+ *
4664
+ * `listTree` inside that walk is wrapped by `#listRelayfileTree` — named,
4665
+ * timed and logged. The `readFile` per candidate path was not: it ran inside a
4666
+ * bare try/catch that swallows failures into `undefined`, with no logger, no
4667
+ * counter and no progress line. A walk of several thousand paths at ~175ms
4668
+ * each therefore emitted its last log line at the final `listTree` and then
4669
+ * went silent for twelve minutes, which is indistinguishable from a hung
4670
+ * process. Three investigation layers could not tell those apart from the logs
4671
+ * alone, so the missing instrumentation is a defect in its own right and not a
4672
+ * nice-to-have.
4673
+ *
4674
+ * Same cadence helper the ready-issue read loop uses, so a long PR probe reads
4675
+ * like a long issue read; the counter carries the same signal into /evidence.
4676
+ */
4677
+ #probeMountWalkProgress(message, issue) {
4678
+ const startedAtMs = this.#clock.now();
4679
+ let lastLoggedAtMs = startedAtMs;
4680
+ return {
4681
+ onRead: (progress) => {
4682
+ this.#increment('probePrMountReads');
4683
+ lastLoggedAtMs = this.#logTimedProgress(message, startedAtMs, lastLoggedAtMs, {
4684
+ issue: issue.key,
4685
+ read: progress.read,
4686
+ total: progress.total,
4687
+ path: progress.path,
4688
+ });
4689
+ },
4690
+ // One line per repository saying why the tree walk was necessary. The walk
4691
+ // is a permanent silent fallback today, and a silent permanent fallback is
4692
+ // indistinguishable from a fast path that is working. This makes the day
4693
+ // the index becomes usable (relayfile-adapters#271) show up in the logs
4694
+ // instead of passing unnoticed.
4695
+ onIndexFallback: (repo, reason) => {
4696
+ this.#increment('probePrIndexFallbacks');
4697
+ this.#increment(PROBE_PR_INDEX_FALLBACK_COUNTERS[reason]);
4698
+ this.#logger.debug?.('[factory] PR probe fell back to a full mount walk', {
4699
+ issue: issue.key,
4700
+ repo,
4701
+ reason,
4702
+ });
4703
+ },
4704
+ };
4705
+ }
4585
4706
  #logTimedProgress(message, startedAtMs, lastLoggedAtMs, metadata) {
4586
4707
  const now = this.#clock.now();
4587
4708
  if (now - lastLoggedAtMs < REMOTE_OPERATION_PROGRESS_INTERVAL_MS) {
@@ -8328,12 +8449,25 @@ export class FactoryLoop {
8328
8449
  const repo = dependencyRepoForIssue(issue, undefined, this.#config);
8329
8450
  if (!repo)
8330
8451
  return false;
8452
+ // This probe does NOT go through `#resolveIssuePr` — it must not fall back to
8453
+ // gh — so it never saw that method's cache, and `#terminalDependencyIdentities`
8454
+ // only ever memoises the TRUE answer. A dependency that is not merged was
8455
+ // therefore re-walked in full for every issue declaring it, on every sweep;
8456
+ // several issues blocked on one dependency multiplied a single tree walk by
8457
+ // the number of blocked issues. Memoise the negative answer too, on exactly
8458
+ // the lifetime of the terminal set beside it: cleared at the top of each
8459
+ // sweep, so a PR that merges between sweeps is still observed.
8460
+ const memoized = this.#dependencyPrProbes.get(identity);
8461
+ if (memoized !== undefined)
8462
+ return memoized;
8331
8463
  const pullRequest = await resolveIssuePrFromMount(this.#mount, this.#config, issue, {
8332
8464
  allowLegacyGithubBranch: true,
8333
8465
  repo,
8334
- }, (prefix) => this.#listRelayfileTree(prefix, 'dependency PR probe resolution'));
8335
- if (normalizePrState(pullRequest?.state) !== 'MERGED')
8466
+ }, (prefix) => this.#listRelayfileTree(prefix, 'dependency PR probe resolution'), this.#probeMountWalkProgress('[factory] dependency PR probe mount read progress', issue));
8467
+ if (normalizePrState(pullRequest?.state) !== 'MERGED') {
8468
+ this.#dependencyPrProbes.set(identity, false);
8336
8469
  return false;
8470
+ }
8337
8471
  this.#terminalDependencyIdentities.add(identity);
8338
8472
  return true;
8339
8473
  }
@@ -14770,8 +14904,25 @@ export class FactoryLoop {
14770
14904
  settleIssueWritebackOnce();
14771
14905
  this.#completionInFlight.delete(completionKey);
14772
14906
  const stateKey = issueStateKey(record.issue);
14773
- this.#probePrGhBackoffUntilMs.delete(stateKey);
14774
- this.#probePrResolvedCache.delete(stateKey);
14907
+ // Both maps are keyed by issue state key PLUS the option suffixes
14908
+ // `#resolveIssuePr` appends (`:open`, `:legacy`, `:open:legacy`), but this
14909
+ // invalidation only ever deleted the bare key. Every `openOnly` probe —
14910
+ // `#openPrForIssue` and `#openCompletionPr`, i.e. the completion path — was
14911
+ // therefore never invalidated at all. That was survivable only because the
14912
+ // mount branch never wrote the cache; now that it does, a stale OPEN entry
14913
+ // outliving completion would be a live correctness bug, so clear the whole
14914
+ // family. Suffixes always start with ':', and no other issue's state key can
14915
+ // be this one followed by ':', so the prefix test cannot over-delete.
14916
+ for (const cacheKey of [...this.#probePrResolvedCache.keys()]) {
14917
+ if (cacheKey === stateKey || cacheKey.startsWith(`${stateKey}:`)) {
14918
+ this.#probePrResolvedCache.delete(cacheKey);
14919
+ }
14920
+ }
14921
+ for (const backoffKey of [...this.#probePrGhBackoffUntilMs.keys()]) {
14922
+ if (backoffKey === stateKey || backoffKey.startsWith(`${stateKey}:`)) {
14923
+ this.#probePrGhBackoffUntilMs.delete(backoffKey);
14924
+ }
14925
+ }
14775
14926
  // Cancellation must see the subscription identity so it can issue the
14776
14927
  // idempotent Relayfile DELETE before clearing the local owner maps.
14777
14928
  await this.#cancelBabysittersForIssue(record.issue);
@@ -17439,6 +17590,7 @@ export class FactoryLoop {
17439
17590
  ? await this.#probePrResolver(issue)
17440
17591
  : await this.#resolveIssuePr(issue, {
17441
17592
  titleMarker: FACTORY_E2E_MARKER,
17593
+ repo: this.#probeRepoForIssue(issue),
17442
17594
  });
17443
17595
  if (!probe) {
17444
17596
  return;
@@ -17711,7 +17863,7 @@ const githubIssueSourceRef = (issue) => {
17711
17863
  }
17712
17864
  return { owner, repo, number: number, url };
17713
17865
  };
17714
- function dependencyRepoForIssue(issue, decision, config) {
17866
+ function dependencyRepoForIssue(issue, decision, config, opts = {}) {
17715
17867
  const source = githubIssueSourceRef(issue);
17716
17868
  if (source)
17717
17869
  return `${source.owner}/${source.repo}`;
@@ -17748,7 +17900,19 @@ function dependencyRepoForIssue(issue, decision, config) {
17748
17900
  ? Object.entries(config.repos.byProject)
17749
17901
  .find(([project]) => project.trim().toLowerCase() === issue.project.trim().toLowerCase())?.[1]
17750
17902
  : undefined;
17751
- return normalize(projectRepo) ?? normalize(config.repos.default);
17903
+ const projectResolved = normalize(projectRepo);
17904
+ if (projectResolved)
17905
+ return projectResolved;
17906
+ // `repos.default` is a DISPATCH fallback, not evidence of where a PR already
17907
+ // lives. Routing precedence is byLabel, byProject, keywordRules, default —
17908
+ // and this helper cannot see `keywordRules` at all, because those match on
17909
+ // title/description through triage rather than on the issue's own fields. So
17910
+ // for a keyword-routed issue the default answer here is confidently wrong:
17911
+ // dispatch opened the PR in the keyword-selected repository, not in
17912
+ // `repos.default`. Callers that only need a dispatch target still want that
17913
+ // fallback; a PROBE must not have it, because a mis-scoped probe reports "no
17914
+ // PR" for an issue that has one, which is worse than a slow unscoped walk.
17915
+ return opts.allowDefault === false ? undefined : normalize(config.repos.default);
17752
17916
  }
17753
17917
  function dependencyIdentityForIssue(issue, repo) {
17754
17918
  const source = githubIssueSourceRef(issue);
@@ -18736,10 +18900,59 @@ const linearIssueMirrorsGithubIssue = (issue, ghIssue) => {
18736
18900
  .split(/\r?\n/u)
18737
18901
  .some((line) => line.trim() === `${GITHUB_MIRROR_SOURCE_PREFIX}${ghIssue.url}`);
18738
18902
  };
18739
- const resolveIssuePrFromMount = async (mount, config, issue, opts = {}, listTree = (prefix) => mount.listTree(prefix)) => {
18903
+ const PROBE_PR_INDEX_FALLBACK_COUNTERS = {
18904
+ 'index-absent': 'probePrIndexAbsent',
18905
+ 'index-shape-unrecognised': 'probePrIndexShapeUnrecognised',
18906
+ 'index-without-head-ref': 'probePrIndexWithoutHeadRef',
18907
+ // Not a fallback condition so much as a standing invitation: rows carry
18908
+ // `headRef`, so the walk is now avoidable and this counter going non-zero is
18909
+ // the cue to build the fast path.
18910
+ 'index-usable': 'probePrIndexUsableButUnused',
18911
+ };
18912
+ /**
18913
+ * Classify the pull index without acting on it.
18914
+ *
18915
+ * Deliberately diagnostic only. Factory's issue-side reader
18916
+ * (`#githubIssuePathsFromIndex`) uses its index as a NARROWING FILTER over
18917
+ * paths it still reads individually, which is safe because it can only
18918
+ * over-select. A pull index cannot be used that way today: it carries no
18919
+ * `headRef`, so it cannot exclude any pull request from a branch match, and a
18920
+ * title hit alone (score 20) must never be returned as the answer while an
18921
+ * unread branch match (score 30) could outrank it. Until the row contract
18922
+ * carries `headRef` (relayfile-adapters#271), the only honest thing to do with
18923
+ * the index is say out loud why it did not help.
18924
+ */
18925
+ const classifyPullIndexFallback = async (mount, repo) => {
18926
+ const [owner, name] = repo.split('/');
18927
+ if (!owner || !name)
18928
+ return 'index-absent';
18929
+ let parsed;
18930
+ try {
18931
+ const { content } = await mount.readFile(`${GITHUB_ISSUE_ROOT}/${owner}/${name}/pulls/_index.json`);
18932
+ parsed = parseJsonContent(content);
18933
+ }
18934
+ catch (error) {
18935
+ // A mount that is failing pass-wide must not be reported as "no index" —
18936
+ // that is the same convention the tree reads below already follow.
18937
+ if (isPassWideRelayfileFault(error))
18938
+ throw error;
18939
+ return 'index-absent';
18940
+ }
18941
+ if (!Array.isArray(parsed))
18942
+ return 'index-shape-unrecognised';
18943
+ // An empty index proves nothing about the row contract, so it must not read
18944
+ // as usable — `[].every(...)` is vacuously true.
18945
+ return parsed.length > 0 && parsed.every((entry) => typeof asRecord(entry)?.headRef === 'string')
18946
+ ? 'index-usable'
18947
+ : 'index-without-head-ref';
18948
+ };
18949
+ const resolveIssuePrFromMount = async (mount, config, issue, opts = {}, listTree = (prefix) => mount.listTree(prefix), observer = {}) => {
18740
18950
  const candidates = [];
18741
18951
  const listErrors = [];
18742
18952
  for (const repo of opts.repo ? [opts.repo] : reposFromConfig(config)) {
18953
+ if (observer.onIndexFallback) {
18954
+ observer.onIndexFallback(repo, await classifyPullIndexFallback(mount, repo));
18955
+ }
18743
18956
  const paths = new Set();
18744
18957
  for (const root of githubPullRoots(repo)) {
18745
18958
  try {
@@ -18752,10 +18965,41 @@ const resolveIssuePrFromMount = async (mount, config, issue, opts = {}, listTree
18752
18965
  listErrors.push(error);
18753
18966
  }
18754
18967
  }
18968
+ // `githubPullRoots` is plural, and both of its roots describe the SAME
18969
+ // repository: the nested `<owner>/<repo>/pulls/` layout and the flat
18970
+ // `<owner>__<repo>/pulls/by-id/` one. A pull request is generally present
18971
+ // under both, spelled differently, so the union walked most PRs twice — in
18972
+ // the live workspace 2877 nested paths plus 1156 by-id paths for roughly
18973
+ // 1156 actual pull requests.
18974
+ //
18975
+ // Collapse them on the identity the PATH already carries. `githubPullPathParts`
18976
+ // reads owner/repo/number out of either spelling and costs no mount read, so
18977
+ // the dedupe is free. Insertion order is preserved and the first spelling
18978
+ // encountered wins, which is the same candidate the existing stable sort kept
18979
+ // when two spellings of one PR tied — so the winner does not move.
18980
+ //
18981
+ // Paths the matcher does not recognise (`pulls/_index.json`, per-PR
18982
+ // `comments/*.json`) carry no PR identity, so they are left in the walk and
18983
+ // still read exactly as before rather than being filtered on a guess.
18984
+ const walk = [];
18985
+ const seenPulls = new Set();
18755
18986
  for (const path of paths) {
18756
18987
  if (!path.endsWith('.json'))
18757
18988
  continue;
18989
+ const parts = githubPullPathParts(path);
18990
+ if (parts) {
18991
+ const identity = `${parts.owner}/${parts.repo}#${parts.number}`.toLowerCase();
18992
+ if (seenPulls.has(identity))
18993
+ continue;
18994
+ seenPulls.add(identity);
18995
+ }
18996
+ walk.push(path);
18997
+ }
18998
+ let read = 0;
18999
+ for (const path of walk) {
18758
19000
  const pr = await readProbePrCandidate(mount, path);
19001
+ read += 1;
19002
+ observer.onRead?.({ read, total: walk.length, path });
18759
19003
  if (opts.openOnly && normalizePrState(pr?.state) !== 'OPEN')
18760
19004
  continue;
18761
19005
  const score = pr