@agent-relay/factory 0.1.69 → 0.1.71
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 +31 -0
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +62 -3
- package/dist/cli/fleet.js.map +1 -1
- package/dist/config/schema.d.ts +84 -40
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +14 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/environments/connection-registry.d.ts +20 -20
- package/dist/fleet/create-fleet.d.ts +8 -1
- package/dist/fleet/create-fleet.d.ts.map +1 -1
- package/dist/fleet/create-fleet.js +3 -1
- package/dist/fleet/create-fleet.js.map +1 -1
- package/dist/fleet/relay-fleet-client.d.ts +29 -0
- package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
- package/dist/fleet/relay-fleet-client.js +82 -19
- package/dist/fleet/relay-fleet-client.js.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts +10 -2
- package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.js +90 -14
- package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
- package/dist/mount/relayfile-operation-timeout.d.ts +97 -0
- package/dist/mount/relayfile-operation-timeout.d.ts.map +1 -0
- package/dist/mount/relayfile-operation-timeout.js +149 -0
- package/dist/mount/relayfile-operation-timeout.js.map +1 -0
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +163 -18
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
4
4
|
import { DEFAULT_READINESS_RECONCILE_TIMEOUT_MS, FactoryConfigSchema } from '../config/schema.js';
|
|
5
|
+
import { DEFAULT_RELAYFILE_OPERATION_TIMEOUT_MS, RelayfileOperationTimeoutError, relayfileTimeoutWithPhase, withRelayfileCallDeadline, } from '../mount/relayfile-operation-timeout.js';
|
|
5
6
|
import { linearByStatePath, linearByIdPath, linearByUuidPath } from '../constants/linear.js';
|
|
6
7
|
import { stateResolutionFromIds } from '../linear/state-resolver.js';
|
|
7
8
|
import { GithubMergeGate, closeProbePr } from '../github/index.js';
|
|
@@ -204,9 +205,25 @@ const DISPATCH_FAILURE_HANDOFF_UNRESOLVED_TTL_MS = 5 * 60_000;
|
|
|
204
205
|
const DEFAULT_LIVE_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
205
206
|
const REMOTE_OPERATION_PROGRESS_INTERVAL_MS = 15_000;
|
|
206
207
|
const REMOTE_OPERATION_SLOW_WARN_MS = 30_000;
|
|
208
|
+
/** How far the cycle-level relayfile backstop sits above the transport deadline. */
|
|
209
|
+
const RELAYFILE_OPERATION_BACKSTOP_RATIO = 1.25;
|
|
207
210
|
const DISCOVERY_SWEEP_LEASE_MS = 5 * 60_000;
|
|
208
211
|
const DISCOVERY_SWEEP_RENEW_MS = 30_000;
|
|
209
212
|
const READINESS_RECONCILE_FAILURE_THRESHOLD = 3;
|
|
213
|
+
/**
|
|
214
|
+
* A relayfile fault a swallowing catch must not turn into "no result".
|
|
215
|
+
*
|
|
216
|
+
* A 429 already escaped every one of these (#297): it is a fact about the
|
|
217
|
+
* dependency, not about the one item being read, so folding it into an empty
|
|
218
|
+
* result reports a clean pass over work that was never served.
|
|
219
|
+
*
|
|
220
|
+
* A per-call timeout (#351) is the same fact in a worse costume. The read that
|
|
221
|
+
* hung will hang for the next item too, and `#githubIssuePaths` swallowing it
|
|
222
|
+
* turns a wedged dependency into a *successful* sweep that discovered zero
|
|
223
|
+
* issues — `consecutiveFailures: 0`, no `lastError`, nothing dispatched. That
|
|
224
|
+
* is the exact silence this bound exists to remove, so it escapes here too.
|
|
225
|
+
*/
|
|
226
|
+
const isPassWideRelayfileFault = (error) => Boolean(relayfileOverload(error)) || error instanceof RelayfileOperationTimeoutError;
|
|
210
227
|
const DISCOVERY_CHANGE_EVENT_LIMIT = 1_000;
|
|
211
228
|
const DISCOVERY_OVERLOAD_BACKOFF_MAX_MS = 5 * 60_000;
|
|
212
229
|
/** First rung of the ladder when the 429 advertises no `Retry-After`. */
|
|
@@ -484,11 +501,22 @@ export class FactoryLoop {
|
|
|
484
501
|
#liveHeartbeatInFlight = false;
|
|
485
502
|
#liveHeartbeatRefresh;
|
|
486
503
|
#liveHeartbeatLastWriteMs = 0;
|
|
504
|
+
#heartbeatStartedAtMs;
|
|
505
|
+
#progressSequence = 0;
|
|
506
|
+
#lastProgressAtMs;
|
|
487
507
|
#stoppingHeartbeatRefreshActive = false;
|
|
488
508
|
#readinessReconcileTimer;
|
|
489
509
|
#readinessReconcileInFlight;
|
|
490
510
|
#readinessReconcileIntervalMs = 60_000;
|
|
491
511
|
#readinessReconcileTimeoutMs = DEFAULT_READINESS_RECONCILE_TIMEOUT_MS;
|
|
512
|
+
/**
|
|
513
|
+
* Deadline for ONE relayfile call (#351).
|
|
514
|
+
*
|
|
515
|
+
* `#readinessReconcileTimeoutMs` bounds the sweep; this bounds the calls
|
|
516
|
+
* inside it. Only the second one can reach a dependency call that never
|
|
517
|
+
* returns: a deadline checked between awaits never regains control to check.
|
|
518
|
+
*/
|
|
519
|
+
#relayfileOperationTimeoutMs = DEFAULT_RELAYFILE_OPERATION_TIMEOUT_MS;
|
|
492
520
|
// Set for exactly as long as a sweep is running. `state` is derived from
|
|
493
521
|
// this, so an in-flight pass can no longer masquerade as the last settled one.
|
|
494
522
|
#readinessReconcileInFlightSinceMs;
|
|
@@ -645,8 +673,13 @@ export class FactoryLoop {
|
|
|
645
673
|
#started = false;
|
|
646
674
|
#startMode;
|
|
647
675
|
#stopping = false;
|
|
676
|
+
#readOnly;
|
|
648
677
|
constructor(config, ports) {
|
|
678
|
+
this.#readOnly = ports.readOnly ?? false;
|
|
649
679
|
this.#config = config;
|
|
680
|
+
// Also read here, not only in `#startLiveSubscription`: a standalone
|
|
681
|
+
// `runOnce()` never starts the live subscription and must still be bounded.
|
|
682
|
+
this.#relayfileOperationTimeoutMs = config.liveSubscription.relayfileOperationTimeoutMs;
|
|
650
683
|
this.#mount = ports.mount;
|
|
651
684
|
// Resolved role<->state mapping. The CLI injects a name-resolved, per-team
|
|
652
685
|
// resolution via ports; fall back to one built from explicit stateIds plus
|
|
@@ -731,7 +764,14 @@ export class FactoryLoop {
|
|
|
731
764
|
}
|
|
732
765
|
},
|
|
733
766
|
});
|
|
734
|
-
|
|
767
|
+
// Read-only commands consume no fleet events, and subscribing is not free:
|
|
768
|
+
// the relay client mints this process's workspace identity to open the
|
|
769
|
+
// socket. Wiring it here unconditionally is what made `factory status`
|
|
770
|
+
// register an agent it then abandoned before presence (factory-cloud#55).
|
|
771
|
+
// The live paths wire in `#start()` instead, where the subscription is
|
|
772
|
+
// actually used and the identity is meant to persist.
|
|
773
|
+
if (!this.#readOnly)
|
|
774
|
+
this.#wireFleetEvents();
|
|
735
775
|
}
|
|
736
776
|
async #resolveIntegrationInstructions() {
|
|
737
777
|
if (this.#integrationInstructionsRefresh) {
|
|
@@ -895,6 +935,13 @@ export class FactoryLoop {
|
|
|
895
935
|
return observedPhase;
|
|
896
936
|
}
|
|
897
937
|
async start(opts = {}) {
|
|
938
|
+
// A read-only instance was built with no fleet event wiring and, in the
|
|
939
|
+
// CLI, with a fleet client that cannot register. Starting one would be a
|
|
940
|
+
// half-live daemon; refuse rather than let a future caller reintroduce the
|
|
941
|
+
// side effect this mode exists to remove.
|
|
942
|
+
if (this.#readOnly) {
|
|
943
|
+
throw new Error('Factory was constructed read-only and cannot be started');
|
|
944
|
+
}
|
|
898
945
|
if (this.#started) {
|
|
899
946
|
return;
|
|
900
947
|
}
|
|
@@ -1215,6 +1262,7 @@ export class FactoryLoop {
|
|
|
1215
1262
|
// `start()` overrides skip the schema's cross-field check, so re-apply its
|
|
1216
1263
|
// floor here: a deadline under one interval would kill every pass.
|
|
1217
1264
|
this.#readinessReconcileTimeoutMs = Math.max(options.reconcileTimeoutMs, options.reconcileIntervalMs);
|
|
1265
|
+
this.#relayfileOperationTimeoutMs = options.relayfileOperationTimeoutMs;
|
|
1218
1266
|
this.#liveConnectStartedAtMs = this.#clock.now();
|
|
1219
1267
|
this.#liveReplaySkewMarginMs = options.replaySkewMarginMs;
|
|
1220
1268
|
const highWatermark = await this.#currentEventHighWatermark();
|
|
@@ -1292,6 +1340,7 @@ export class FactoryLoop {
|
|
|
1292
1340
|
}
|
|
1293
1341
|
}
|
|
1294
1342
|
async #startLiveHeartbeat() {
|
|
1343
|
+
this.#beginHeartbeatLifetime();
|
|
1295
1344
|
this.#liveHeartbeatActive = true;
|
|
1296
1345
|
await this.#writeLiveHeartbeat('running');
|
|
1297
1346
|
this.#scheduleLiveHeartbeatRefresh();
|
|
@@ -1356,6 +1405,11 @@ export class FactoryLoop {
|
|
|
1356
1405
|
await this.#writeLoopHeartbeat(this.#config.loop.heartbeatPath, this.#config.loop.registryPath, status, 0, 0);
|
|
1357
1406
|
this.#liveHeartbeatLastWriteMs = this.#clock.now();
|
|
1358
1407
|
}
|
|
1408
|
+
#beginHeartbeatLifetime() {
|
|
1409
|
+
this.#heartbeatStartedAtMs = this.#clock.now();
|
|
1410
|
+
this.#progressSequence = 0;
|
|
1411
|
+
this.#lastProgressAtMs = undefined;
|
|
1412
|
+
}
|
|
1359
1413
|
#liveOptions(overrides) {
|
|
1360
1414
|
return {
|
|
1361
1415
|
transport: overrides.transport ?? this.#config.liveSubscription.transport,
|
|
@@ -1364,6 +1418,8 @@ export class FactoryLoop {
|
|
|
1364
1418
|
replaySkewMarginMs: overrides.replaySkewMarginMs ?? this.#config.liveSubscription.replaySkewMarginMs,
|
|
1365
1419
|
reconcileIntervalMs: overrides.reconcileIntervalMs ?? this.#config.liveSubscription.reconcileIntervalMs,
|
|
1366
1420
|
reconcileTimeoutMs: overrides.reconcileTimeoutMs ?? this.#config.liveSubscription.reconcileTimeoutMs,
|
|
1421
|
+
relayfileOperationTimeoutMs: overrides.relayfileOperationTimeoutMs
|
|
1422
|
+
?? this.#config.liveSubscription.relayfileOperationTimeoutMs,
|
|
1367
1423
|
};
|
|
1368
1424
|
}
|
|
1369
1425
|
async #currentEventCursor(limit) {
|
|
@@ -2296,6 +2352,10 @@ export class FactoryLoop {
|
|
|
2296
2352
|
leaseReleased = completed;
|
|
2297
2353
|
if (!completed)
|
|
2298
2354
|
throw new Error('discovery sweep lease was lost before completion');
|
|
2355
|
+
// This is the progress boundary consumed by deployment health. A timer
|
|
2356
|
+
// firing, a sweep starting, or a failed/deferred sweep must not move it.
|
|
2357
|
+
this.#progressSequence += 1;
|
|
2358
|
+
this.#lastProgressAtMs = this.#clock.now();
|
|
2299
2359
|
this.#logger.info?.('[factory] discovery sweep checkpoint committed', {
|
|
2300
2360
|
owner: claim.lease.owner,
|
|
2301
2361
|
epoch: claim.lease.epoch,
|
|
@@ -2462,7 +2522,13 @@ export class FactoryLoop {
|
|
|
2462
2522
|
}
|
|
2463
2523
|
const paths = await this.#readyIssuePaths();
|
|
2464
2524
|
const orphanRecovery = issueSource === 'github'
|
|
2465
|
-
? await this.#githubOrphanRecoveryContext()
|
|
2525
|
+
? await this.#githubOrphanRecoveryContext(dryRun)
|
|
2526
|
+
: undefined;
|
|
2527
|
+
// Preserved-not-reconciled is a fact the caller needs either way, but the
|
|
2528
|
+
// two causes are not the same event: a dry run skips the context by
|
|
2529
|
+
// design, a live sweep that has none failed to build one.
|
|
2530
|
+
const orphanRecoveryDegraded = issueSource === 'github' && orphanRecovery === undefined
|
|
2531
|
+
? (dryRun ? 'dry-run' : 'context-unavailable')
|
|
2466
2532
|
: undefined;
|
|
2467
2533
|
const pulled = [];
|
|
2468
2534
|
const triaged = [];
|
|
@@ -2719,7 +2785,15 @@ export class FactoryLoop {
|
|
|
2719
2785
|
this.#reconciledGithubInProgress.delete(recoveredIdentity);
|
|
2720
2786
|
}
|
|
2721
2787
|
}
|
|
2722
|
-
report = {
|
|
2788
|
+
report = {
|
|
2789
|
+
pulled,
|
|
2790
|
+
triaged,
|
|
2791
|
+
dispatched,
|
|
2792
|
+
skipped,
|
|
2793
|
+
dryRun,
|
|
2794
|
+
slackDegraded: this.#slackDegraded,
|
|
2795
|
+
...(orphanRecoveryDegraded ? { orphanRecoveryDegraded } : {}),
|
|
2796
|
+
};
|
|
2723
2797
|
return report;
|
|
2724
2798
|
}
|
|
2725
2799
|
catch (error) {
|
|
@@ -2740,6 +2814,7 @@ export class FactoryLoop {
|
|
|
2740
2814
|
dispatched: report.dispatched.length,
|
|
2741
2815
|
skipped: report.skipped.length,
|
|
2742
2816
|
slackDegraded: report.slackDegraded ?? false,
|
|
2817
|
+
orphanRecoveryDegraded: report.orphanRecoveryDegraded,
|
|
2743
2818
|
relayfileWaitWarnings: (this.#counters.relayfileOperationWaitWarnings ?? 0) - relayfileWaitWarningsAtStart,
|
|
2744
2819
|
relayfileSlowOperations: (this.#counters.relayfileSlowOperations ?? 0) - relayfileSlowOperationsAtStart,
|
|
2745
2820
|
relayfileOperationFailures: (this.#counters.relayfileOperationFailures ?? 0) - relayfileOperationFailuresAtStart,
|
|
@@ -3010,7 +3085,7 @@ export class FactoryLoop {
|
|
|
3010
3085
|
: { available: true, highWatermark };
|
|
3011
3086
|
}
|
|
3012
3087
|
catch (error) {
|
|
3013
|
-
if (
|
|
3088
|
+
if (isPassWideRelayfileFault(error))
|
|
3014
3089
|
throw error;
|
|
3015
3090
|
this.#increment('discoveryHighWatermarkFailures');
|
|
3016
3091
|
this.#logger.warn?.('[factory] discovery high-watermark unavailable; using a full sweep without caching', {
|
|
@@ -3033,7 +3108,7 @@ export class FactoryLoop {
|
|
|
3033
3108
|
events = [...page.events].sort(compareDiscoveryEvents);
|
|
3034
3109
|
}
|
|
3035
3110
|
catch (error) {
|
|
3036
|
-
if (
|
|
3111
|
+
if (isPassWideRelayfileFault(error))
|
|
3037
3112
|
throw error;
|
|
3038
3113
|
this.#logger.warn?.('[factory] discovery change feed unavailable; refreshing tree prefixes once', {
|
|
3039
3114
|
error: describeError(error).errorMessage,
|
|
@@ -3095,7 +3170,34 @@ export class FactoryLoop {
|
|
|
3095
3170
|
}
|
|
3096
3171
|
}
|
|
3097
3172
|
}
|
|
3098
|
-
|
|
3173
|
+
/**
|
|
3174
|
+
* The safety context orphan recovery needs before it may release a leaked
|
|
3175
|
+
* `factory:in-progress` claim: who is online, which issues are actively
|
|
3176
|
+
* owned, and which durable lifecycle rows look abandoned.
|
|
3177
|
+
*
|
|
3178
|
+
* A dry run never builds it. `#reconcileOrphanedGithubInProgress` refuses to
|
|
3179
|
+
* release a claim under `dryRun` before it so much as looks at the context,
|
|
3180
|
+
* and `mayRecoverGithubOrphan` in `#performRunOnce` is itself `!dryRun` —
|
|
3181
|
+
* so the context was already provably unused on that path. Gathering it
|
|
3182
|
+
* anyway was not free: `fleet.roster()` mints this process's workspace
|
|
3183
|
+
* identity on demand, which is exactly what a read-only client refuses
|
|
3184
|
+
* (#343) and exactly what a dry run must not need. Deciding what a sweep
|
|
3185
|
+
* WOULD do must not require an identity to do it with.
|
|
3186
|
+
*
|
|
3187
|
+
* Returning `undefined` is therefore the honest answer for a dry run, not a
|
|
3188
|
+
* failure: the caller reports the sweep as `orphanRecoveryDegraded:
|
|
3189
|
+
* 'dry-run'` and preserves in-progress issues, which is what a dry run does
|
|
3190
|
+
* regardless. The failure counter and the warn below stay for the live path,
|
|
3191
|
+
* where an absent context IS a degradation.
|
|
3192
|
+
*/
|
|
3193
|
+
async #githubOrphanRecoveryContext(dryRun) {
|
|
3194
|
+
if (dryRun) {
|
|
3195
|
+
this.#increment('githubOrphanRecoveryContextSkippedDryRun');
|
|
3196
|
+
this.#logger.info?.('[factory] dry run skipped the orphan-recovery safety context; in-progress issues are preserved', {
|
|
3197
|
+
reason: 'a dry run never releases an in-progress claim, so it needs no workspace identity to build one',
|
|
3198
|
+
});
|
|
3199
|
+
return undefined;
|
|
3200
|
+
}
|
|
3099
3201
|
try {
|
|
3100
3202
|
const [registry, roster, lifecycles, waitingClarifications] = await Promise.all([
|
|
3101
3203
|
readFactoryInFlightRegistry(this.#config.loop.registryPath),
|
|
@@ -3672,7 +3774,17 @@ export class FactoryLoop {
|
|
|
3672
3774
|
progressTimer.unref?.();
|
|
3673
3775
|
}
|
|
3674
3776
|
try {
|
|
3675
|
-
|
|
3777
|
+
// The bound the 2026-08-23 wedge needed (#351). `#readinessReconcileTimeoutMs`
|
|
3778
|
+
// is 90 minutes and rejects only the WAIT — the sweep keeps its discovery
|
|
3779
|
+
// lease and the next cycle coalesces onto it — so a call that never
|
|
3780
|
+
// returns stopped dispatch for as long as the process lived. Bounding the
|
|
3781
|
+
// call instead makes the pass unwind, which releases the lease and lets
|
|
3782
|
+
// the next cycle actually start.
|
|
3783
|
+
//
|
|
3784
|
+
// Slightly above the transport's own deadline so the mount's cancellation
|
|
3785
|
+
// wins the race and reports the more precise error; this is the backstop
|
|
3786
|
+
// for mounts that cannot honour a signal.
|
|
3787
|
+
const result = await withRelayfileCallDeadline(operation, details.phase, this.#relayfileOperationBackstopMs(), fn);
|
|
3676
3788
|
const elapsedMs = this.#elapsedSince(startedAtMs);
|
|
3677
3789
|
const count = opts.count?.(result);
|
|
3678
3790
|
if (opts.logComplete) {
|
|
@@ -3729,7 +3841,11 @@ export class FactoryLoop {
|
|
|
3729
3841
|
error: describeError(error).errorMessage,
|
|
3730
3842
|
});
|
|
3731
3843
|
}
|
|
3732
|
-
|
|
3844
|
+
// The transport deadline is meant to win the race above, and the mount
|
|
3845
|
+
// does not know which phase it was serving. Without this, `lastError`
|
|
3846
|
+
// would name the call but not the context — one of many list/read sites
|
|
3847
|
+
// (codex on #354).
|
|
3848
|
+
throw relayfileTimeoutWithPhase(error, details.phase);
|
|
3733
3849
|
}
|
|
3734
3850
|
finally {
|
|
3735
3851
|
if (progressTimer) {
|
|
@@ -3737,6 +3853,19 @@ export class FactoryLoop {
|
|
|
3737
3853
|
}
|
|
3738
3854
|
}
|
|
3739
3855
|
}
|
|
3856
|
+
/**
|
|
3857
|
+
* The cycle-level backstop budget: the transport deadline plus a margin.
|
|
3858
|
+
*
|
|
3859
|
+
* Proportional rather than a flat grace so it holds at both ends of the
|
|
3860
|
+
* range — a five-minute production budget and a millisecond test budget both
|
|
3861
|
+
* leave the transport room to fire first.
|
|
3862
|
+
*/
|
|
3863
|
+
#relayfileOperationBackstopMs() {
|
|
3864
|
+
const timeoutMs = this.#relayfileOperationTimeoutMs;
|
|
3865
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
3866
|
+
return undefined;
|
|
3867
|
+
return Math.ceil(timeoutMs * RELAYFILE_OPERATION_BACKSTOP_RATIO);
|
|
3868
|
+
}
|
|
3740
3869
|
#elapsedSince(startedAtMs) {
|
|
3741
3870
|
return Math.max(0, this.#clock.now() - startedAtMs);
|
|
3742
3871
|
}
|
|
@@ -3756,6 +3885,7 @@ export class FactoryLoop {
|
|
|
3756
3885
|
const maxConsecutiveFailures = Math.min(5, Math.max(1, Math.trunc(opts.maxConsecutiveFailures ?? this.#config.loop.maxConsecutiveFailures)));
|
|
3757
3886
|
const heartbeatPath = opts.heartbeatPath ?? this.#config.loop.heartbeatPath;
|
|
3758
3887
|
const registryPath = opts.registryPath ?? this.#config.loop.registryPath;
|
|
3888
|
+
this.#beginHeartbeatLifetime();
|
|
3759
3889
|
this.#loopReapPaths = { heartbeatPath, registryPath };
|
|
3760
3890
|
const reports = [];
|
|
3761
3891
|
let consecutiveFailures = 0;
|
|
@@ -6541,7 +6671,7 @@ export class FactoryLoop {
|
|
|
6541
6671
|
return [...issuePaths.values()].sort();
|
|
6542
6672
|
}
|
|
6543
6673
|
catch (error) {
|
|
6544
|
-
if (
|
|
6674
|
+
if (isPassWideRelayfileFault(error))
|
|
6545
6675
|
throw error;
|
|
6546
6676
|
this.#githubIssuePathIndexReady = false;
|
|
6547
6677
|
this.#increment('githubIssueListFailures');
|
|
@@ -6557,7 +6687,7 @@ export class FactoryLoop {
|
|
|
6557
6687
|
parsed = parseJsonContent(content);
|
|
6558
6688
|
}
|
|
6559
6689
|
catch (error) {
|
|
6560
|
-
if (
|
|
6690
|
+
if (isPassWideRelayfileFault(error))
|
|
6561
6691
|
throw error;
|
|
6562
6692
|
return undefined;
|
|
6563
6693
|
}
|
|
@@ -6654,7 +6784,7 @@ export class FactoryLoop {
|
|
|
6654
6784
|
this.#increment('githubIssueMirrorsCreated');
|
|
6655
6785
|
}
|
|
6656
6786
|
catch (error) {
|
|
6657
|
-
if (
|
|
6787
|
+
if (isPassWideRelayfileFault(error))
|
|
6658
6788
|
throw error;
|
|
6659
6789
|
this.#logger.error?.('[factory] failed to ingest GitHub issue', error);
|
|
6660
6790
|
}
|
|
@@ -7205,13 +7335,28 @@ export class FactoryLoop {
|
|
|
7205
7335
|
}
|
|
7206
7336
|
async #writeLoopHeartbeat(path, registryPath, status, iteration, maxIterations) {
|
|
7207
7337
|
const updatedAtMs = this.#clock.now();
|
|
7338
|
+
this.#heartbeatStartedAtMs ??= updatedAtMs;
|
|
7208
7339
|
const heartbeat = {
|
|
7209
7340
|
pid: process.pid,
|
|
7210
7341
|
status,
|
|
7342
|
+
source: maxIterations === 0 ? 'live-timer' : 'bounded-loop',
|
|
7211
7343
|
iteration,
|
|
7212
7344
|
maxIterations,
|
|
7345
|
+
startedAt: new Date(this.#heartbeatStartedAtMs).toISOString(),
|
|
7346
|
+
startedAtMs: this.#heartbeatStartedAtMs,
|
|
7347
|
+
progressContract: 'discovery-sweep-v1',
|
|
7213
7348
|
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
7214
7349
|
updatedAtMs,
|
|
7350
|
+
...(this.#lastProgressAtMs !== undefined
|
|
7351
|
+
? {
|
|
7352
|
+
progress: {
|
|
7353
|
+
sequence: this.#progressSequence,
|
|
7354
|
+
operation: 'discovery-sweep',
|
|
7355
|
+
updatedAt: new Date(this.#lastProgressAtMs).toISOString(),
|
|
7356
|
+
updatedAtMs: this.#lastProgressAtMs,
|
|
7357
|
+
},
|
|
7358
|
+
}
|
|
7359
|
+
: {}),
|
|
7215
7360
|
registryPath,
|
|
7216
7361
|
eventListener: this.#eventListenerStatus(),
|
|
7217
7362
|
readinessReconcile: this.#readinessReconcileStatus(),
|
|
@@ -7443,7 +7588,7 @@ export class FactoryLoop {
|
|
|
7443
7588
|
return resolvedIssue;
|
|
7444
7589
|
}
|
|
7445
7590
|
catch (error) {
|
|
7446
|
-
if (
|
|
7591
|
+
if (isPassWideRelayfileFault(error))
|
|
7447
7592
|
throw error;
|
|
7448
7593
|
if (isMissingIssueFileError(error) && isIssuePathUnderRoot(path)) {
|
|
7449
7594
|
this.#increment('phantomSkipped');
|
|
@@ -8539,7 +8684,7 @@ export class FactoryLoop {
|
|
|
8539
8684
|
paths = await this.#listRelayfileTree(root, 'exact-head PR confirmation');
|
|
8540
8685
|
}
|
|
8541
8686
|
catch (error) {
|
|
8542
|
-
if (
|
|
8687
|
+
if (isPassWideRelayfileFault(error))
|
|
8543
8688
|
throw error;
|
|
8544
8689
|
continue;
|
|
8545
8690
|
}
|
|
@@ -8809,7 +8954,7 @@ export class FactoryLoop {
|
|
|
8809
8954
|
return await this.#listRelayfileTree(root, 'published PR confirmation');
|
|
8810
8955
|
}
|
|
8811
8956
|
catch (error) {
|
|
8812
|
-
if (
|
|
8957
|
+
if (isPassWideRelayfileFault(error))
|
|
8813
8958
|
throw error;
|
|
8814
8959
|
return [];
|
|
8815
8960
|
}
|
|
@@ -10391,7 +10536,7 @@ export class FactoryLoop {
|
|
|
10391
10536
|
}
|
|
10392
10537
|
}
|
|
10393
10538
|
catch (error) {
|
|
10394
|
-
if (
|
|
10539
|
+
if (isPassWideRelayfileFault(error))
|
|
10395
10540
|
throw error;
|
|
10396
10541
|
this.#logger.warn?.('[factory] unable to list GitHub issue comments for replay', { prefix, error });
|
|
10397
10542
|
}
|
|
@@ -12937,7 +13082,7 @@ export class FactoryLoop {
|
|
|
12937
13082
|
found.push(...tree.filter((path) => path.endsWith('.json') && numberSegment.test(path)));
|
|
12938
13083
|
}
|
|
12939
13084
|
catch (error) {
|
|
12940
|
-
if (
|
|
13085
|
+
if (isPassWideRelayfileFault(error))
|
|
12941
13086
|
throw error;
|
|
12942
13087
|
// try the next root
|
|
12943
13088
|
}
|
|
@@ -14116,7 +14261,7 @@ export class FactoryLoop {
|
|
|
14116
14261
|
return await this.#listRelayfileTree(prefix, 'Slack identity lookup');
|
|
14117
14262
|
}
|
|
14118
14263
|
catch (error) {
|
|
14119
|
-
if (
|
|
14264
|
+
if (isPassWideRelayfileFault(error))
|
|
14120
14265
|
throw error;
|
|
14121
14266
|
return [];
|
|
14122
14267
|
}
|
|
@@ -17043,7 +17188,7 @@ const resolveIssuePrFromMount = async (mount, config, issue, opts = {}, listTree
|
|
|
17043
17188
|
paths.add(path);
|
|
17044
17189
|
}
|
|
17045
17190
|
catch (error) {
|
|
17046
|
-
if (
|
|
17191
|
+
if (isPassWideRelayfileFault(error))
|
|
17047
17192
|
throw error;
|
|
17048
17193
|
listErrors.push(error);
|
|
17049
17194
|
}
|