@agent-relay/factory 0.1.44 → 0.1.46
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/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +25 -3
- package/dist/cli/fleet.js.map +1 -1
- package/dist/fleet/internal-fleet-client.d.ts +6 -3
- package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
- package/dist/fleet/internal-fleet-client.js +61 -2
- package/dist/fleet/internal-fleet-client.js.map +1 -1
- package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
- package/dist/fleet/relay-fleet-client.js +18 -18
- package/dist/fleet/relay-fleet-client.js.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.js +17 -7
- package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +82 -2
- package/dist/orchestrator/factory.js.map +1 -1
- package/package.json +1 -1
|
@@ -105,10 +105,19 @@ const GITHUB_FACTORY_LABEL = 'factory';
|
|
|
105
105
|
const GITHUB_LIFECYCLE_LABELS = new Set(['factory:in-progress', 'factory:human-review']);
|
|
106
106
|
const GITHUB_MIRROR_TITLE_PREFIX = '[factory]';
|
|
107
107
|
const GITHUB_MIRROR_SOURCE_PREFIX = 'Source: ';
|
|
108
|
+
const STALE_LOCAL_AGENT_RECLAIM_MAX_ATTEMPTS = 3;
|
|
109
|
+
const STALE_LOCAL_AGENT_RECLAIM_BACKOFF_MS = 500;
|
|
108
110
|
export const DEFAULT_FACTORY_LOOP_HEARTBEAT_PATH = '/tmp/factory-run/factory-loop-heartbeat.json';
|
|
109
111
|
export const DEFAULT_FACTORY_LOOP_REGISTRY_PATH = '/tmp/factory-run/factory-loop-registry.json';
|
|
110
112
|
class DispatchLifecycleCapacityError extends Error {
|
|
111
113
|
}
|
|
114
|
+
class DispatchLifecycleOwnedElsewhereError extends Error {
|
|
115
|
+
leaseUntilMs;
|
|
116
|
+
constructor(leaseUntilMs) {
|
|
117
|
+
super('durable dispatch is still owned by another publisher');
|
|
118
|
+
this.leaseUntilMs = leaseUntilMs;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
112
121
|
const realClock = {
|
|
113
122
|
now: () => Date.now(),
|
|
114
123
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
@@ -199,6 +208,7 @@ export class FactoryLoop {
|
|
|
199
208
|
#dispatchLifecycleRetryTimers = new Map();
|
|
200
209
|
#dispatchLifecycleDrives = new Set();
|
|
201
210
|
#dispatchLifecycleCapacityWaitLogged = new Set();
|
|
211
|
+
#dispatchLifecycleOwnershipWaitLogged = new Set();
|
|
202
212
|
#localReleaseCheckpoints = new Map();
|
|
203
213
|
#dispatchLifecycleRenewTimer;
|
|
204
214
|
#clarificationSweepTimer;
|
|
@@ -598,6 +608,7 @@ export class FactoryLoop {
|
|
|
598
608
|
for (const timer of this.#dispatchLifecycleRetryTimers.values())
|
|
599
609
|
clearTimeout(timer);
|
|
600
610
|
this.#dispatchLifecycleRetryTimers.clear();
|
|
611
|
+
this.#dispatchLifecycleOwnershipWaitLogged.clear();
|
|
601
612
|
if (this.#completionSweepTimer)
|
|
602
613
|
clearTimeout(this.#completionSweepTimer);
|
|
603
614
|
this.#completionSweepTimer = undefined;
|
|
@@ -671,7 +682,16 @@ export class FactoryLoop {
|
|
|
671
682
|
}
|
|
672
683
|
}
|
|
673
684
|
async #releaseOwnedDispatchLifecycleLeases() {
|
|
674
|
-
|
|
685
|
+
// The epoch cache is an execution optimization, not the durable ownership
|
|
686
|
+
// authority. Error/fence paths may evict a cached epoch while its persisted
|
|
687
|
+
// lease is still ours, so enumerate state before shutdown relinquishment.
|
|
688
|
+
const owned = new Map(this.#dispatchLifecycleEpochs);
|
|
689
|
+
for (const [key, lifecycle] of await this.#state.listDispatchLifecycles(this.#workspaceId)) {
|
|
690
|
+
if (lifecycle.lease?.owner === this.#dispatchLifecycleOwner) {
|
|
691
|
+
owned.set(key, lifecycle.lease.epoch);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
for (const [key, epoch] of owned) {
|
|
675
695
|
await this.#state.releaseDispatchLifecycleLease(this.#workspaceId, key, this.#dispatchLifecycleOwner, epoch);
|
|
676
696
|
if (this.#dispatchLifecycleEpochs.get(key) === epoch) {
|
|
677
697
|
this.#dispatchLifecycleEpochs.delete(key);
|
|
@@ -2739,9 +2759,11 @@ export class FactoryLoop {
|
|
|
2739
2759
|
const drive = this.#driveDispatchLifecycle(key)
|
|
2740
2760
|
.then(() => {
|
|
2741
2761
|
this.#dispatchLifecycleCapacityWaitLogged.delete(key);
|
|
2762
|
+
this.#dispatchLifecycleOwnershipWaitLogged.delete(key);
|
|
2742
2763
|
})
|
|
2743
2764
|
.catch((error) => {
|
|
2744
2765
|
if (error instanceof DispatchLifecycleCapacityError) {
|
|
2766
|
+
this.#dispatchLifecycleOwnershipWaitLogged.delete(key);
|
|
2745
2767
|
if (!this.#dispatchLifecycleCapacityWaitLogged.has(key)) {
|
|
2746
2768
|
this.#dispatchLifecycleCapacityWaitLogged.add(key);
|
|
2747
2769
|
this.#increment('dispatchLifecycleCapacityWaits');
|
|
@@ -2751,8 +2773,23 @@ export class FactoryLoop {
|
|
|
2751
2773
|
});
|
|
2752
2774
|
}
|
|
2753
2775
|
}
|
|
2776
|
+
else if (error instanceof DispatchLifecycleOwnedElsewhereError) {
|
|
2777
|
+
this.#dispatchLifecycleCapacityWaitLogged.delete(key);
|
|
2778
|
+
if (!this.#dispatchLifecycleOwnershipWaitLogged.has(key)) {
|
|
2779
|
+
this.#dispatchLifecycleOwnershipWaitLogged.add(key);
|
|
2780
|
+
this.#increment('dispatchLifecycleOwnershipWaits');
|
|
2781
|
+
this.#logger.warn?.('[factory] durable dispatch is leased by another publisher; waiting for lease release', {
|
|
2782
|
+
issue: record.issue.key,
|
|
2783
|
+
leaseRemainingMs: error.leaseUntilMs === undefined
|
|
2784
|
+
? undefined
|
|
2785
|
+
: Math.max(0, error.leaseUntilMs - this.#clock.now()),
|
|
2786
|
+
retryMs: DISPATCH_LIFECYCLE_RETRY_MS,
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2754
2790
|
else {
|
|
2755
2791
|
this.#dispatchLifecycleCapacityWaitLogged.delete(key);
|
|
2792
|
+
this.#dispatchLifecycleOwnershipWaitLogged.delete(key);
|
|
2756
2793
|
this.#logger.warn?.('[factory] durable dispatch lifecycle retry failed', {
|
|
2757
2794
|
issue: record.issue.key,
|
|
2758
2795
|
error: describeError(error).errorMessage,
|
|
@@ -2806,7 +2843,7 @@ export class FactoryLoop {
|
|
|
2806
2843
|
if (!this.#dispatchLifecycleEpochs.has(key)) {
|
|
2807
2844
|
const claim = await this.#state.claimDispatchLifecycle(this.#workspaceId, key, lifecycle, this.#dispatchLifecycleOwner, this.#clock.now(), DISPATCH_LIFECYCLE_LEASE_MS);
|
|
2808
2845
|
if (!claim.acquired || !claim.lease) {
|
|
2809
|
-
throw new
|
|
2846
|
+
throw new DispatchLifecycleOwnedElsewhereError(claim.lifecycle.lease?.leaseUntilMs);
|
|
2810
2847
|
}
|
|
2811
2848
|
this.#dispatchLifecycleEpochs.set(key, claim.lease.epoch);
|
|
2812
2849
|
this.#scheduleDispatchLifecycleRenewal();
|
|
@@ -4678,6 +4715,20 @@ export class FactoryLoop {
|
|
|
4678
4715
|
return;
|
|
4679
4716
|
}
|
|
4680
4717
|
}
|
|
4718
|
+
// The internal broker can retain a historical name after its cloud
|
|
4719
|
+
// presence is authoritatively offline (relay#1116-family). Reclaim that
|
|
4720
|
+
// supported fleet registration before attempting the deterministic
|
|
4721
|
+
// resume/respawn; otherwise recovery immediately collides with the dead
|
|
4722
|
+
// name and concludes useful work as terminal.
|
|
4723
|
+
if (tracingReconciledExit && this.#fleet.placementLocality === 'local') {
|
|
4724
|
+
if (!await this.#reclaimStaleLocalAgentName(record, name)) {
|
|
4725
|
+
// Do not immediately collide with a row the broker just refused to
|
|
4726
|
+
// release. Leave durable ownership intact and let the lifecycle retry
|
|
4727
|
+
// re-run reconciliation after broker pressure subsides.
|
|
4728
|
+
this.#scheduleDispatchLifecycleRetry(record);
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4681
4732
|
let recovered = false;
|
|
4682
4733
|
if (tracked.sessionRef) {
|
|
4683
4734
|
const resumeKey = `${issueKey(record.issue)}:${name}:${tracked.sessionRef}`;
|
|
@@ -4785,6 +4836,35 @@ export class FactoryLoop {
|
|
|
4785
4836
|
this.#error(error, record.issue);
|
|
4786
4837
|
}
|
|
4787
4838
|
}
|
|
4839
|
+
async #reclaimStaleLocalAgentName(record, name) {
|
|
4840
|
+
let lastError;
|
|
4841
|
+
for (let attempt = 1; attempt <= STALE_LOCAL_AGENT_RECLAIM_MAX_ATTEMPTS; attempt += 1) {
|
|
4842
|
+
try {
|
|
4843
|
+
await this.#fleet.release(name, 'reconciled-missing');
|
|
4844
|
+
this.#increment('staleLocalAgentNamesReclaimed');
|
|
4845
|
+
this.#logger.info?.('[factory] reclaimed stale local agent name before recovery', {
|
|
4846
|
+
issue: record.issue.key,
|
|
4847
|
+
name,
|
|
4848
|
+
attempt,
|
|
4849
|
+
});
|
|
4850
|
+
return true;
|
|
4851
|
+
}
|
|
4852
|
+
catch (error) {
|
|
4853
|
+
lastError = error;
|
|
4854
|
+
if (attempt < STALE_LOCAL_AGENT_RECLAIM_MAX_ATTEMPTS) {
|
|
4855
|
+
await this.#clock.sleep(STALE_LOCAL_AGENT_RECLAIM_BACKOFF_MS * attempt);
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
this.#increment('staleLocalAgentNameReclaimFailures');
|
|
4860
|
+
this.#logger.warn?.('[factory] stale local agent name reclaim failed; deferring recovery', {
|
|
4861
|
+
issue: record.issue.key,
|
|
4862
|
+
name,
|
|
4863
|
+
attempts: STALE_LOCAL_AGENT_RECLAIM_MAX_ATTEMPTS,
|
|
4864
|
+
error: lastError,
|
|
4865
|
+
});
|
|
4866
|
+
return false;
|
|
4867
|
+
}
|
|
4788
4868
|
// Publish a PR from the implementer's committed branch when it exited without
|
|
4789
4869
|
// opening one. Best-effort and idempotent: returns undefined when there is no
|
|
4790
4870
|
// clone or nothing publishable (no branch / no commits ahead of base —
|