@agent-relay/factory 0.1.39 → 0.1.41
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/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +142 -27
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/ports/state.d.ts +1 -0
- package/dist/ports/state.d.ts.map +1 -1
- package/dist/state/file-state-store.d.ts +1 -0
- package/dist/state/file-state-store.d.ts.map +1 -1
- package/dist/state/file-state-store.js +18 -0
- package/dist/state/file-state-store.js.map +1 -1
- package/dist/state/in-memory-state-store.d.ts +1 -0
- package/dist/state/in-memory-state-store.d.ts.map +1 -1
- package/dist/state/in-memory-state-store.js +12 -0
- package/dist/state/in-memory-state-store.js.map +1 -1
- package/package.json +1 -1
|
@@ -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;AA8FD,eAAO,MAAM,mCAAmC,iDAAiD,CAAA;AACjG,eAAO,MAAM,kCAAkC,gDAAgD,CAAA;AAQ/F,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAEjF;AAED,qBAAa,WAAY,YAAW,OAAO;;
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/orchestrator/factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -238,6 +238,7 @@ export class FactoryLoop {
|
|
|
238
238
|
#agentExitsInFlight = new Map();
|
|
239
239
|
#agentLifecycleSignalsInFlight = new Map();
|
|
240
240
|
#startupAgentAdoptionActive = false;
|
|
241
|
+
#startupRosterExitSignals;
|
|
241
242
|
// Composite issue + PR identities for which a babysitter has already been spawned, so repeated
|
|
242
243
|
// webhooks / agent-exit safety nets don't respawn it while multi-repository issues retain one
|
|
243
244
|
// owner per PR.
|
|
@@ -2275,20 +2276,7 @@ export class FactoryLoop {
|
|
|
2275
2276
|
// after durable records are restored and is the authoritative signal.
|
|
2276
2277
|
if (this.#startupAgentAdoptionActive)
|
|
2277
2278
|
return;
|
|
2278
|
-
|
|
2279
|
-
// is installed, before durable agents are restored. Queue a later
|
|
2280
|
-
// roster-reconciled exit behind it instead of dropping the newer event.
|
|
2281
|
-
const previous = this.#agentExitsInFlight.get(name) ?? Promise.resolve();
|
|
2282
|
-
const handling = previous
|
|
2283
|
-
.catch(() => undefined)
|
|
2284
|
-
.then(async () => await this.#handleAgentExit(name, reason))
|
|
2285
|
-
.catch((error) => this.#error(error))
|
|
2286
|
-
.finally(() => {
|
|
2287
|
-
if (this.#agentExitsInFlight.get(name) === handling) {
|
|
2288
|
-
this.#agentExitsInFlight.delete(name);
|
|
2289
|
-
}
|
|
2290
|
-
});
|
|
2291
|
-
this.#agentExitsInFlight.set(name, handling);
|
|
2279
|
+
this.#queueAgentExit(name, reason);
|
|
2292
2280
|
});
|
|
2293
2281
|
}
|
|
2294
2282
|
if (!this.#offDeliveryFailed) {
|
|
@@ -2317,6 +2305,23 @@ export class FactoryLoop {
|
|
|
2317
2305
|
});
|
|
2318
2306
|
}
|
|
2319
2307
|
}
|
|
2308
|
+
#queueAgentExit(name, reason) {
|
|
2309
|
+
this.#startupRosterExitSignals?.add(name);
|
|
2310
|
+
// Broker replay can deliver an old exit immediately when the listener is
|
|
2311
|
+
// installed, before durable agents are restored. Queue a later
|
|
2312
|
+
// roster-reconciled exit behind it instead of dropping the newer event.
|
|
2313
|
+
const previous = this.#agentExitsInFlight.get(name) ?? Promise.resolve();
|
|
2314
|
+
const handling = previous
|
|
2315
|
+
.catch(() => undefined)
|
|
2316
|
+
.then(async () => await this.#handleAgentExit(name, reason))
|
|
2317
|
+
.catch((error) => this.#error(error))
|
|
2318
|
+
.finally(() => {
|
|
2319
|
+
if (this.#agentExitsInFlight.get(name) === handling) {
|
|
2320
|
+
this.#agentExitsInFlight.delete(name);
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
this.#agentExitsInFlight.set(name, handling);
|
|
2324
|
+
}
|
|
2320
2325
|
// Durable backends survive orchestrator restarts: re-adopt the agents recorded
|
|
2321
2326
|
// in the durable lifecycle store, restore their full batch/spec association,
|
|
2322
2327
|
// then reconcile once so exits that happened while this process was down are
|
|
@@ -2325,8 +2330,9 @@ export class FactoryLoop {
|
|
|
2325
2330
|
try {
|
|
2326
2331
|
const batch = await this.#batch();
|
|
2327
2332
|
const agents = [];
|
|
2333
|
+
const durableAgentNames = new Set();
|
|
2328
2334
|
let hasNonterminalDurableLifecycle = false;
|
|
2329
|
-
const durableLifecycles = await this.#state.listDispatchLifecycles(this.#workspaceId);
|
|
2335
|
+
const durableLifecycles = await this.#deduplicateQueuedGithubLifecycleAliases(await this.#state.listDispatchLifecycles(this.#workspaceId));
|
|
2330
2336
|
this.#logger.info?.('[factory] durable startup adoption loaded', {
|
|
2331
2337
|
lifecycles: durableLifecycles.length,
|
|
2332
2338
|
});
|
|
@@ -2363,6 +2369,7 @@ export class FactoryLoop {
|
|
|
2363
2369
|
const node = agent.tracked.result?.node;
|
|
2364
2370
|
if (invocationId || node || agent.tracked.result) {
|
|
2365
2371
|
agents.push({ name: agent.name, invocationId, node });
|
|
2372
|
+
durableAgentNames.add(agent.name);
|
|
2366
2373
|
}
|
|
2367
2374
|
}
|
|
2368
2375
|
}
|
|
@@ -2392,7 +2399,27 @@ export class FactoryLoop {
|
|
|
2392
2399
|
this.#logger.info?.('[factory] durable startup roster reconciliation started', {
|
|
2393
2400
|
agents: agents.map((agent) => agent.name),
|
|
2394
2401
|
});
|
|
2395
|
-
|
|
2402
|
+
this.#startupRosterExitSignals = new Set();
|
|
2403
|
+
try {
|
|
2404
|
+
await this.#fleet.reconcileTrackedAgents?.();
|
|
2405
|
+
const signalled = this.#startupRosterExitSignals;
|
|
2406
|
+
const online = new Set((await this.#fleet.roster()).agents.map((agent) => agent.name));
|
|
2407
|
+
const missing = [...durableAgentNames]
|
|
2408
|
+
.filter((name) => !online.has(name) && !signalled?.has(name));
|
|
2409
|
+
for (const name of missing) {
|
|
2410
|
+
this.#fleet.markAgentTerminal?.(name, 'reconciled-missing');
|
|
2411
|
+
this.#queueAgentExit(name, 'reconciled-missing');
|
|
2412
|
+
}
|
|
2413
|
+
if (missing.length > 0) {
|
|
2414
|
+
this.#increment('startupRosterMissingExitsSynthesized', missing.length);
|
|
2415
|
+
this.#logger.info?.('[factory] synthesized missing durable startup roster exits', {
|
|
2416
|
+
agents: missing,
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
finally {
|
|
2421
|
+
this.#startupRosterExitSignals = undefined;
|
|
2422
|
+
}
|
|
2396
2423
|
this.#logger.info?.('[factory] durable startup roster reconciliation completed', {
|
|
2397
2424
|
pendingExits: [...this.#agentExitsInFlight.keys()].filter((name) => agents.some((agent) => agent.name === name)),
|
|
2398
2425
|
});
|
|
@@ -2418,6 +2445,64 @@ export class FactoryLoop {
|
|
|
2418
2445
|
this.#logger.warn?.('[factory] failed to re-adopt durable in-flight agents', { error });
|
|
2419
2446
|
}
|
|
2420
2447
|
}
|
|
2448
|
+
async #deduplicateQueuedGithubLifecycleAliases(lifecycles) {
|
|
2449
|
+
const groups = new Map();
|
|
2450
|
+
for (const entry of lifecycles) {
|
|
2451
|
+
if (isTerminalDispatchLifecycle(entry[1]))
|
|
2452
|
+
continue;
|
|
2453
|
+
const identity = githubIssueRefIdentity(entry[1].issue);
|
|
2454
|
+
if (!identity)
|
|
2455
|
+
continue;
|
|
2456
|
+
const group = groups.get(identity) ?? [];
|
|
2457
|
+
group.push(entry);
|
|
2458
|
+
groups.set(identity, group);
|
|
2459
|
+
}
|
|
2460
|
+
const clearedKeys = new Set();
|
|
2461
|
+
for (const [identity, group] of groups) {
|
|
2462
|
+
if (group.length < 2)
|
|
2463
|
+
continue;
|
|
2464
|
+
const active = group.filter(([, lifecycle]) => lifecycle.phase !== 'queued');
|
|
2465
|
+
// Two independently active aliases may each own useful work. Preserve
|
|
2466
|
+
// both for operator reconciliation instead of guessing which branch wins.
|
|
2467
|
+
if (active.length > 1) {
|
|
2468
|
+
this.#increment('dispatchLifecycleGithubAliasConflicts');
|
|
2469
|
+
this.#logger.warn?.('[factory] retained conflicting active GitHub lifecycle aliases', {
|
|
2470
|
+
identity,
|
|
2471
|
+
count: group.length,
|
|
2472
|
+
});
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
const nowMs = this.#clock.now();
|
|
2476
|
+
const ownedElsewhere = group.some(([, lifecycle]) => lifecycle.lease &&
|
|
2477
|
+
lifecycle.lease.owner !== this.#dispatchLifecycleOwner &&
|
|
2478
|
+
lifecycle.lease.leaseUntilMs > nowMs);
|
|
2479
|
+
if (ownedElsewhere) {
|
|
2480
|
+
this.#increment('dispatchLifecycleGithubAliasDedupeDeferred');
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
const winner = active[0] ?? [...group].sort(compareQueuedGithubLifecycleAliases)[0];
|
|
2484
|
+
for (const [key, lifecycle] of group) {
|
|
2485
|
+
if (key === winner[0] || lifecycle.phase !== 'queued')
|
|
2486
|
+
continue;
|
|
2487
|
+
const cleared = await this.#state.clearQueuedDispatchLifecycle(this.#workspaceId, key, lifecycle.lease);
|
|
2488
|
+
if (!cleared) {
|
|
2489
|
+
this.#increment('dispatchLifecycleGithubAliasDedupeDeferred');
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
this.#dispatchLifecycleEpochs.delete(key);
|
|
2493
|
+
const timer = this.#dispatchLifecycleRetryTimers.get(key);
|
|
2494
|
+
if (timer)
|
|
2495
|
+
clearTimeout(timer);
|
|
2496
|
+
this.#dispatchLifecycleRetryTimers.delete(key);
|
|
2497
|
+
this.#dispatchLifecycleCapacityWaitLogged.delete(key);
|
|
2498
|
+
clearedKeys.add(key);
|
|
2499
|
+
this.#increment('dispatchLifecycleGithubAliasesCollapsed');
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
return clearedKeys.size > 0
|
|
2503
|
+
? lifecycles.filter(([key]) => !clearedKeys.has(key))
|
|
2504
|
+
: lifecycles;
|
|
2505
|
+
}
|
|
2421
2506
|
async #drainAgentExitsInFlight(names, timeoutMs) {
|
|
2422
2507
|
const drain = async () => {
|
|
2423
2508
|
for (;;) {
|
|
@@ -4567,11 +4652,17 @@ export class FactoryLoop {
|
|
|
4567
4652
|
}
|
|
4568
4653
|
return;
|
|
4569
4654
|
}
|
|
4570
|
-
|
|
4655
|
+
// A normal remote exit can race branch replication, so leave it in the
|
|
4656
|
+
// publishing retry loop briefly. Startup reconciliation is different:
|
|
4657
|
+
// the live roster has already proved the hydrated session is gone. If
|
|
4658
|
+
// its retained branch still has nothing publishable, restart the saved
|
|
4659
|
+
// worker instead of consuming an implementation slot forever.
|
|
4660
|
+
if (!tracingReconciledExit && tracked.result?.locality === 'remote' && tracked.spec.branch) {
|
|
4571
4661
|
this.#scheduleDispatchLifecycleRetry(record);
|
|
4572
4662
|
return;
|
|
4573
4663
|
}
|
|
4574
4664
|
}
|
|
4665
|
+
let recovered = false;
|
|
4575
4666
|
if (tracked.sessionRef) {
|
|
4576
4667
|
const resumeKey = `${issueKey(record.issue)}:${name}:${tracked.sessionRef}`;
|
|
4577
4668
|
if (await this.#state.isResumed(this.#workspaceId, resumeKey)) {
|
|
@@ -4595,6 +4686,7 @@ export class FactoryLoop {
|
|
|
4595
4686
|
try {
|
|
4596
4687
|
await resume;
|
|
4597
4688
|
await this.#state.markResumed(this.#workspaceId, resumeKey);
|
|
4689
|
+
recovered = true;
|
|
4598
4690
|
}
|
|
4599
4691
|
catch (error) {
|
|
4600
4692
|
if (isAgentAlreadyExistsError(error)) {
|
|
@@ -4645,6 +4737,7 @@ export class FactoryLoop {
|
|
|
4645
4737
|
channel: tracked.spec.channel,
|
|
4646
4738
|
});
|
|
4647
4739
|
batch.recordSpawn(record, tracked.spec, invocationId, result);
|
|
4740
|
+
recovered = true;
|
|
4648
4741
|
}
|
|
4649
4742
|
catch (error) {
|
|
4650
4743
|
if (!isAgentAlreadyExistsError(error)) {
|
|
@@ -4667,6 +4760,10 @@ export class FactoryLoop {
|
|
|
4667
4760
|
}
|
|
4668
4761
|
}
|
|
4669
4762
|
}
|
|
4763
|
+
if (recovered && tracked.spec.role === 'implementer') {
|
|
4764
|
+
await this.#writeInFlightRegistry();
|
|
4765
|
+
await this.#saveDispatchLifecycle(record, 'running');
|
|
4766
|
+
}
|
|
4670
4767
|
}
|
|
4671
4768
|
catch (error) {
|
|
4672
4769
|
this.#error(error, record.issue);
|
|
@@ -10982,24 +11079,42 @@ const normalizeGithubIssueCommentWatch = (watch) => ({
|
|
|
10982
11079
|
processedCommentIds: [...new Set(watch.processedCommentIds ?? [])],
|
|
10983
11080
|
});
|
|
10984
11081
|
const githubIssueReadCandidatePaths = (path) => {
|
|
11082
|
+
const candidates = [];
|
|
10985
11083
|
if (path.endsWith('/')) {
|
|
10986
|
-
|
|
11084
|
+
candidates.push(`${path}meta.json`, `${path}metadata.json`);
|
|
10987
11085
|
}
|
|
10988
|
-
if (path.endsWith('/meta.json')) {
|
|
10989
|
-
|
|
11086
|
+
else if (path.endsWith('/meta.json')) {
|
|
11087
|
+
candidates.push(path, path.replace(/\/meta\.json$/u, '/metadata.json'));
|
|
10990
11088
|
}
|
|
10991
|
-
if (path.endsWith('/metadata.json')) {
|
|
10992
|
-
|
|
11089
|
+
else if (path.endsWith('/metadata.json')) {
|
|
11090
|
+
candidates.push(path, path.replace(/\/metadata\.json$/u, '/meta.json'));
|
|
10993
11091
|
}
|
|
10994
|
-
if (githubIssuePathParts(path)) {
|
|
10995
|
-
|
|
11092
|
+
else if (githubIssuePathParts(path)) {
|
|
11093
|
+
candidates.push(path);
|
|
10996
11094
|
}
|
|
10997
|
-
if (githubIssueDirectoryPathParts(path)) {
|
|
11095
|
+
else if (githubIssueDirectoryPathParts(path)) {
|
|
10998
11096
|
// meta.json is the canonical relayfile GitHub issue basename. metadata.json
|
|
10999
11097
|
// remains a legacy read fallback for older local mount-state snapshots.
|
|
11000
|
-
|
|
11098
|
+
candidates.push(`${path}/meta.json`, `${path}/metadata.json`);
|
|
11099
|
+
}
|
|
11100
|
+
else {
|
|
11101
|
+
candidates.push(path);
|
|
11001
11102
|
}
|
|
11002
|
-
|
|
11103
|
+
// Title-derived directories are mutable aliases. A renamed issue may leave
|
|
11104
|
+
// a durable lifecycle pointing at its old slug, while the adapter continues
|
|
11105
|
+
// to expose the stable public by-id aliases. Always include both supported
|
|
11106
|
+
// repository layouts so recovery never hot-loops on an obsolete title path.
|
|
11107
|
+
const parts = githubIssuePathParts(path) ?? githubIssueDirectoryPathParts(path);
|
|
11108
|
+
if (parts) {
|
|
11109
|
+
candidates.push(`${GITHUB_ISSUE_ROOT}/${parts.owner}__${parts.repo}/issues/by-id/${parts.number}.json`, `${GITHUB_ISSUE_ROOT}/${parts.owner}/${parts.repo}/issues/by-id/${parts.number}.json`);
|
|
11110
|
+
}
|
|
11111
|
+
return [...new Set(candidates)];
|
|
11112
|
+
};
|
|
11113
|
+
const compareQueuedGithubLifecycleAliases = (left, right) => {
|
|
11114
|
+
const stablePath = (lifecycle) => lifecycle.issue.path.includes('/issues/by-id/') ? 0 : 1;
|
|
11115
|
+
return stablePath(left[1]) - stablePath(right[1]) ||
|
|
11116
|
+
(right[1].updatedAtMs ?? 0) - (left[1].updatedAtMs ?? 0) ||
|
|
11117
|
+
left[0].localeCompare(right[0]);
|
|
11003
11118
|
};
|
|
11004
11119
|
const githubIssueDirectoryPathParts = (path) => {
|
|
11005
11120
|
const match = path.match(/^\/github\/repos\/(?:([^/]+)\/([^/]+)|([A-Za-z0-9-]+)__([^/]+))\/issues\/(\d+)(?:__([^/]+))?$/u);
|