@agent-relay/factory 0.1.72 → 0.1.73
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/diagnose.d.ts +3 -2
- package/dist/cli/diagnose.d.ts.map +1 -1
- package/dist/cli/diagnose.js +60 -5
- package/dist/cli/diagnose.js.map +1 -1
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +5 -0
- package/dist/cli/fleet.js.map +1 -1
- package/dist/fleet/control-plane-circuit.d.ts +10 -0
- package/dist/fleet/control-plane-circuit.d.ts.map +1 -1
- package/dist/fleet/control-plane-circuit.js +9 -1
- package/dist/fleet/control-plane-circuit.js.map +1 -1
- package/dist/fleet/relay-fleet-client.d.ts +21 -2
- package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
- package/dist/fleet/relay-fleet-client.js +97 -17
- package/dist/fleet/relay-fleet-client.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/observability/events.d.ts +14 -14
- package/dist/orchestrator/dispatch-failure-reason.d.ts +60 -0
- package/dist/orchestrator/dispatch-failure-reason.d.ts.map +1 -0
- package/dist/orchestrator/dispatch-failure-reason.js +127 -0
- package/dist/orchestrator/dispatch-failure-reason.js.map +1 -0
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +159 -28
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/orchestrator/public-health.d.ts.map +1 -1
- package/dist/orchestrator/public-health.js +116 -6
- package/dist/orchestrator/public-health.js.map +1 -1
- package/dist/ports/fleet.d.ts +38 -0
- package/dist/ports/fleet.d.ts.map +1 -1
- package/dist/ports/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/types.d.ts +118 -14
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ import { createFactoryCloudEventV1, factoryCloudReleaseReasonV1, } from '../obse
|
|
|
36
36
|
import { telemetryErrorClass } from '../observability/error-class.js';
|
|
37
37
|
import { derivedReadinessReconcileState, publicHealthFromHeartbeat, readinessReconcileInFlightMs, } from './public-health.js';
|
|
38
38
|
import { factorySweepSkipReasonCounts } from './sweep-skip-reason.js';
|
|
39
|
+
import { factoryDispatchFailureReasonCodeForErrorClass, factoryDispatchFailureReasonCounts, } from './dispatch-failure-reason.js';
|
|
39
40
|
import { boundedRunCostTotal, CostLedger } from '../cost/ledger.js';
|
|
40
41
|
import { createTicketDispatchDelivery } from '../delivery/ticket-dispatch.js';
|
|
41
42
|
import { canonicalTrajectorySessionRef, renderTrajectoryPointer, stripTrajectoryPointers, } from '../trajectory.js';
|
|
@@ -553,18 +554,30 @@ export class FactoryLoop {
|
|
|
553
554
|
#readinessReconcileLastError;
|
|
554
555
|
#readinessReconcileLastErrorClass;
|
|
555
556
|
/**
|
|
556
|
-
* The last *
|
|
557
|
+
* The last *enumerating* sweep's arithmetic (#355).
|
|
557
558
|
*
|
|
558
|
-
* Held as one record rather than
|
|
559
|
+
* Held as one record rather than three fields so it can only ever be replaced
|
|
559
560
|
* whole: publishing a `dispatched` from one pass beside a `candidates` from
|
|
560
561
|
* another would be worse than publishing neither, since the whole use of
|
|
561
562
|
* these numbers is comparing them to each other.
|
|
562
563
|
*
|
|
563
|
-
* `undefined` until a sweep
|
|
564
|
+
* `undefined` until a sweep enumerates, and never initialised to zeroes —
|
|
564
565
|
* "this daemon has not finished a sweep" and "a sweep finished and found
|
|
565
566
|
* nothing" are the two readings #355 has to tell apart.
|
|
566
567
|
*/
|
|
567
568
|
#readinessReconcileLastSweep;
|
|
569
|
+
/**
|
|
570
|
+
* Whether the MOST RECENT pass deferred, tracked apart from the counts above
|
|
571
|
+
* (#358 review, CodeRabbit — Major, and right).
|
|
572
|
+
*
|
|
573
|
+
* A deferred pass enumerates nothing and settles in milliseconds, so folding
|
|
574
|
+
* it into the snapshot overwrote the last real sweep's numbers with zeroes.
|
|
575
|
+
* On a container where another process holds the lease for any length of time
|
|
576
|
+
* — the #347/#349 condition — every pass would publish `candidates: 0` and the
|
|
577
|
+
* last actual enumeration would be unrecoverable, destroying the measurement
|
|
578
|
+
* this whole change exists to provide.
|
|
579
|
+
*/
|
|
580
|
+
#readinessReconcileLastSweepDeferred;
|
|
568
581
|
#liveEventQueue = [];
|
|
569
582
|
#liveEventDrainScheduled = false;
|
|
570
583
|
#liveEventDrainActive = false;
|
|
@@ -1331,9 +1344,11 @@ export class FactoryLoop {
|
|
|
1331
1344
|
// cold container it is the first — and for the next interval, only —
|
|
1332
1345
|
// sweep whose counts exist. Leaving it unrecorded would make a daemon
|
|
1333
1346
|
// that has completed a full pass still read as "never ran" (#355).
|
|
1334
|
-
|
|
1347
|
+
const report = await this.runOnce();
|
|
1335
1348
|
this.#readinessReconcileLastDurationMs = this.#elapsedSince(backfillStartedAtMs);
|
|
1336
|
-
|
|
1349
|
+
const completedAtMs = this.#clock.now();
|
|
1350
|
+
this.#readinessReconcileLastCompletedAtMs = completedAtMs;
|
|
1351
|
+
this.#recordReadinessSweepOutcome(report, completedAtMs);
|
|
1337
1352
|
}
|
|
1338
1353
|
catch (error) {
|
|
1339
1354
|
this.#readinessReconcileLastDurationMs = this.#elapsedSince(backfillStartedAtMs);
|
|
@@ -1559,20 +1574,28 @@ export class FactoryLoop {
|
|
|
1559
1574
|
const report = await this.#runOnceWithReadinessDeadline();
|
|
1560
1575
|
this.#readinessReconcileConsecutiveFailures = 0;
|
|
1561
1576
|
this.#readinessReconcileLastDurationMs = this.#elapsedSince(startedAtMs);
|
|
1562
|
-
|
|
1577
|
+
const completedAtMs = this.#clock.now();
|
|
1578
|
+
this.#readinessReconcileLastCompletedAtMs = completedAtMs;
|
|
1563
1579
|
this.#readinessReconcileLastError = undefined;
|
|
1564
1580
|
this.#readinessReconcileLastErrorClass = undefined;
|
|
1565
1581
|
// The three integers below have gone to stdout since this loop existed,
|
|
1566
1582
|
// and stdout does not reach the deployed container's operator (#355).
|
|
1567
1583
|
// Publishing them is what lets a reader tell a sweep that saw eligible
|
|
1568
1584
|
// work and rejected it from one that never pulled it at all.
|
|
1569
|
-
this.#recordReadinessSweepOutcome(report);
|
|
1585
|
+
this.#recordReadinessSweepOutcome(report, completedAtMs);
|
|
1570
1586
|
this.#logger.info?.('[factory] periodic readiness reconciliation completed', {
|
|
1571
1587
|
durationMs: this.#readinessReconcileLastDurationMs,
|
|
1572
1588
|
candidates: report.pulled.length,
|
|
1573
1589
|
dispatched: report.dispatched.length,
|
|
1574
1590
|
skipped: report.skipped.length,
|
|
1575
|
-
|
|
1591
|
+
// THIS pass's breakdown, never the retained snapshot (#359 review,
|
|
1592
|
+
// codex P2). Logging the retained one beside a deferred pass's zeroes
|
|
1593
|
+
// produced a line that contradicted its own arithmetic —
|
|
1594
|
+
// `skipped: 0` next to a non-empty breakdown — and this log is what a
|
|
1595
|
+
// local operator reads.
|
|
1596
|
+
skipReasons: factorySweepSkipReasonCounts(report.skipped),
|
|
1597
|
+
dispatchFailures: report.skipped.filter((entry) => entry.code === 'dispatch-failed').length,
|
|
1598
|
+
dispatchFailureReasons: factoryDispatchFailureReasonCounts(report.skipped),
|
|
1576
1599
|
discoveryDeferred: report.discoveryDeferred,
|
|
1577
1600
|
});
|
|
1578
1601
|
}
|
|
@@ -1596,6 +1619,10 @@ export class FactoryLoop {
|
|
|
1596
1619
|
this.#readinessReconcileLastDurationMs = this.#elapsedSince(startedAtMs);
|
|
1597
1620
|
this.#readinessReconcileLastFailureAtMs = this.#clock.now();
|
|
1598
1621
|
this.#readinessReconcileLastError = errorMessage;
|
|
1622
|
+
// This failure is now the latest settled pass. A deferral marker left by
|
|
1623
|
+
// an older pass would falsely describe this one as lease contention when
|
|
1624
|
+
// the timestamps/error below prove that it acquired the lease and failed.
|
|
1625
|
+
this.#readinessReconcileLastSweepDeferred = undefined;
|
|
1599
1626
|
// The class, unlike the message, is publishable: #295 puts it on the
|
|
1600
1627
|
// unauthenticated health surface through the same allowlist.
|
|
1601
1628
|
this.#readinessReconcileLastErrorClass = telemetryErrorClass(error);
|
|
@@ -2703,6 +2730,10 @@ export class FactoryLoop {
|
|
|
2703
2730
|
continue;
|
|
2704
2731
|
}
|
|
2705
2732
|
const recoveredIdentity = recoveredOrphan ? githubIssueRefIdentity(issueRef(issue)) : undefined;
|
|
2733
|
+
// Reset per work unit, and advanced by assignment immediately before
|
|
2734
|
+
// each stage rather than inferred in the catch: the whole value of the
|
|
2735
|
+
// `unclassified-*` codes is that they name the stage honestly (#355).
|
|
2736
|
+
let attemptPhase = 'gate';
|
|
2706
2737
|
try {
|
|
2707
2738
|
if (recoveredOrphan) {
|
|
2708
2739
|
const dispatchBlock = await this.#dispatchBlockReason(issue);
|
|
@@ -2723,8 +2754,10 @@ export class FactoryLoop {
|
|
|
2723
2754
|
});
|
|
2724
2755
|
continue;
|
|
2725
2756
|
}
|
|
2757
|
+
attemptPhase = 'triage';
|
|
2726
2758
|
const decision = await this.triageIssue(issue);
|
|
2727
2759
|
triaged.push(decision);
|
|
2760
|
+
attemptPhase = 'dispatch';
|
|
2728
2761
|
const result = await this.dispatch(decision, { dryRun });
|
|
2729
2762
|
// A completed dispatch — even one that parks or escalates the issue —
|
|
2730
2763
|
// proves the pipeline still works, so the fuse below starts over.
|
|
@@ -2825,6 +2858,11 @@ export class FactoryLoop {
|
|
|
2825
2858
|
issue: issueRef(issue),
|
|
2826
2859
|
reason: perItemDispatchSkipReason(error),
|
|
2827
2860
|
code: 'dispatch-failed',
|
|
2861
|
+
// The publishable half of the same classification. `reason` is the
|
|
2862
|
+
// operator's sentence and stays off the health surface; this token
|
|
2863
|
+
// is what tells a reader watching `dispatch-failed: 5` which of
|
|
2864
|
+
// five very different bugs they are looking at (#355).
|
|
2865
|
+
failureCode: perItemDispatchFailureCode(error, attemptPhase),
|
|
2828
2866
|
});
|
|
2829
2867
|
continue;
|
|
2830
2868
|
}
|
|
@@ -4488,6 +4526,9 @@ export class FactoryLoop {
|
|
|
4488
4526
|
})) ?? [],
|
|
4489
4527
|
counters: { ...this.#counters },
|
|
4490
4528
|
fleetControlPlane: this.#fleetControlPlane.status(),
|
|
4529
|
+
// Optional on the port: a backend with no socket omits it, and an absent
|
|
4530
|
+
// value stays absent rather than being invented as healthy.
|
|
4531
|
+
...(this.#fleet.fleetConnectStatus ? { fleetConnect: this.#fleet.fleetConnectStatus() } : {}),
|
|
4491
4532
|
slackDegraded: this.#slackDegraded,
|
|
4492
4533
|
slackDegradedReason: this.#slackDegradedReason,
|
|
4493
4534
|
eventListener: this.#eventListenerStatus(),
|
|
@@ -4519,16 +4560,37 @@ export class FactoryLoop {
|
|
|
4519
4560
|
*
|
|
4520
4561
|
* Only successful passes reach here: a pass that threw has no report, and
|
|
4521
4562
|
* inventing zeroes for it would publish "found nothing" for a sweep that
|
|
4522
|
-
* never got to look. The previous pass's numbers stay put
|
|
4523
|
-
* `
|
|
4563
|
+
* never got to look. The previous enumerating pass's numbers stay put
|
|
4564
|
+
* instead, dated by `lastEnumeratedAtMs`, which is the honest reading.
|
|
4565
|
+
*
|
|
4566
|
+
* A deferred pass gets the same treatment for the same reason. It settles
|
|
4567
|
+
* successfully, and `lastCompletedAtMs` moves — deliberately, because the
|
|
4568
|
+
* #295/#296 stall derivation reads that timestamp against `lastStartedAtMs`,
|
|
4569
|
+
* and freezing it would report a functioning daemon as a hung one after ten
|
|
4570
|
+
* intervals of deferring correctly to another owner. But it enumerated
|
|
4571
|
+
* nothing, so its zeroes are not a measurement of anything and must not
|
|
4572
|
+
* replace one. Only the marker is recorded.
|
|
4524
4573
|
*/
|
|
4525
|
-
#recordReadinessSweepOutcome(report) {
|
|
4574
|
+
#recordReadinessSweepOutcome(report, completedAtMs) {
|
|
4575
|
+
if (report.discoveryDeferred) {
|
|
4576
|
+
this.#readinessReconcileLastSweepDeferred = report.discoveryDeferred;
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4579
|
+
this.#readinessReconcileLastSweepDeferred = undefined;
|
|
4526
4580
|
this.#readinessReconcileLastSweep = {
|
|
4527
4581
|
candidates: report.pulled.length,
|
|
4528
4582
|
dispatched: report.dispatched.length,
|
|
4529
4583
|
skipped: report.skipped.length,
|
|
4530
4584
|
skipReasons: factorySweepSkipReasonCounts(report.skipped),
|
|
4531
|
-
|
|
4585
|
+
// Counted from the same entries `skipReasons` counts, so the parts sum to
|
|
4586
|
+
// `skipReasons['dispatch-failed']` by construction rather than by a
|
|
4587
|
+
// second traversal agreeing with the first.
|
|
4588
|
+
dispatchFailures: report.skipped.filter((entry) => entry.code === 'dispatch-failed').length,
|
|
4589
|
+
dispatchFailureReasons: factoryDispatchFailureReasonCounts(report.skipped),
|
|
4590
|
+
// The caller's completion stamp, not a fresh clock read: on a pass that
|
|
4591
|
+
// enumerated, `lastEnumeratedAtMs` and `lastCompletedAtMs` describe the
|
|
4592
|
+
// same instant and must not drift apart by a tick.
|
|
4593
|
+
enumeratedAtMs: completedAtMs,
|
|
4532
4594
|
};
|
|
4533
4595
|
}
|
|
4534
4596
|
#readinessReconcileStatus() {
|
|
@@ -4616,11 +4678,21 @@ export class FactoryLoop {
|
|
|
4616
4678
|
...(Object.keys(this.#readinessReconcileLastSweep.skipReasons).length > 0
|
|
4617
4679
|
? { skipReasons: { ...this.#readinessReconcileLastSweep.skipReasons } }
|
|
4618
4680
|
: {}),
|
|
4619
|
-
|
|
4620
|
-
|
|
4681
|
+
// Unconditional, unlike the breakdown below it: a zero here is the
|
|
4682
|
+
// fact "this sweep attempted dispatches and none of them failed",
|
|
4683
|
+
// which no other field on this surface can express.
|
|
4684
|
+
dispatchFailures: this.#readinessReconcileLastSweep.dispatchFailures,
|
|
4685
|
+
...(Object.keys(this.#readinessReconcileLastSweep.dispatchFailureReasons).length > 0
|
|
4686
|
+
? { dispatchFailureReasons: { ...this.#readinessReconcileLastSweep.dispatchFailureReasons } }
|
|
4621
4687
|
: {}),
|
|
4688
|
+
lastEnumeratedAtMs: this.#readinessReconcileLastSweep.enumeratedAtMs,
|
|
4622
4689
|
}
|
|
4623
4690
|
: {}),
|
|
4691
|
+
// Independent of the trio: a daemon whose FIRST pass deferred has no
|
|
4692
|
+
// counts to publish and still needs to say why.
|
|
4693
|
+
...(this.#readinessReconcileLastSweepDeferred
|
|
4694
|
+
? { discoveryDeferred: this.#readinessReconcileLastSweepDeferred }
|
|
4695
|
+
: {}),
|
|
4624
4696
|
...(this.#readinessReconcileLastError ? { lastError: this.#readinessReconcileLastError } : {}),
|
|
4625
4697
|
...(this.#readinessReconcileLastErrorClass
|
|
4626
4698
|
? { lastErrorClass: this.#readinessReconcileLastErrorClass }
|
|
@@ -7453,6 +7525,9 @@ export class FactoryLoop {
|
|
|
7453
7525
|
readinessReconcile: this.#readinessReconcileStatus(),
|
|
7454
7526
|
dispatchCapacity: this.#dispatchCapacityStatus(),
|
|
7455
7527
|
fleetControlPlane: this.#fleetControlPlane.status(),
|
|
7528
|
+
// Optional on the port: a backend with no socket omits it, and an absent
|
|
7529
|
+
// value stays absent rather than being invented as healthy.
|
|
7530
|
+
...(this.#fleet.fleetConnectStatus ? { fleetConnect: this.#fleet.fleetConnectStatus() } : {}),
|
|
7456
7531
|
};
|
|
7457
7532
|
// The deployed container serves `/healthz` straight out of this file and
|
|
7458
7533
|
// has no redaction logic of its own, so publish the already-safe view here
|
|
@@ -18271,21 +18346,28 @@ const githubIssueIndexRepoRoots = (path) => {
|
|
|
18271
18346
|
`/github/repos/${owner}__${repo}/issues`,
|
|
18272
18347
|
];
|
|
18273
18348
|
};
|
|
18274
|
-
|
|
18275
|
-
|
|
18349
|
+
/** How far to follow wrapped provider failures without trusting an unbounded chain. */
|
|
18350
|
+
const RELAYFILE_OVERLOAD_CAUSE_DEPTH = 4;
|
|
18351
|
+
const relayfileOverload = (error, depth = 0) => {
|
|
18352
|
+
if (depth > RELAYFILE_OVERLOAD_CAUSE_DEPTH)
|
|
18353
|
+
return undefined;
|
|
18354
|
+
const flat = asRecord(error);
|
|
18355
|
+
if (!flat)
|
|
18356
|
+
return undefined;
|
|
18276
18357
|
const response = asRecord(flat.response) ?? {};
|
|
18277
18358
|
const data = asRecord(flat.data) ?? asRecord(response.data) ?? {};
|
|
18278
18359
|
const details = asRecord(flat.details) ?? asRecord(data.details) ?? {};
|
|
18279
18360
|
const statusValue = flat.status ?? flat.statusCode ?? response.status ?? response.statusCode;
|
|
18280
18361
|
const status = typeof statusValue === 'number' ? statusValue : Number(statusValue);
|
|
18281
|
-
if (status
|
|
18282
|
-
|
|
18283
|
-
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18362
|
+
if (status === 429) {
|
|
18363
|
+
const retryValue = flat.retryAfterSeconds ?? details.retryAfterSeconds ?? data.retryAfterSeconds;
|
|
18364
|
+
const parsedRetry = typeof retryValue === 'number' ? retryValue : Number(retryValue);
|
|
18365
|
+
const retryAfterSeconds = Number.isFinite(parsedRetry) && parsedRetry >= 0 ? parsedRetry : undefined;
|
|
18366
|
+
const reason = stringValue(flat.reason) ?? stringValue(details.reason) ?? stringValue(data.reason) ??
|
|
18367
|
+
stringValue(flat.code) ?? stringValue(data.code) ?? 'rate_limited';
|
|
18368
|
+
return { status, reason, ...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }) };
|
|
18369
|
+
}
|
|
18370
|
+
return relayfileOverload(flat.cause, depth + 1);
|
|
18289
18371
|
};
|
|
18290
18372
|
/**
|
|
18291
18373
|
* relayfile's overload reason codes, allowlisted.
|
|
@@ -18804,13 +18886,14 @@ const PASS_FATAL_CAUSE_DEPTH = 4;
|
|
|
18804
18886
|
* classification has to follow the cause chain rather than trust the outermost
|
|
18805
18887
|
* type.
|
|
18806
18888
|
*/
|
|
18807
|
-
const
|
|
18889
|
+
const findWrappedErrorOfType = (error, type, depth = 0) => {
|
|
18808
18890
|
if (depth > PASS_FATAL_CAUSE_DEPTH || !(error instanceof Error))
|
|
18809
|
-
return
|
|
18891
|
+
return undefined;
|
|
18810
18892
|
if (error instanceof type)
|
|
18811
|
-
return
|
|
18812
|
-
return
|
|
18893
|
+
return error;
|
|
18894
|
+
return findWrappedErrorOfType(error.cause, type, depth + 1);
|
|
18813
18895
|
};
|
|
18896
|
+
const wrapsErrorOfType = (error, type) => findWrappedErrorOfType(error, type) !== undefined;
|
|
18814
18897
|
/**
|
|
18815
18898
|
* How many *unclassified* per-item failures without an intervening successful
|
|
18816
18899
|
* dispatch end the pass. Named per-item conditions (a lifecycle claim refusal,
|
|
@@ -18875,6 +18958,54 @@ const perItemDispatchSkipReason = (error) => {
|
|
|
18875
18958
|
}
|
|
18876
18959
|
return `dispatch failed (${telemetryErrorClass(error)})`;
|
|
18877
18960
|
};
|
|
18961
|
+
const UNCLASSIFIED_PHASE_CODES = {
|
|
18962
|
+
gate: 'unclassified-gate',
|
|
18963
|
+
triage: 'unclassified-triage',
|
|
18964
|
+
dispatch: 'unclassified-dispatch',
|
|
18965
|
+
};
|
|
18966
|
+
/**
|
|
18967
|
+
* Why a dispatch attempt failed, as a code from the published vocabulary (#355).
|
|
18968
|
+
*
|
|
18969
|
+
* The sibling of `perItemDispatchSkipReason`: that returns the operator's
|
|
18970
|
+
* sentence, this returns the one token that may cross onto the unauthenticated
|
|
18971
|
+
* health surface, and both are recorded at the skip site from the same thrown
|
|
18972
|
+
* value. The token is never parsed back out of the sentence — a reworded
|
|
18973
|
+
* message would silently empty a bucket, and this vocabulary is what an
|
|
18974
|
+
* operator reads when the daemon's stdout does not reach them.
|
|
18975
|
+
*
|
|
18976
|
+
* Ordered most specific first. Every branch follows the bounded cause chain,
|
|
18977
|
+
* because `contextualError` and the control-plane guard both rethrow wrapped.
|
|
18978
|
+
* `relayfileOverload` is also the loop's shedding predicate, so widening it at
|
|
18979
|
+
* the source keeps the health code, skip counter, fuse, and durable overload
|
|
18980
|
+
* ratchet on one verdict instead of merely relabelling the published bucket.
|
|
18981
|
+
*/
|
|
18982
|
+
const perItemDispatchFailureCode = (error, phase) => {
|
|
18983
|
+
if (relayfileOverload(error) !== undefined)
|
|
18984
|
+
return 'relayfile-overloaded';
|
|
18985
|
+
if (wrapsErrorOfType(error, LiveDispatchStateChangedError))
|
|
18986
|
+
return 'live-state-changed';
|
|
18987
|
+
if (wrapsErrorOfType(error, LatePlacementReleasedError))
|
|
18988
|
+
return 'late-placement-released';
|
|
18989
|
+
const refused = findWrappedErrorOfType(error, DispatchLifecycleClaimRefusedError);
|
|
18990
|
+
if (refused)
|
|
18991
|
+
return refused.refusal === 'terminal' ? 'lifecycle-terminal' : 'lifecycle-owned-elsewhere';
|
|
18992
|
+
if (wrapsErrorOfType(error, FleetControlPlaneCircuitOpenError))
|
|
18993
|
+
return 'control-plane-open';
|
|
18994
|
+
// The class-name tail of the vocabulary; see its own doc comment for why
|
|
18995
|
+
// these five are not `instanceof`. Walked down the cause chain like the
|
|
18996
|
+
// branches above, because `contextualError` wraps in a plain `Error` and
|
|
18997
|
+
// reading only the outermost name would miss every wrapped spawn failure.
|
|
18998
|
+
// `telemetryErrorClass` is the same allowlist that guards every other
|
|
18999
|
+
// identifier leaving this process, so a hostile `name` cannot invent a key
|
|
19000
|
+
// here either — it collapses to `Error`, which the map does not hold.
|
|
19001
|
+
for (let cursor = error, depth = 0; cursor instanceof Error && depth <= PASS_FATAL_CAUSE_DEPTH; depth += 1) {
|
|
19002
|
+
const named = factoryDispatchFailureReasonCodeForErrorClass(telemetryErrorClass(cursor));
|
|
19003
|
+
if (named)
|
|
19004
|
+
return named;
|
|
19005
|
+
cursor = cursor.cause;
|
|
19006
|
+
}
|
|
19007
|
+
return UNCLASSIFIED_PHASE_CODES[phase];
|
|
19008
|
+
};
|
|
18878
19009
|
const triageEscalationQuestion = (decision, issue) => {
|
|
18879
19010
|
const routedRepos = decision.routes.map((route) => route.repo).filter(Boolean);
|
|
18880
19011
|
const subject = issue?.title?.trim() || decision.issue.key;
|