@agent-relay/factory 0.1.33 → 0.1.35
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.
- package/README.md +15 -3
- package/dist/cli/fleet.d.ts +4 -0
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +99 -6
- package/dist/cli/fleet.js.map +1 -1
- package/dist/config/schema.d.ts +15 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts +18 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.js +26 -0
- package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
- package/dist/mount/relayfile-integration-preflight.d.ts +28 -0
- package/dist/mount/relayfile-integration-preflight.d.ts.map +1 -0
- package/dist/mount/relayfile-integration-preflight.js +92 -0
- package/dist/mount/relayfile-integration-preflight.js.map +1 -0
- package/dist/observability/cloud-reporter.d.ts.map +1 -1
- package/dist/observability/cloud-reporter.js +21 -5
- package/dist/observability/cloud-reporter.js.map +1 -1
- package/dist/observability/events.d.ts +67 -27
- package/dist/observability/events.d.ts.map +1 -1
- package/dist/observability/events.js +13 -0
- package/dist/observability/events.js.map +1 -1
- package/dist/observability/instance-identity.d.ts +14 -0
- package/dist/observability/instance-identity.d.ts.map +1 -1
- package/dist/observability/instance-identity.js +18 -0
- package/dist/observability/instance-identity.js.map +1 -1
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +160 -18
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/ports/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/mount.d.ts +18 -0
- package/dist/ports/mount.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance-identity.js","sourceRoot":"","sources":["../../src/observability/instance-identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,MAAM,IAAI,GAAG,6EAA6E,CAAA;
|
|
1
|
+
{"version":3,"file":"instance-identity.js","sourceRoot":"","sources":["../../src/observability/instance-identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,MAAM,IAAI,GAAG,6EAA6E,CAAA;AAC1F,MAAM,gCAAgC,GAAG,GAAG,CAAA;AAO5C;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CAAC,MAAiC;IAC1E,IAAI,MAAM,CAAC,SAAS,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QAChD,OAAO,4BAA4B,CAAC,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,CAAA;IACpE,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACtD,OAAO,4BAA4B,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAAC,IAAY;IAC9D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACtD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAA;QACtF,OAAO,QAAQ,CAAA;IACjB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAA;IAC7C,CAAC;IAED,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC5D,MAAM,aAAa,GAAG,GAAG,IAAI,IAAI,UAAU,EAAE,MAAM,CAAA;IACnD,IAAI,eAAe,GAAG,KAAK,CAAA;IAC3B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;QACrD,eAAe,GAAG,IAAI,CAAA;QACtB,MAAM,EAAE,GAAG,UAAU,EAAE,CAAA;QACvB,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAA;YACzC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;QACrB,CAAC;gBAAS,CAAC;YACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QACtB,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAA;YAC/B,OAAO,EAAE,CAAA;QACX,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAA;QAC/C,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAA;IAC/C,CAAC;YAAS,CAAC;QACT,IAAI,eAAe;YAAE,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IACzE,CAAC;IAED,MAAM,EAAE,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAChD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAA;IAChF,OAAO,EAAE,CAAA;AACX,CAAC;AAED,MAAM,oBAAoB,GAAG,CAAC,KAAc,EAAW,EAAE,CACvD,OAAO,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAA;AAE3F,MAAM,kBAAkB,GAAG,CAAC,KAAc,EAAW,EAAE,CACrD,OAAO,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAA;AAE3F,MAAM,4BAA4B,GAAG,CAAC,KAAa,EAAsB,EAAE;IACzE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IACzB,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,gCAAgC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;AAC/F,CAAC,CAAA"}
|
|
@@ -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,EAMV,WAAW,EAOX,WAAW,EAKZ,MAAM,UAAU,CAAA;AA+BjB,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;
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/orchestrator/factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -134,6 +134,8 @@ export class FactoryLoop {
|
|
|
134
134
|
#githubIssueCommentQueues = new Map();
|
|
135
135
|
#githubIssueAuthors = new Map();
|
|
136
136
|
#githubIssueAuthorLookups = new Map();
|
|
137
|
+
#githubIssuePreferredPaths = new Map();
|
|
138
|
+
#githubIssuePathIndexReady = false;
|
|
137
139
|
#slackReporterUserIds = new Map();
|
|
138
140
|
#slackReporterUserIdLookups = new Map();
|
|
139
141
|
#reconciledGithubInProgress = new Set();
|
|
@@ -1881,6 +1883,9 @@ export class FactoryLoop {
|
|
|
1881
1883
|
this.#error(error, decision.issue);
|
|
1882
1884
|
throw error;
|
|
1883
1885
|
}
|
|
1886
|
+
if (!this.#isIssueReady(liveIssue)) {
|
|
1887
|
+
throw new LiveDispatchStateChangedError(decision.issue.key);
|
|
1888
|
+
}
|
|
1884
1889
|
if (!isDispatchableIssue(liveIssue)) {
|
|
1885
1890
|
const error = new Error(`Refusing to dispatch ${decision.issue.key}: not reconciled real Linear issue`);
|
|
1886
1891
|
this.#error(error, decision.issue);
|
|
@@ -1977,6 +1982,12 @@ export class FactoryLoop {
|
|
|
1977
1982
|
await this.#ensureGithubAgentQuestionWatch(record, liveIssue);
|
|
1978
1983
|
const spawnedForReaperHandoff = [];
|
|
1979
1984
|
try {
|
|
1985
|
+
if (!dryRun) {
|
|
1986
|
+
const issue = await this.#readIssue(dispatchDecision.issue.path);
|
|
1987
|
+
if (!issue || !this.#isIssueReady(issue)) {
|
|
1988
|
+
throw new LiveDispatchStateChangedError(dispatchDecision.issue.key);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1980
1991
|
const specs = dispatchSpecs(dispatchDecision);
|
|
1981
1992
|
const agents = [];
|
|
1982
1993
|
for (const spec of specs) {
|
|
@@ -2038,8 +2049,10 @@ export class FactoryLoop {
|
|
|
2038
2049
|
// acknowledged spawns, so cleanup never races a name-only survivor.
|
|
2039
2050
|
const failureHandoffs = this.#dispatchFailureHandoffs(record, spawnedForReaperHandoff);
|
|
2040
2051
|
await this.#persistDispatchFailureReaperHandoff(record, failureHandoffs);
|
|
2041
|
-
let worktreesTornDown = await this.#teardownFailedDispatchWorktrees(failureHandoffs);
|
|
2042
2052
|
const liveStateChanged = error instanceof LiveDispatchStateChangedError;
|
|
2053
|
+
const cancellationReason = factoryCloudDispatchCancellationReason(error);
|
|
2054
|
+
const cleanupReason = liveStateChanged ? 'live dispatch state changed' : 'dispatch failed';
|
|
2055
|
+
let worktreesTornDown = await this.#teardownFailedDispatchWorktrees(failureHandoffs, cleanupReason);
|
|
2043
2056
|
if (liveStateChanged && !failureHandoffs.some((handoff) => handoff.worktree)) {
|
|
2044
2057
|
const failed = await this.#releaseAndTerminateAgents(failureHandoffs.map((handoff) => [handoff.name, handoff.tracked]), 'live dispatch state changed', 'completion');
|
|
2045
2058
|
if (failed.length === 0) {
|
|
@@ -2052,13 +2065,13 @@ export class FactoryLoop {
|
|
|
2052
2065
|
let failedState;
|
|
2053
2066
|
if (liveStateChanged) {
|
|
2054
2067
|
await this.#clearDispatchInFlight(decision.issue);
|
|
2055
|
-
await this.#saveDispatchLifecycle(record, 'abandoned');
|
|
2068
|
+
await this.#saveDispatchLifecycle(record, 'abandoned', undefined, undefined, new Set(), { cancellationReason });
|
|
2056
2069
|
this.#increment('dispatchLiveStateRaces');
|
|
2057
2070
|
}
|
|
2058
2071
|
else {
|
|
2059
2072
|
await this.#recordDispatchFailure(decision.issue);
|
|
2060
2073
|
failedState = await this.#state.getDispatchAttempts(this.#workspaceId, decision.issue.key);
|
|
2061
|
-
await this.#saveDispatchLifecycle(record, failedState?.terminal ? 'abandoned' : 'retryable');
|
|
2074
|
+
await this.#saveDispatchLifecycle(record, failedState?.terminal ? 'abandoned' : 'retryable', undefined, undefined, new Set(), { cancellationReason: failedState?.terminal ? cancellationReason : undefined });
|
|
2062
2075
|
}
|
|
2063
2076
|
batch.abandon(decision.issue);
|
|
2064
2077
|
if (!liveStateChanged && !failedState?.terminal)
|
|
@@ -2301,6 +2314,7 @@ export class FactoryLoop {
|
|
|
2301
2314
|
operation: 'save_lifecycle',
|
|
2302
2315
|
previousPhase: options.previousPhase,
|
|
2303
2316
|
errorCode: options.errorCode,
|
|
2317
|
+
cancellationReason: options.cancellationReason,
|
|
2304
2318
|
dryRun: lifecycle.dryRun,
|
|
2305
2319
|
trackedAgents: lifecycle.agents.length,
|
|
2306
2320
|
},
|
|
@@ -2334,7 +2348,7 @@ export class FactoryLoop {
|
|
|
2334
2348
|
},
|
|
2335
2349
|
});
|
|
2336
2350
|
}
|
|
2337
|
-
async #saveDispatchLifecycle(record, phase, pullRequest, releaseReason, releasedAgentNames = new Set()) {
|
|
2351
|
+
async #saveDispatchLifecycle(record, phase, pullRequest, releaseReason, releasedAgentNames = new Set(), telemetry = {}) {
|
|
2338
2352
|
if (record.dryRun || !this.#usesDurableDispatchLifecycle())
|
|
2339
2353
|
return true;
|
|
2340
2354
|
const key = issueKey(record.issue);
|
|
@@ -2368,7 +2382,7 @@ export class FactoryLoop {
|
|
|
2368
2382
|
? 'run.succeeded'
|
|
2369
2383
|
: lifecycle.phase === 'abandoned'
|
|
2370
2384
|
? 'run.cancelled'
|
|
2371
|
-
: 'run.phase_changed', { previousPhase: previous?.phase });
|
|
2385
|
+
: 'run.phase_changed', { previousPhase: previous?.phase, cancellationReason: telemetry.cancellationReason });
|
|
2372
2386
|
}
|
|
2373
2387
|
if (isTerminalDispatchLifecycle(lifecycle)) {
|
|
2374
2388
|
this.#dispatchLifecycleEpochs.delete(key);
|
|
@@ -2538,6 +2552,16 @@ export class FactoryLoop {
|
|
|
2538
2552
|
}
|
|
2539
2553
|
}
|
|
2540
2554
|
async #resumeDurableDispatch(record) {
|
|
2555
|
+
if (!record.dryRun) {
|
|
2556
|
+
const issue = await this.#readIssue(record.issue.path);
|
|
2557
|
+
if (!issue) {
|
|
2558
|
+
throw new Error(`Unable to recover durable dispatch ${record.issue.key}: issue is not currently readable`);
|
|
2559
|
+
}
|
|
2560
|
+
if (isGithubIssue(issue) && !this.#isGithubIssueResumable(issue)) {
|
|
2561
|
+
await this.#abandonDurableResume(record, 'live GitHub issue is closed or no longer ready-for-agent');
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2541
2565
|
const agents = [];
|
|
2542
2566
|
const specs = dispatchSpecs(record.decision);
|
|
2543
2567
|
const plannedNames = new Set(specs.map((spec) => spec.name));
|
|
@@ -2585,6 +2609,55 @@ export class FactoryLoop {
|
|
|
2585
2609
|
}
|
|
2586
2610
|
}
|
|
2587
2611
|
}
|
|
2612
|
+
#isGithubIssueResumable(issue) {
|
|
2613
|
+
if (this.#isIssueReady(issue))
|
|
2614
|
+
return true;
|
|
2615
|
+
if (githubFactoryIssueIsClosed(issue))
|
|
2616
|
+
return false;
|
|
2617
|
+
const labels = new Set(issue.labels.map((label) => label.trim().toLowerCase()));
|
|
2618
|
+
const required = this.#config.safety.requireLabel.trim().toLowerCase();
|
|
2619
|
+
return Boolean(required) &&
|
|
2620
|
+
labels.has(required) &&
|
|
2621
|
+
labels.has('factory:in-progress') &&
|
|
2622
|
+
!labels.has('factory:human-review');
|
|
2623
|
+
}
|
|
2624
|
+
async #abandonDurableResume(record, reason) {
|
|
2625
|
+
const handoffs = this.#dispatchFailureHandoffs(record, [...record.agents].map(([name, tracked]) => ({
|
|
2626
|
+
issue: record.issue,
|
|
2627
|
+
name,
|
|
2628
|
+
tracked: cloneTrackedAgent(tracked),
|
|
2629
|
+
persistedAtMs: this.#clock.now(),
|
|
2630
|
+
})));
|
|
2631
|
+
await this.#persistDispatchFailureReaperHandoff(record, handoffs);
|
|
2632
|
+
if (!await this.#saveDispatchLifecycle(record, 'abandoned', undefined, reason, new Set(), { cancellationReason: 'source_state_changed' }))
|
|
2633
|
+
return;
|
|
2634
|
+
await this.#clearDispatchInFlight(record.issue);
|
|
2635
|
+
const batch = await this.#batch();
|
|
2636
|
+
batch.abandon(record.issue);
|
|
2637
|
+
for (const [name] of record.agents) {
|
|
2638
|
+
this.#fleet.markAgentTerminal?.(name, 'durable-dispatch-abandoned');
|
|
2639
|
+
}
|
|
2640
|
+
if (handoffs.some((handoff) => handoff.worktree)) {
|
|
2641
|
+
await this.#teardownFailedDispatchWorktrees(handoffs, 'live dispatch state changed');
|
|
2642
|
+
}
|
|
2643
|
+
else if (handoffs.length > 0) {
|
|
2644
|
+
const failed = new Set(await this.#releaseAndTerminateAgents(handoffs.map((handoff) => [handoff.name, handoff.tracked]), 'live dispatch state changed', 'completion'));
|
|
2645
|
+
for (const handoff of handoffs) {
|
|
2646
|
+
if (failed.has(handoff.name))
|
|
2647
|
+
continue;
|
|
2648
|
+
await this.#state.clearFailureHandoff(this.#workspaceId, registryHandoffKey(handoff.issue, handoff.name));
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
await this.#stopSlackWatcher(record.issue);
|
|
2652
|
+
await this.#stopGithubIssueCommentWatcherForIssue(record.issue);
|
|
2653
|
+
await this.#writeInFlightRegistry();
|
|
2654
|
+
this.#increment('dispatchLifecycleStaleIssuesAbandoned');
|
|
2655
|
+
this.#resolveDispatchTerminalWaiters(record.issue);
|
|
2656
|
+
this.#logger.info?.('[factory] abandoned durable dispatch whose live issue is no longer ready', {
|
|
2657
|
+
issue: record.issue.key,
|
|
2658
|
+
reason,
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2588
2661
|
async #finishDurableRelease(record, releaseReason) {
|
|
2589
2662
|
const batch = await this.#batch();
|
|
2590
2663
|
const reason = releaseReason ?? (this.#config.terminalState === 'human-review' ? 'issue-human-review' : 'issue-done');
|
|
@@ -2752,8 +2825,7 @@ export class FactoryLoop {
|
|
|
2752
2825
|
if (!isGithubIssue(issue)) {
|
|
2753
2826
|
return this.#states.isRole(issue.stateId, 'readyForAgent');
|
|
2754
2827
|
}
|
|
2755
|
-
|
|
2756
|
-
if (githubState === 'closed') {
|
|
2828
|
+
if (githubFactoryIssueIsClosed(issue)) {
|
|
2757
2829
|
return false;
|
|
2758
2830
|
}
|
|
2759
2831
|
const labels = new Set(issue.labels.map((label) => label.trim().toLowerCase()));
|
|
@@ -2878,9 +2950,14 @@ export class FactoryLoop {
|
|
|
2878
2950
|
}
|
|
2879
2951
|
}
|
|
2880
2952
|
}
|
|
2953
|
+
for (const [identity, path] of issuePaths) {
|
|
2954
|
+
this.#githubIssuePreferredPaths.set(identity, path);
|
|
2955
|
+
}
|
|
2956
|
+
this.#githubIssuePathIndexReady = true;
|
|
2881
2957
|
return [...issuePaths.values()].sort();
|
|
2882
2958
|
}
|
|
2883
2959
|
catch (error) {
|
|
2960
|
+
this.#githubIssuePathIndexReady = false;
|
|
2884
2961
|
this.#increment('githubIssueListFailures');
|
|
2885
2962
|
this.#logger.warn?.('[factory] failed to list GitHub issue source tree', error);
|
|
2886
2963
|
return [];
|
|
@@ -2945,7 +3022,11 @@ export class FactoryLoop {
|
|
|
2945
3022
|
}
|
|
2946
3023
|
}
|
|
2947
3024
|
async #readGithubIssue(path) {
|
|
2948
|
-
const
|
|
3025
|
+
const preferredPath = await this.#preferredGithubIssuePath(path);
|
|
3026
|
+
const candidatePaths = [...new Set([
|
|
3027
|
+
...githubIssueReadCandidatePaths(preferredPath),
|
|
3028
|
+
...githubIssueReadCandidatePaths(path),
|
|
3029
|
+
])];
|
|
2949
3030
|
try {
|
|
2950
3031
|
for (const candidatePath of candidatePaths) {
|
|
2951
3032
|
try {
|
|
@@ -2969,6 +3050,30 @@ export class FactoryLoop {
|
|
|
2969
3050
|
throw error;
|
|
2970
3051
|
}
|
|
2971
3052
|
}
|
|
3053
|
+
async #preferredGithubIssuePath(path) {
|
|
3054
|
+
const parts = githubIssuePathParts(path) ?? githubIssueDirectoryPathParts(path);
|
|
3055
|
+
if (!parts)
|
|
3056
|
+
return path;
|
|
3057
|
+
const identity = githubIssueIdentity(parts.owner, parts.repo, parts.number);
|
|
3058
|
+
const cached = this.#githubIssuePreferredPaths.get(identity);
|
|
3059
|
+
if (cached && githubIssuePathPreference(cached) <= githubIssuePathPreference(path))
|
|
3060
|
+
return cached;
|
|
3061
|
+
if (githubIssuePathPreference(path) === 0) {
|
|
3062
|
+
this.#githubIssuePreferredPaths.set(identity, path);
|
|
3063
|
+
return path;
|
|
3064
|
+
}
|
|
3065
|
+
// Normal discovery has already indexed every configured GitHub issue path.
|
|
3066
|
+
// A dispatch-only replacement owner can reach this method before that
|
|
3067
|
+
// backfill, so build the same shared index once instead of traversing both
|
|
3068
|
+
// repository trees separately for every durable issue it recovers.
|
|
3069
|
+
if (!this.#githubIssuePathIndexReady) {
|
|
3070
|
+
await this.#githubIssuePaths();
|
|
3071
|
+
}
|
|
3072
|
+
const indexed = this.#githubIssuePreferredPaths.get(identity);
|
|
3073
|
+
return indexed && githubIssuePathPreference(indexed) < githubIssuePathPreference(path)
|
|
3074
|
+
? indexed
|
|
3075
|
+
: path;
|
|
3076
|
+
}
|
|
2972
3077
|
async #findGithubIssueMirror(ghIssue, candidates) {
|
|
2973
3078
|
const draftPath = githubIssueMirrorDraftPath(ghIssue);
|
|
2974
3079
|
try {
|
|
@@ -3494,10 +3599,10 @@ export class FactoryLoop {
|
|
|
3494
3599
|
}
|
|
3495
3600
|
return [...handoffs.values()];
|
|
3496
3601
|
}
|
|
3497
|
-
async #teardownFailedDispatchWorktrees(handoffs) {
|
|
3602
|
+
async #teardownFailedDispatchWorktrees(handoffs, releaseReason = 'dispatch failed') {
|
|
3498
3603
|
if (!this.#worktrees || !handoffs.some((handoff) => handoff.worktree))
|
|
3499
3604
|
return false;
|
|
3500
|
-
const failed = await this.#releaseAndTerminateAgents(handoffs.map((handoff) => [handoff.name, handoff.tracked]),
|
|
3605
|
+
const failed = await this.#releaseAndTerminateAgents(handoffs.map((handoff) => [handoff.name, handoff.tracked]), releaseReason, 'completion');
|
|
3501
3606
|
if (failed.length > 0)
|
|
3502
3607
|
return false;
|
|
3503
3608
|
try {
|
|
@@ -3648,7 +3753,12 @@ export class FactoryLoop {
|
|
|
3648
3753
|
});
|
|
3649
3754
|
}
|
|
3650
3755
|
catch (error) {
|
|
3651
|
-
|
|
3756
|
+
const wrapped = contextualError(`Dispatch spawn failed for ${record.issue.key}/${spec.name} (${spec.capability}) cwd=${spec.clonePath ?? 'default'}`, error);
|
|
3757
|
+
throw Object.assign(wrapped, {
|
|
3758
|
+
factoryCancellationReason: isDispatchDeliveryError(error)
|
|
3759
|
+
? 'agent_delivery_failed'
|
|
3760
|
+
: 'agent_spawn_failed',
|
|
3761
|
+
});
|
|
3652
3762
|
}
|
|
3653
3763
|
batch.recordSpawn(record, spec, invocationId, result);
|
|
3654
3764
|
if (!await this.#saveDispatchLifecycle(record, 'dispatching')) {
|
|
@@ -8710,6 +8820,9 @@ export function isDispatchableIssue(issue) {
|
|
|
8710
8820
|
if (!isGithubIssue(issue)) {
|
|
8711
8821
|
return false;
|
|
8712
8822
|
}
|
|
8823
|
+
if (githubFactoryIssueIsClosed(issue)) {
|
|
8824
|
+
return false;
|
|
8825
|
+
}
|
|
8713
8826
|
const payload = wrappedPayload(issue.raw);
|
|
8714
8827
|
const source = asRecord(payload.source);
|
|
8715
8828
|
const sourceId = stringValue(source?.id);
|
|
@@ -8724,6 +8837,7 @@ const isGithubIssue = (issue) => {
|
|
|
8724
8837
|
return stringValue(source?.provider)?.toLowerCase() === 'github' &&
|
|
8725
8838
|
(stringValue(wrapper?.provider)?.toLowerCase() === 'github' || isGithubIssueFilePath(issue.path));
|
|
8726
8839
|
};
|
|
8840
|
+
const githubFactoryIssueIsClosed = (issue) => (issue.state?.name ?? stringValue(wrappedPayload(issue.raw).state) ?? '').trim().toLowerCase() === 'closed';
|
|
8727
8841
|
const githubIssueSourceRef = (issue) => {
|
|
8728
8842
|
const source = asRecord(wrappedPayload(issue.raw).source);
|
|
8729
8843
|
if (stringValue(source?.provider)?.toLowerCase() !== 'github') {
|
|
@@ -9172,13 +9286,15 @@ const githubIssueRefIdentity = (issue) => {
|
|
|
9172
9286
|
return parts ? githubIssueIdentity(parts.owner, parts.repo, parts.number) : undefined;
|
|
9173
9287
|
};
|
|
9174
9288
|
const githubIssuePathPreference = (path) => {
|
|
9175
|
-
if (path.includes('/issues/by-id/'))
|
|
9176
|
-
return 0;
|
|
9177
9289
|
if (path.endsWith('/meta.json'))
|
|
9290
|
+
return 0;
|
|
9291
|
+
if (path.endsWith('/metadata.json'))
|
|
9178
9292
|
return 1;
|
|
9179
|
-
if (path.
|
|
9293
|
+
if (path.includes('/issues/by-id/'))
|
|
9180
9294
|
return 2;
|
|
9181
|
-
|
|
9295
|
+
if (path.endsWith('.json'))
|
|
9296
|
+
return 3;
|
|
9297
|
+
return 4;
|
|
9182
9298
|
};
|
|
9183
9299
|
const githubAgentNameMatchesIssue = (name, issue) => {
|
|
9184
9300
|
const parts = githubIssuePathParts(issue.path);
|
|
@@ -9494,11 +9610,15 @@ export const githubRepoSubscriptionGlobs = (config) => configuredGithubRepoParts
|
|
|
9494
9610
|
const githubIssueScanRoots = (config) => {
|
|
9495
9611
|
const roots = new Set();
|
|
9496
9612
|
for (const { owner, repo } of configuredGithubRepoParts(config)) {
|
|
9497
|
-
|
|
9498
|
-
|
|
9613
|
+
for (const root of githubIssueRepoRoots(owner, repo))
|
|
9614
|
+
roots.add(root);
|
|
9499
9615
|
}
|
|
9500
9616
|
return [...roots];
|
|
9501
9617
|
};
|
|
9618
|
+
const githubIssueRepoRoots = (owner, repo) => [
|
|
9619
|
+
`${GITHUB_ISSUE_ROOT}/${owner}/${repo}/issues`,
|
|
9620
|
+
`${GITHUB_ISSUE_ROOT}/${owner}__${repo}/issues`,
|
|
9621
|
+
];
|
|
9502
9622
|
const githubRepoPathParts = (path) => {
|
|
9503
9623
|
const compactSegment = path.match(/^\/github\/repos\/([^/]+)\//u)?.[1];
|
|
9504
9624
|
const separator = compactSegment?.indexOf('__') ?? -1;
|
|
@@ -10247,7 +10367,13 @@ const isAgentAlreadyExistsError = (error) => {
|
|
|
10247
10367
|
const defaultRestartPolicy = (spec) =>
|
|
10248
10368
|
// Factory owns durable resume/respawn decisions. Broker-level retries race
|
|
10249
10369
|
// that lifecycle and can re-register the same name before Factory resumes it.
|
|
10250
|
-
|
|
10370
|
+
// The reviewer shares the implementer's dispatch lifecycle: it is spawned in
|
|
10371
|
+
// the same batch, torn down by the same dispatch-failure/teardown paths, and
|
|
10372
|
+
// resumed through the same durable #resumeDurableDispatch flow. Without this
|
|
10373
|
+
// opt-out the broker's default restart policy re-registers a torn-down
|
|
10374
|
+
// reviewer's name as an orphan (relay#1116-family) while the dashboard only
|
|
10375
|
+
// reports dispatch_failed — the exact orphan/restart sequence being audited.
|
|
10376
|
+
spec.role === 'implementer' || spec.role === 'reviewer' || spec.role === 'babysitter'
|
|
10251
10377
|
? { maxRestarts: 0 }
|
|
10252
10378
|
: spec.restartPolicy;
|
|
10253
10379
|
const slackPayloadTs = (threadId) => threadId.replace(/_/g, '.');
|
|
@@ -10315,6 +10441,22 @@ const isRegistrationLagInjectionError = (error) => {
|
|
|
10315
10441
|
return /agent_not_found|recipient unavailable|not registered|unknown recipient|no such (agent|recipient)|timed out waiting for delivery_injected/i
|
|
10316
10442
|
.test(errorMessage);
|
|
10317
10443
|
};
|
|
10444
|
+
const isDispatchDeliveryError = (error) => {
|
|
10445
|
+
if (isRegistrationLagInjectionError(error))
|
|
10446
|
+
return true;
|
|
10447
|
+
const { errorMessage } = describeError(error);
|
|
10448
|
+
return /delivery[_ -]failed|dead-lettered|max delivery retries exceeded/iu.test(errorMessage);
|
|
10449
|
+
};
|
|
10450
|
+
const factoryCloudDispatchCancellationReason = (error) => {
|
|
10451
|
+
if (error instanceof LiveDispatchStateChangedError)
|
|
10452
|
+
return 'source_state_changed';
|
|
10453
|
+
if (error && typeof error === 'object' && 'factoryCancellationReason' in error) {
|
|
10454
|
+
const reason = error.factoryCancellationReason;
|
|
10455
|
+
if (reason === 'agent_spawn_failed' || reason === 'agent_delivery_failed')
|
|
10456
|
+
return reason;
|
|
10457
|
+
}
|
|
10458
|
+
return 'dispatch_failed';
|
|
10459
|
+
};
|
|
10318
10460
|
const telemetryRunStatus = (phase) => {
|
|
10319
10461
|
switch (phase) {
|
|
10320
10462
|
case 'queued':
|