@agent-relay/factory 0.1.49 → 0.1.50
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":"AAIA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAI1E,OAAO,KAAK,EAMV,WAAW,EASX,WAAW,EAKZ,MAAM,UAAU,CAAA;AAiCjB,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;AA4BjB,KAAK,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,YAAY,GAAG,oBAAoB,GAAG,OAAO,CAAA;AACjG,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;AAoFtD,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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"}
|
|
@@ -90,6 +90,7 @@ const DISPATCH_LIFECYCLE_LEASE_MS = 5 * 60_000;
|
|
|
90
90
|
const DISPATCH_LIFECYCLE_RENEW_MS = 60_000;
|
|
91
91
|
const DISPATCH_LIFECYCLE_RETRY_MS = 1_000;
|
|
92
92
|
const STARTUP_AGENT_EXIT_DRAIN_TIMEOUT_MS = 30_000;
|
|
93
|
+
const RECONCILED_AGENT_EXIT_CONCURRENCY = 4;
|
|
93
94
|
const SLACK_EVENT_WATERMARK_CACHE_MS = 60_000;
|
|
94
95
|
const SLACK_CONVERSATION_TURN_LEASE_MS = 60_000;
|
|
95
96
|
const SLACK_CONVERSATION_TURN_RETRY_MS = 1_000;
|
|
@@ -207,6 +208,7 @@ export class FactoryLoop {
|
|
|
207
208
|
#dispatchTerminalWaiters = new Map();
|
|
208
209
|
#dispatchLifecycleRetryTimers = new Map();
|
|
209
210
|
#dispatchLifecycleDrives = new Set();
|
|
211
|
+
#abandonedDispatchReasons = new Map();
|
|
210
212
|
#dispatchLifecycleCapacityWaitLogged = new Set();
|
|
211
213
|
#dispatchLifecycleOwnershipWaitLogged = new Set();
|
|
212
214
|
#localReleaseCheckpoints = new Map();
|
|
@@ -246,6 +248,8 @@ export class FactoryLoop {
|
|
|
246
248
|
#completionSweepActive = false;
|
|
247
249
|
#completionInFlight = new Set();
|
|
248
250
|
#agentExitsInFlight = new Map();
|
|
251
|
+
#reconciledAgentExitsActive = 0;
|
|
252
|
+
#reconciledAgentExitWaiters = [];
|
|
249
253
|
#agentLifecycleSignalsInFlight = new Map();
|
|
250
254
|
#startupAgentAdoptionActive = false;
|
|
251
255
|
#startupRosterExitSignals;
|
|
@@ -608,6 +612,7 @@ export class FactoryLoop {
|
|
|
608
612
|
for (const timer of this.#dispatchLifecycleRetryTimers.values())
|
|
609
613
|
clearTimeout(timer);
|
|
610
614
|
this.#dispatchLifecycleRetryTimers.clear();
|
|
615
|
+
this.#abandonedDispatchReasons.clear();
|
|
611
616
|
this.#dispatchLifecycleOwnershipWaitLogged.clear();
|
|
612
617
|
if (this.#completionSweepTimer)
|
|
613
618
|
clearTimeout(this.#completionSweepTimer);
|
|
@@ -2341,7 +2346,13 @@ export class FactoryLoop {
|
|
|
2341
2346
|
const previous = this.#agentExitsInFlight.get(name) ?? Promise.resolve();
|
|
2342
2347
|
const handling = previous
|
|
2343
2348
|
.catch(() => undefined)
|
|
2344
|
-
.then(async () =>
|
|
2349
|
+
.then(async () => {
|
|
2350
|
+
if (reason === 'reconciled-missing') {
|
|
2351
|
+
await this.#withReconciledAgentExitSlot(async () => this.#handleAgentExit(name, reason));
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
await this.#handleAgentExit(name, reason);
|
|
2355
|
+
})
|
|
2345
2356
|
.catch((error) => this.#error(error))
|
|
2346
2357
|
.finally(() => {
|
|
2347
2358
|
if (this.#agentExitsInFlight.get(name) === handling) {
|
|
@@ -2448,7 +2459,12 @@ export class FactoryLoop {
|
|
|
2448
2459
|
// the hydrated durable session is no longer usable. Classify it as a
|
|
2449
2460
|
// reconciled miss so remote implementers recover a PR or restart
|
|
2450
2461
|
// instead of waiting forever for branch replication after a crash.
|
|
2451
|
-
|
|
2462
|
+
const rolePriority = (name) => {
|
|
2463
|
+
const role = batch.getIssueByAgent(name)?.agents.get(name)?.spec.role;
|
|
2464
|
+
return role === 'implementer' ? 0 : role === 'babysitter' ? 1 : 2;
|
|
2465
|
+
};
|
|
2466
|
+
const orderedSignals = [...signalled].sort((left, right) => rolePriority(left) - rolePriority(right) || left.localeCompare(right));
|
|
2467
|
+
for (const name of orderedSignals)
|
|
2452
2468
|
this.#queueAgentExit(name, 'reconciled-missing');
|
|
2453
2469
|
if (synthesized.length > 0) {
|
|
2454
2470
|
this.#increment('startupRosterMissingExitsSynthesized', synthesized.length);
|
|
@@ -2566,6 +2582,20 @@ export class FactoryLoop {
|
|
|
2566
2582
|
clearTimeout(timer);
|
|
2567
2583
|
return completed;
|
|
2568
2584
|
}
|
|
2585
|
+
async #withReconciledAgentExitSlot(run) {
|
|
2586
|
+
if (this.#reconciledAgentExitsActive >= RECONCILED_AGENT_EXIT_CONCURRENCY) {
|
|
2587
|
+
this.#increment('reconciledAgentExitBackpressure');
|
|
2588
|
+
await new Promise((resolve) => this.#reconciledAgentExitWaiters.push(resolve));
|
|
2589
|
+
}
|
|
2590
|
+
this.#reconciledAgentExitsActive += 1;
|
|
2591
|
+
try {
|
|
2592
|
+
await run();
|
|
2593
|
+
}
|
|
2594
|
+
finally {
|
|
2595
|
+
this.#reconciledAgentExitsActive -= 1;
|
|
2596
|
+
this.#reconciledAgentExitWaiters.shift()?.();
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2569
2599
|
#scheduleDispatchLifecycleRenewal() {
|
|
2570
2600
|
if (this.#dispatchLifecycleRenewTimer || this.#dispatchLifecycleEpochs.size === 0)
|
|
2571
2601
|
return;
|
|
@@ -2893,6 +2923,11 @@ export class FactoryLoop {
|
|
|
2893
2923
|
return;
|
|
2894
2924
|
if (acquiredNow && this.#config.babysitter.enabled)
|
|
2895
2925
|
await this.#restoreBabysitterOwnership();
|
|
2926
|
+
const abandonedReason = this.#abandonedDispatchReasons.get(key);
|
|
2927
|
+
if (abandonedReason !== undefined) {
|
|
2928
|
+
await this.#abandonStuckDispatch(record, abandonedReason);
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2896
2931
|
if (acquiredNow && lifecycle.phase === 'running') {
|
|
2897
2932
|
if (this.#fleet.hydrateTracked) {
|
|
2898
2933
|
this.#fleet.hydrateTracked(lifecycle.agents.map((agent) => ({
|
|
@@ -4567,6 +4602,7 @@ export class FactoryLoop {
|
|
|
4567
4602
|
if (isCompletionReason(reason)) {
|
|
4568
4603
|
if (exiting?.spec.role === 'implementer' && await this.#issueHasCompletionPr(record, {
|
|
4569
4604
|
openOnly: this.#config.babysitter.enabled,
|
|
4605
|
+
preferExactBranch: tracingReconciledExit,
|
|
4570
4606
|
}, exiting)) {
|
|
4571
4607
|
if (this.#config.babysitter.enabled)
|
|
4572
4608
|
await this.#ensureBabysitterForIssue(record);
|
|
@@ -4624,6 +4660,7 @@ export class FactoryLoop {
|
|
|
4624
4660
|
const hasCompletionPr = tracked.spec.role === 'implementer'
|
|
4625
4661
|
? await this.#issueHasCompletionPr(record, {
|
|
4626
4662
|
openOnly: this.#config.babysitter.enabled,
|
|
4663
|
+
preferExactBranch: tracingReconciledExit,
|
|
4627
4664
|
}, tracked)
|
|
4628
4665
|
: false;
|
|
4629
4666
|
if (tracingReconciledExit) {
|
|
@@ -4729,30 +4766,43 @@ export class FactoryLoop {
|
|
|
4729
4766
|
return;
|
|
4730
4767
|
}
|
|
4731
4768
|
}
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4769
|
+
// Bound recovery by the durable logical agent, not by the session ref.
|
|
4770
|
+
// Harnesses may return a fresh session ref from every successful resume,
|
|
4771
|
+
// and no-sessionRef workers use respawn instead. Keying only resumptions
|
|
4772
|
+
// by the changing ref (and not recording respawns at all) let an agent
|
|
4773
|
+
// that exited immediately consume a batch slot forever while Factory
|
|
4774
|
+
// continuously recreated it. One successful recovery is enough; a
|
|
4775
|
+
// subsequent no-PR implementer exit is terminal and frees the slot.
|
|
4776
|
+
const recoveryLifecycle = await this.#state.getDispatchLifecycle(this.#workspaceId, issueKey(record.issue));
|
|
4777
|
+
const recoveryRunIdentity = recoveryLifecycle?.runId
|
|
4778
|
+
?? tracked.spec.branch
|
|
4779
|
+
?? batch.invocationIdFor(record.issue, tracked.spec);
|
|
4780
|
+
const recoveryKey = `${recoveryRunIdentity}:${tracked.spec.name}`;
|
|
4781
|
+
if (await this.#state.isResumed(this.#workspaceId, recoveryKey)) {
|
|
4782
|
+
if (tracked.spec.role === 'implementer') {
|
|
4783
|
+
await this.#concludeTerminalImplementer(record, name, 'stalled-no-pr');
|
|
4745
4784
|
}
|
|
4746
|
-
|
|
4747
|
-
|
|
4785
|
+
return;
|
|
4786
|
+
}
|
|
4787
|
+
const existing = this.#resumeInFlight.get(recoveryKey);
|
|
4788
|
+
if (existing) {
|
|
4789
|
+
try {
|
|
4748
4790
|
await existing;
|
|
4749
|
-
return;
|
|
4750
4791
|
}
|
|
4792
|
+
catch {
|
|
4793
|
+
// The initiating recovery handler owns classification and logging.
|
|
4794
|
+
// Followers only coalesce onto its lifetime and must not re-report
|
|
4795
|
+
// the same failure.
|
|
4796
|
+
}
|
|
4797
|
+
return;
|
|
4798
|
+
}
|
|
4799
|
+
let recovered = false;
|
|
4800
|
+
if (tracked.sessionRef) {
|
|
4751
4801
|
const resume = this.#resumeTrackedAgent(record, name, tracked);
|
|
4752
|
-
this.#resumeInFlight.set(
|
|
4802
|
+
this.#resumeInFlight.set(recoveryKey, resume);
|
|
4753
4803
|
try {
|
|
4754
4804
|
await resume;
|
|
4755
|
-
await this.#state.markResumed(this.#workspaceId,
|
|
4805
|
+
await this.#state.markResumed(this.#workspaceId, recoveryKey);
|
|
4756
4806
|
recovered = true;
|
|
4757
4807
|
}
|
|
4758
4808
|
catch (error) {
|
|
@@ -4760,11 +4810,11 @@ export class FactoryLoop {
|
|
|
4760
4810
|
// The broker never released this agent's name on exit
|
|
4761
4811
|
// (relay#1116-family), so re-registering collides with the stuck
|
|
4762
4812
|
// name. Retrying just re-collides forever. Treat it as terminal for
|
|
4763
|
-
// this name: record the
|
|
4813
|
+
// this name: record the recovery key so subsequent exit events
|
|
4764
4814
|
// short-circuit, count it, and warn once. The external reaper / a
|
|
4765
4815
|
// broker restart reclaims the leaked name.
|
|
4766
4816
|
this.#fleet.markAgentTerminal?.(name, 'resume-already-exists');
|
|
4767
|
-
await this.#state.markResumed(this.#workspaceId,
|
|
4817
|
+
await this.#state.markResumed(this.#workspaceId, recoveryKey);
|
|
4768
4818
|
this.#increment('resumeNameCollisions');
|
|
4769
4819
|
this.#logger.warn?.('[factory] resume skipped: broker still holds agent name (relay#1116); not retrying', {
|
|
4770
4820
|
issue: record.issue.key,
|
|
@@ -4783,12 +4833,12 @@ export class FactoryLoop {
|
|
|
4783
4833
|
}
|
|
4784
4834
|
}
|
|
4785
4835
|
finally {
|
|
4786
|
-
this.#resumeInFlight.delete(
|
|
4836
|
+
this.#resumeInFlight.delete(recoveryKey);
|
|
4787
4837
|
}
|
|
4788
4838
|
}
|
|
4789
4839
|
else {
|
|
4790
4840
|
const invocationId = `${batch.invocationIdFor(record.issue, tracked.spec)}:restart:${this.#clock.now()}`;
|
|
4791
|
-
|
|
4841
|
+
const respawn = (async () => {
|
|
4792
4842
|
await this.#prepareAgentWorktree(record, tracked.spec);
|
|
4793
4843
|
const result = await this.#fleet.spawn({
|
|
4794
4844
|
name: tracked.spec.name,
|
|
@@ -4804,6 +4854,11 @@ export class FactoryLoop {
|
|
|
4804
4854
|
channel: tracked.spec.channel,
|
|
4805
4855
|
});
|
|
4806
4856
|
batch.recordSpawn(record, tracked.spec, invocationId, result);
|
|
4857
|
+
})();
|
|
4858
|
+
this.#resumeInFlight.set(recoveryKey, respawn);
|
|
4859
|
+
try {
|
|
4860
|
+
await respawn;
|
|
4861
|
+
await this.#state.markResumed(this.#workspaceId, recoveryKey);
|
|
4807
4862
|
recovered = true;
|
|
4808
4863
|
}
|
|
4809
4864
|
catch (error) {
|
|
@@ -4817,6 +4872,7 @@ export class FactoryLoop {
|
|
|
4817
4872
|
// the dispatch so a reviewer blocked on the implementer's DM does not
|
|
4818
4873
|
// hang the owned-broker dispose-wait (#67).
|
|
4819
4874
|
this.#fleet.markAgentTerminal?.(name, 'respawn-already-exists');
|
|
4875
|
+
await this.#state.markResumed(this.#workspaceId, recoveryKey);
|
|
4820
4876
|
this.#increment('resumeNameCollisions');
|
|
4821
4877
|
this.#logger.warn?.('[factory] respawn skipped: broker still holds agent name (relay#1116); not retrying', {
|
|
4822
4878
|
issue: record.issue.key,
|
|
@@ -4826,6 +4882,9 @@ export class FactoryLoop {
|
|
|
4826
4882
|
await this.#concludeTerminalImplementer(record, name, 'respawn-already-exists');
|
|
4827
4883
|
}
|
|
4828
4884
|
}
|
|
4885
|
+
finally {
|
|
4886
|
+
this.#resumeInFlight.delete(recoveryKey);
|
|
4887
|
+
}
|
|
4829
4888
|
}
|
|
4830
4889
|
if (recovered && tracked.spec.role === 'implementer') {
|
|
4831
4890
|
await this.#writeInFlightRegistry();
|
|
@@ -5302,6 +5361,8 @@ export class FactoryLoop {
|
|
|
5302
5361
|
// the release-driven exit event so it cannot re-trigger a resume before the
|
|
5303
5362
|
// record leaves the batch.
|
|
5304
5363
|
async #abandonStuckDispatch(record, reason) {
|
|
5364
|
+
const key = issueKey(record.issue);
|
|
5365
|
+
this.#abandonedDispatchReasons.set(key, reason);
|
|
5305
5366
|
const agents = [...record.agents];
|
|
5306
5367
|
for (const [agentName, tracked] of agents) {
|
|
5307
5368
|
if (tracked.spec.role === 'implementer')
|
|
@@ -5334,6 +5395,22 @@ export class FactoryLoop {
|
|
|
5334
5395
|
await this.#writeInFlightRegistry();
|
|
5335
5396
|
return;
|
|
5336
5397
|
}
|
|
5398
|
+
// Batch completion alone only frees the process-local slot. Durable
|
|
5399
|
+
// capacity is computed from lifecycle phases, so leaving this row in
|
|
5400
|
+
// `publishing`/`running` makes every queued issue wait forever even though
|
|
5401
|
+
// all agents and worktrees are already gone. Fence the terminal phase
|
|
5402
|
+
// before promoting the next issue.
|
|
5403
|
+
if (!await this.#saveDispatchLifecycle(record, 'abandoned', undefined, reason)) {
|
|
5404
|
+
this.#increment('abandonedDispatchReleaseRetries');
|
|
5405
|
+
// #saveDispatchLifecycle already schedules the generic durable retry. The
|
|
5406
|
+
// pending reason makes that retry re-acquire ownership and return here,
|
|
5407
|
+
// rather than merely restoring the old running lifecycle and leaking the
|
|
5408
|
+
// slot. The abandoned-specific scheduler remains the non-durable fallback.
|
|
5409
|
+
this.#scheduleAbandonedDispatchRetry(record, reason);
|
|
5410
|
+
await this.#writeInFlightRegistry();
|
|
5411
|
+
return;
|
|
5412
|
+
}
|
|
5413
|
+
this.#abandonedDispatchReasons.delete(key);
|
|
5337
5414
|
await this.#recordDispatchTerminal(record.issue);
|
|
5338
5415
|
const next = (await this.#batch()).complete(record.issue);
|
|
5339
5416
|
await this.#drainReadyClarificationWake();
|
|
@@ -5367,7 +5444,12 @@ export class FactoryLoop {
|
|
|
5367
5444
|
if (!issue) {
|
|
5368
5445
|
return false;
|
|
5369
5446
|
}
|
|
5370
|
-
|
|
5447
|
+
// During startup recovery, the implementer's deterministic branch is the
|
|
5448
|
+
// strongest and cheapest lookup. Avoid scanning mounted PR metadata across
|
|
5449
|
+
// every configured repository. Normal event-driven exits keep using the
|
|
5450
|
+
// webhook-fed mount so they do not introduce a GitHub API dependency.
|
|
5451
|
+
if (implementer?.spec.branch &&
|
|
5452
|
+
(opts.preferExactBranch || record.decision.implementers.length > 1)) {
|
|
5371
5453
|
const sourceOwner = record.issue.path
|
|
5372
5454
|
? githubIssuePathParts(record.issue.path)?.owner
|
|
5373
5455
|
: undefined;
|