@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.
@@ -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;;gBA+J7B,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY;IAkIhD,uBAAuB,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAiEvD,KAAK,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAsHpD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwIrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8mBxB,OAAO,CAAC,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IAkoBlE,OAAO,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAyDrE,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC;IAOxD,QAAQ,CAAC,QAAQ,EAAE,cAAc,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,cAAc,CAAC;IAsT7H,MAAM,IAAI,aAAa;IAiBvB,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI;CAg5QxD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,WAAW,CAiC5E;AAyBD,wBAAsB,oCAAoC,CACxD,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,EACpC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,WAAW,CAAC,CAetB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,iBAAiB,CA6BlF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,WAAW,CAEnF;AAoDD,wBAAsB,wBAAwB,CAC5C,IAAI,SAAsC,GACzC,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAM3C;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,oBAAoB,GAAG,SAAS,EAC3C,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,mBAAmB,CAgBrB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAO7D;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAiB/D;AAglBD,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAwBpH,CAAA;AAoYD,eAAO,MAAM,2BAA2B,GAAI,QAAQ,aAAa,KAAG,MAAM,EAItE,CAAA;AA2mBJ,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,MAC4B,CAAA;AAkUvE,eAAO,MAAM,eAAe,GAAI,OAAO,WAAW,KAAG,MAAM,GAAG,SAG7D,CAAA"}
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
- // Broker replay can deliver an old exit immediately when the listener
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
- await this.#fleet.reconcileTrackedAgents?.();
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
- if (tracked.result?.locality === 'remote' && tracked.spec.branch) {
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
- return [`${path}meta.json`, `${path}metadata.json`];
11084
+ candidates.push(`${path}meta.json`, `${path}metadata.json`);
10987
11085
  }
10988
- if (path.endsWith('/meta.json')) {
10989
- return [path, path.replace(/\/meta\.json$/u, '/metadata.json')];
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
- return [path, path.replace(/\/metadata\.json$/u, '/meta.json')];
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
- return [path];
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
- return [`${path}/meta.json`, `${path}/metadata.json`];
11098
+ candidates.push(`${path}/meta.json`, `${path}/metadata.json`);
11099
+ }
11100
+ else {
11101
+ candidates.push(path);
11001
11102
  }
11002
- return [path];
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);