@agent-relay/factory 0.1.24 → 0.1.25
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.
|
@@ -17,7 +17,6 @@ type GithubIssueSource = {
|
|
|
17
17
|
path: string;
|
|
18
18
|
raw: Record<string, unknown>;
|
|
19
19
|
};
|
|
20
|
-
export declare const LIVE_GITHUB_ISSUE_GLOB = "/github/repos/**";
|
|
21
20
|
export declare const DEFAULT_FACTORY_LOOP_HEARTBEAT_PATH = "/tmp/factory-run/factory-loop-heartbeat.json";
|
|
22
21
|
export declare const DEFAULT_FACTORY_LOOP_REGISTRY_PATH = "/tmp/factory-run/factory-loop-registry.json";
|
|
23
22
|
export declare function createFactory(config: FactoryConfig, ports: FactoryPorts): Factory;
|
|
@@ -56,6 +55,7 @@ export declare const githubIssuePathParts: (path: string) => {
|
|
|
56
55
|
number: number;
|
|
57
56
|
slug?: string;
|
|
58
57
|
} | undefined;
|
|
58
|
+
export declare const githubRepoSubscriptionGlobs: (config: FactoryConfig) => string[];
|
|
59
59
|
export declare const keyFromPath: (path: string) => string;
|
|
60
60
|
export declare const changeEventPath: (event: ChangeEvent) => string | undefined;
|
|
61
61
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/orchestrator/factory.ts"],"names":[],"mappings":"AAIA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAI1E,OAAO,KAAK,EAKV,WAAW,EAKX,WAAW,EAKZ,MAAM,UAAU,CAAA;AA6BjB,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EACP,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,mBAAmB,EAEnB,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EAGnB,QAAQ,EACR,eAAe,EACf,WAAW,EAGX,cAAc,EAEf,MAAM,UAAU,CAAA;AAOjB,KAAK,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,YAAY,GAAG,oBAAoB,GAAG,OAAO,CAAA;AACjG,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;AAiEtD,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;
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/orchestrator/factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -34,15 +34,6 @@ const GITHUB_ISSUE_ROOT = '/github/repos';
|
|
|
34
34
|
const READY_EVENTS_LIMIT = 100;
|
|
35
35
|
const LIVE_ISSUE_GLOB = `${ISSUE_ROOT}/**`;
|
|
36
36
|
const LIVE_RELAYFLOW_GLOB = '/**';
|
|
37
|
-
// Subscribe broadly under /github/repos and let isGithubIssueFilePath() /
|
|
38
|
-
// githubPullPathParts() re-validate the exact shape in the callback.
|
|
39
|
-
// globMatchesPath() treats a non-terminal `**` as a single-segment wildcard, so
|
|
40
|
-
// a more specific glob like `.../issues/**/*.json` would miss the two-segment
|
|
41
|
-
// <owner>/<repo> prefix and the nested <number>__<slug>/meta.json (and
|
|
42
|
-
// directory) event shapes this factory accepts. A terminal `**` prefix-matches
|
|
43
|
-
// every descendant, so all supported issue AND pull-request path variants reach
|
|
44
|
-
// the handler — including the PR change events the babysitter reacts to.
|
|
45
|
-
export const LIVE_GITHUB_ISSUE_GLOB = `${GITHUB_ISSUE_ROOT}/**`;
|
|
46
37
|
const LIVE_DEDUPE_LIMIT = 5_000;
|
|
47
38
|
const LIVE_EVENT_DRAIN_BATCH_SIZE = 5;
|
|
48
39
|
const COMPLETION_SWEEP_INTERVAL_MS = 15_000;
|
|
@@ -453,7 +444,7 @@ export class FactoryLoop {
|
|
|
453
444
|
if ((opts.mode ?? 'live') === 'live') {
|
|
454
445
|
this.#started = true;
|
|
455
446
|
try {
|
|
456
|
-
await this.#startLiveSubscription(opts.liveSubscription);
|
|
447
|
+
await this.#startLiveSubscription(issueSource, opts.liveSubscription);
|
|
457
448
|
await this.#rearmSlackReplyWatchers();
|
|
458
449
|
await this.#drainReadyClarificationWake();
|
|
459
450
|
await this.#rearmGithubIssueCommentWatchers();
|
|
@@ -467,7 +458,7 @@ export class FactoryLoop {
|
|
|
467
458
|
}
|
|
468
459
|
}
|
|
469
460
|
await this.#backfillReadyIssues();
|
|
470
|
-
this.#subscription = this.#mount.subscribe(this.#subscriptionGlobs([`${ISSUE_ROOT}/**/*.json
|
|
461
|
+
this.#subscription = this.#mount.subscribe(this.#subscriptionGlobs(issueSource, [`${ISSUE_ROOT}/**/*.json`]), (event) => {
|
|
471
462
|
void this.#dispatchRelayflowEvent(event);
|
|
472
463
|
// The SDK types `resource` as always-present, but the polling fallback and
|
|
473
464
|
// degraded-sync paths can deliver events without it. Skip those rather
|
|
@@ -610,7 +601,7 @@ export class FactoryLoop {
|
|
|
610
601
|
async dispose() {
|
|
611
602
|
await this.stop();
|
|
612
603
|
}
|
|
613
|
-
async #startLiveSubscription(overrides = {}) {
|
|
604
|
+
async #startLiveSubscription(issueSource, overrides = {}) {
|
|
614
605
|
const options = this.#liveOptions(overrides);
|
|
615
606
|
await this.#startLiveHeartbeat();
|
|
616
607
|
this.#liveConnectStartedAtMs = this.#clock.now();
|
|
@@ -624,41 +615,51 @@ export class FactoryLoop {
|
|
|
624
615
|
replaySkewMarginMs: this.#liveReplaySkewMarginMs,
|
|
625
616
|
highWatermarkRouteUnavailable: highWatermark.routeUnavailable,
|
|
626
617
|
});
|
|
627
|
-
// Register the live subscription BEFORE the startup
|
|
618
|
+
// Register the live subscription BEFORE the startup backfill so an issue
|
|
628
619
|
// that becomes Ready *during* the pull is captured, not lost in the window
|
|
629
620
|
// between listTree and subscribe. Events buffer (deferred drain) until the
|
|
630
621
|
// pull finishes; batch dedupe then suppresses any overlap with what the
|
|
631
622
|
// pull already dispatched.
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
623
|
+
this.#deferLiveEventDrain = true;
|
|
624
|
+
try {
|
|
625
|
+
if (options.transport === 'poll') {
|
|
626
|
+
// Capture the cursor before the backfill. Events written while listTree
|
|
627
|
+
// is running are then picked up by the first poll instead of falling
|
|
628
|
+
// into a cursor-advance gap.
|
|
629
|
+
this.#liveEventCursor = await this.#currentEventCursor(options.eventLimit);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
this.#subscription = this.#mount.subscribe(this.#subscriptionGlobs(issueSource, [LIVE_ISSUE_GLOB]), (event) => {
|
|
633
|
+
this.#enqueueLiveEvent(event);
|
|
634
|
+
}, { from: 'now', coalesce: 'none' });
|
|
635
|
+
}
|
|
636
|
+
if (highWatermark.routeUnavailable) {
|
|
637
|
+
this.#increment('liveHighWatermarkFullPullFallbacks');
|
|
638
|
+
}
|
|
639
|
+
this.#increment('liveStartupBackfills');
|
|
640
|
+
this.#logger.info?.('[factory] running startup ready-issue backfill before draining buffered events', {
|
|
641
|
+
highWatermarkRouteUnavailable: highWatermark.routeUnavailable,
|
|
642
|
+
});
|
|
644
643
|
try {
|
|
645
644
|
await this.runOnce();
|
|
646
645
|
}
|
|
647
646
|
catch (error) {
|
|
648
|
-
// A startup
|
|
649
|
-
// to the live event stream (plus any buffered events) instead of
|
|
650
|
-
// the factory down.
|
|
651
|
-
this.#increment('
|
|
647
|
+
// A startup backfill failure must not abort the daemon: log it and fall
|
|
648
|
+
// back to the live event stream (plus any buffered events) instead of
|
|
649
|
+
// leaving the factory down.
|
|
650
|
+
this.#increment('liveStartupBackfillErrors');
|
|
651
|
+
if (highWatermark.routeUnavailable) {
|
|
652
|
+
this.#increment('liveHighWatermarkFullPullErrors');
|
|
653
|
+
}
|
|
652
654
|
this.#error(error);
|
|
653
655
|
}
|
|
654
|
-
finally {
|
|
655
|
-
this.#deferLiveEventDrain = false;
|
|
656
|
-
this.#scheduleLiveEventDrain();
|
|
657
|
-
}
|
|
658
|
-
await this.#refreshLiveHeartbeatIfDue();
|
|
659
656
|
}
|
|
657
|
+
finally {
|
|
658
|
+
this.#deferLiveEventDrain = false;
|
|
659
|
+
this.#scheduleLiveEventDrain();
|
|
660
|
+
}
|
|
661
|
+
await this.#refreshLiveHeartbeatIfDue();
|
|
660
662
|
if (options.transport === 'poll') {
|
|
661
|
-
this.#liveEventCursor = await this.#currentEventCursor(options.eventLimit);
|
|
662
663
|
this.#scheduleLivePoll(0, options);
|
|
663
664
|
}
|
|
664
665
|
}
|
|
@@ -873,14 +874,25 @@ export class FactoryLoop {
|
|
|
873
874
|
return;
|
|
874
875
|
await this.#refreshLiveHeartbeat();
|
|
875
876
|
}
|
|
876
|
-
#subscriptionGlobs(
|
|
877
|
-
|
|
877
|
+
#subscriptionGlobs(issueSource, linearGlobs) {
|
|
878
|
+
if (this.#relayflows)
|
|
879
|
+
return [LIVE_RELAYFLOW_GLOB];
|
|
880
|
+
return [
|
|
881
|
+
...(issueSource === 'linear' ? linearGlobs : []),
|
|
882
|
+
...githubRepoSubscriptionGlobs(this.#config),
|
|
883
|
+
];
|
|
878
884
|
}
|
|
879
885
|
async #prepareLiveEventForDrain(event, seenIssueKeys) {
|
|
880
886
|
const path = changeEventPath(event);
|
|
881
887
|
if (!path) {
|
|
882
888
|
return { dispatchRelayflow: false };
|
|
883
889
|
}
|
|
890
|
+
if (!this.#relayflows &&
|
|
891
|
+
path.startsWith(`${GITHUB_ISSUE_ROOT}/`) &&
|
|
892
|
+
!isConfiguredGithubRepoPath(path, this.#config)) {
|
|
893
|
+
this.#increment('liveGithubEventsOutsideConfiguredRepos');
|
|
894
|
+
return { dispatchRelayflow: false };
|
|
895
|
+
}
|
|
884
896
|
const isPullPath = isGithubPullFilePath(path);
|
|
885
897
|
const babysitterEvent = this.#config.babysitter.enabled
|
|
886
898
|
? githubBabysitterEventPathParts(path)
|
|
@@ -8006,24 +8018,69 @@ const reposFromConfig = (config) => {
|
|
|
8006
8018
|
].filter((repo) => Boolean(repo)));
|
|
8007
8019
|
return [...repos];
|
|
8008
8020
|
};
|
|
8009
|
-
const
|
|
8010
|
-
const
|
|
8011
|
-
for (const
|
|
8012
|
-
|
|
8021
|
+
const configuredGithubRepoParts = (config) => {
|
|
8022
|
+
const repos = new Map();
|
|
8023
|
+
for (const configuredRepo of reposFromConfig(config)) {
|
|
8024
|
+
let parts = githubRepoParts(configuredRepo);
|
|
8025
|
+
if (!parts) {
|
|
8026
|
+
try {
|
|
8027
|
+
parts = githubRepoParts(normalizeGithubRepo(configuredRepo, config.repos.org));
|
|
8028
|
+
}
|
|
8029
|
+
catch {
|
|
8030
|
+
continue;
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8013
8033
|
if (!parts)
|
|
8014
8034
|
continue;
|
|
8015
|
-
|
|
8035
|
+
repos.set(`${parts.owner.toLowerCase()}/${parts.repo.toLowerCase()}`, parts);
|
|
8036
|
+
}
|
|
8037
|
+
return [...repos.values()];
|
|
8038
|
+
};
|
|
8039
|
+
// A terminal `**` is intentional: relayfile's matcher treats a non-terminal
|
|
8040
|
+
// `**` as a single-segment wildcard. Scoping at the repository root still
|
|
8041
|
+
// covers every supported issue, PR, review, comment, and check path without
|
|
8042
|
+
// subscribing this factory to other repositories in the workspace.
|
|
8043
|
+
export const githubRepoSubscriptionGlobs = (config) => configuredGithubRepoParts(config).flatMap(({ owner, repo }) => [
|
|
8044
|
+
`${GITHUB_ISSUE_ROOT}/${owner}/${repo}/**`,
|
|
8045
|
+
`${GITHUB_ISSUE_ROOT}/${owner}__${repo}/**`,
|
|
8046
|
+
]);
|
|
8047
|
+
const githubIssueScanRoots = (config) => {
|
|
8048
|
+
const roots = new Set();
|
|
8049
|
+
for (const { owner, repo } of configuredGithubRepoParts(config)) {
|
|
8050
|
+
roots.add(`${GITHUB_ISSUE_ROOT}/${owner}/${repo}/issues`);
|
|
8051
|
+
roots.add(`${GITHUB_ISSUE_ROOT}/${owner}__${repo}/issues`);
|
|
8016
8052
|
}
|
|
8017
8053
|
return [...roots];
|
|
8018
8054
|
};
|
|
8055
|
+
const githubRepoPathParts = (path) => {
|
|
8056
|
+
const compactSegment = path.match(/^\/github\/repos\/([^/]+)\//u)?.[1];
|
|
8057
|
+
const separator = compactSegment?.indexOf('__') ?? -1;
|
|
8058
|
+
if (compactSegment && separator > 0 && separator < compactSegment.length - 2) {
|
|
8059
|
+
return {
|
|
8060
|
+
owner: compactSegment.slice(0, separator),
|
|
8061
|
+
repo: compactSegment.slice(separator + 2),
|
|
8062
|
+
};
|
|
8063
|
+
}
|
|
8064
|
+
const nested = path.match(/^\/github\/repos\/([^/]+)\/([^/]+)\//u);
|
|
8065
|
+
if (nested)
|
|
8066
|
+
return { owner: nested[1], repo: nested[2] };
|
|
8067
|
+
return undefined;
|
|
8068
|
+
};
|
|
8069
|
+
const isConfiguredGithubRepoPath = (path, config) => {
|
|
8070
|
+
const pathParts = githubRepoPathParts(path);
|
|
8071
|
+
if (!pathParts)
|
|
8072
|
+
return false;
|
|
8073
|
+
const pathRepo = `${pathParts.owner.toLowerCase()}/${pathParts.repo.toLowerCase()}`;
|
|
8074
|
+
return configuredGithubRepoParts(config).some(({ owner, repo }) => `${owner.toLowerCase()}/${repo.toLowerCase()}` === pathRepo);
|
|
8075
|
+
};
|
|
8019
8076
|
const githubRepoParts = (repo) => {
|
|
8020
8077
|
const split = repo.match(/^([^/]+)\/([^/]+)$/u);
|
|
8021
8078
|
if (split) {
|
|
8022
8079
|
return { owner: split[1], repo: split[2] };
|
|
8023
8080
|
}
|
|
8024
|
-
const
|
|
8025
|
-
if (
|
|
8026
|
-
return { owner:
|
|
8081
|
+
const separator = repo.indexOf('__');
|
|
8082
|
+
if (separator > 0 && separator < repo.length - 2 && !repo.includes('/')) {
|
|
8083
|
+
return { owner: repo.slice(0, separator), repo: repo.slice(separator + 2) };
|
|
8027
8084
|
}
|
|
8028
8085
|
return undefined;
|
|
8029
8086
|
};
|