@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":"AAKA,OAAO,EAKL,KAAK,aAAa,EAEnB,MAAM,kBAAkB,CAAA;AAgBzB,OAAO,KAAK,EAOV,WAAW,EASX,WAAW,EAMZ,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAIV,iBAAiB,EAYjB,8BAA8B,EAC9B,oBAAoB,EACrB,MAAM,gBAAgB,CAAA;AAmCvB,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EAEP,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,mBAAmB,EAEnB,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EASnB,QAAQ,EACR,eAAe,EACf,WAAW,EAGX,cAAc,EAGf,MAAM,UAAU,CAAA;AAyDjB,KAAK,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,YAAY,GAAG,oBAAoB,GAAG,OAAO,CAAA;AACjG,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;AAgItD,KAAK,iBAAiB,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC7B,CAAA;AAwPD,eAAO,MAAM,mCAAmC,iDAAiD,CAAA;AACjG,eAAO,MAAM,kCAAkC,gDAAgD,CAAA;AAW/F;;;;;;;;;;GAUG;AACH,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,EACrB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,EACpB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,IAAI,CAaN;AA6ED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAEjF;AAuBD,qBAAa,WAAY,YAAW,OAAO;;
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
14774
|
-
|
|
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
|
-
|
|
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
|
|
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
|