Skip to content

Commit 066ff51

Browse files
fix(service-automation): end the backoff retry of a degraded connector on a configuration fault (#28014)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DAgUzvVk8NtjaKQTR762kuz)_ Fixes #27992 Clause-②: no Metadata-protocol: none Skills-docs: follow-up owed — content/docs/automation/connectors.mdx (its fault table has no row for a configuration fault met on a degraded instance's retry; `git grep -n -i degraded -- skills` finds no retry wording; `docs/adr/0097-declarative-connector-instances.md` §degrade is governed and left as is) ## 维护者速读 - 改了什么:连接器在启动时连不上上游会进入「降级」并按退避定时重试;如果之后重试时上游可达、但配置本身有错(例如 `include` 写了上游没有的工具),现在会停止重试,并把原因换成配置错误的原文。 - 为什么改:之前这种情况只记一条日志,实例仍留在重试集合里,连接器列表一直显示过时的「连不上」原因,并每 5 分钟徒劳地重连一次;ADR-0097 规定配置错误不重试。 - 风险与回滚:只影响「已降级且重试时遇到配置错误」这一条路径;凭据被拒与上游仍不可达两条路径不变(有对照测试)。回滚即还原本 PR 的 `plugin.ts`。 - 先看:`packages/services/service-automation/src/plugin.ts`(工厂 catch 里新加的分支,与新方法 `failDegradedConnectorInstance`);`connector-retry-configuration-fault.test.ts`。 - 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/connector-retry-configuration-fault.test.ts`,预期 6/6 通过。 - 需要你做的:无,席位落地。 ## What changes `reconcileDeclaredConnectors` routes a provider factory's throw three ways: a credential rejection holds, an upstream-unavailable fault degrades and retries on the backoff, and anything else is a configuration fault that `fail()`s (fatal at boot, logged and skipped on a reload). The skip left an instance that was already degraded in `degradedInstances`, with its husk still carrying the stale "could not reach" reason, so the backoff timer kept reconnecting against a fault no retry can clear. Now, on a soft run, a configuration fault met by an instance in `degradedInstances` goes to a new `failDegradedConnectorInstance`: - The instance is held (the existing `held` flag the credential path uses): `scheduleDeclarativeRetry` skips it, and a reconcile with the same signature skips it. A change of its declaration or a restart re-attempts it. - With no live connector, the husk is registered again with the provider's configuration message as its `degradedReason`, so `GET /api/v1/automation/connectors` and the `connector_action` refusal name the configuration fault. - With a live connector (a changed entry whose new configuration was pending a retry), the old connector keeps serving and the retry stops. - One `error` record says the instance is no longer retried; the provider's message rides `meta` (#5575). Untouched: the boot path (still fatal), a configuration fault on an instance that was not degraded (still logged and skipped), the credential hold path, and the unavailable degrade path. Reading chosen for "the failed state": `ConnectorState` (`packages/spec/src/integration/connector-descriptor.ts`) has only `ready` and `degraded`, so the descriptor keeps `state: 'degraded'` and the failure is carried by the replaced reason plus the ended retry. Adding a `failed` state would widen a published spec type and is outside this card. ## Tests New `packages/services/service-automation/src/connector-retry-configuration-fault.test.ts`, under fake timers, with fake `mcp` and `openapi` provider factories shaped like the real ones (unavailable marker while down, a plain `Error` naming `providerConfig.include` once up). The `openapi` pin needed no provider-package edit: the path is provider-agnostic in `plugin.ts`. - `mcp` / `openapi`: degraded at boot, the backoff retry meets the include fault, the descriptor's `degradedReason` is the configuration message and no longer "could not reach", and 64 × the base delay later the factory has not been called again. - An unchanged reload does not re-attempt it; an edit of the declaration does and recovers it to `ready`. - A changed entry pending a retry: the old connector keeps serving and the retry stops. - Control: an upstream still unreachable on the retry keeps degrading and is retried again. - Control: a credential refused on the retry keeps its hold path. ## Acceptance notes - The `connector_action` refusal (`builtin/connector-nodes.ts`) still ends with "the platform retries automatically" for every degraded husk, a held one included (a refused credential today, a configuration fault after this PR); the engine's `Connector registered DEGRADED` warn likewise says "the materializer retries with backoff". Both are outside this card's file surface, so the reason text names the fault but the sentence after it still promises a retry. - The other per-instance `fail()` arms in `reconcileDeclaredConnectors` (no provider factory, unparseable `retryConfig`, host credential resolver throwing) can also be met by an instance that is already degraded, after an edit; they keep the old skip-and-retry behaviour. This PR routes only the factory's configuration fault, which is the card's mechanism. - Not touched, as ordered: a misspelt narrowing `include` on a hot reload of a live connector still keeps the wider set serving. ## Verification All readings at head `fbe1de19`. <details><summary>Reproduction on main (red), fix (green), ablation, restore</summary> - On `main` (`46943657`'s `plugin.ts`, with the new test file): `Tests 4 failed | 2 passed (6)`. The `mcp` and `openapi` cases fail on the reason (`expected 'connector-mcp provider: connector …' to match /providerConfig\.include names 'nope'/`, the stale "could not reach" text), and the changed-entry case fails with `expected 67 to be 3`: 67 factory calls in 64 base delays. The two controls pass. - With the fix: `Tests 6 passed (6)`. - Ablation through `scripts/ablation-replace.mjs`, anchor `if (!opts.fatal && this.degradedInstances.has(name)) {` replaced by `if (false /* ABLATION */ && this.degradedInstances.has(name)) {`: anchor 1 to 0, blob 95e8c37 to 577056522a42; run `Tests 4 failed | 2 passed (6)` (the same four; both controls stay green); restore: blob after restore equals the blob at HEAD (95e8c37), `git diff HEAD` empty; re-run `Tests 6 passed (6)`. The test imports `./plugin.js` from source, so no `dist/` leg applies. </details> <details><summary>Package suite and typecheck</summary> - `pnpm --filter @objectstack/service-automation test`: `Test Files 222 passed (222)`, `Tests 2836 passed (2836)`. - `pnpm --filter @objectstack/service-automation typecheck`: exit 0 (`check:test-typecheck: OK`). </details> <details><summary>dispatch-gates families (60 derived, 60 run, 0 NOT-MEASURED) and pnpm lint</summary> Run after a whole-tree `turbo run build` (exit 0). `dispatch-gates --ran`: 60 derived families accounted for, 60 run, 0 NOT-MEASURED. ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/check-tenant-audit-census.mjs :: exit 0 node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:durability-log-level :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 pnpm lint :: exit 0 ``` </details> --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4czEP1ZwBWHusgCZugF)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 21b2a10 commit 066ff51

3 files changed

Lines changed: 292 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
fix(service-automation): a degraded connector instance whose backoff retry meets a configuration fault stops retrying and reports the configuration reason
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A declarative connector instance whose upstream was unreachable at boot is degraded and retried on a backoff (#3017). When the upstream later answered and the provider refused the instance's configuration (for example an `mcp` or `openapi` `providerConfig.include` naming a tool or operation the upstream does not offer), the fault was logged and skipped, but the instance stayed in the retry set: `GET /api/v1/automation/connectors` kept the stale "could not reach" reason, and the timer reconnected against a fault no retry can clear.
11+
12+
ADR-0097 does not retry a configuration fault, and now that holds on this path too:
13+
14+
- The instance's `degradedReason` becomes the provider's configuration message, and the retry timer no longer schedules it. An unchanged metadata reload does not re-attempt it; an edit of its declaration, or a restart, does.
15+
- When a previously-materialized connector is still serving (a changed entry whose new configuration was pending a retry), it keeps serving and the retry stops.
16+
- A credential the upstream refuses on the retry keeps its hold path, and an upstream still unreachable on the retry keeps degrading and retrying, as before.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// #27992 — a configuration fault met on a degraded instance's backoff retry.
4+
// The instance was degraded at boot (its upstream was unreachable, #3017);
5+
// once the upstream answers, the provider refuses the entry's configuration —
6+
// an `include` naming a tool (mcp) or an operation (openapi) the upstream does
7+
// not offer. ADR-0097 does not retry a configuration fault, so the retry ends
8+
// there: the stale "could not reach" reason is replaced by the configuration
9+
// one, and no further retry is scheduled. A credential refusal keeps its hold
10+
// path, and an unreachable upstream keeps degrading on the backoff.
11+
12+
import { describe, it, expect, afterEach, vi } from 'vitest';
13+
import { LiteKernel } from '@objectstack/core';
14+
import type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';
15+
import {
16+
ConnectorCredentialRejectedError,
17+
ConnectorUpstreamUnavailableError,
18+
} from '@objectstack/spec/integration';
19+
import { AutomationServicePlugin, DECLARATIVE_RETRY_BASE_MS } from './plugin.js';
20+
import type { AutomationEngine } from './engine.js';
21+
22+
type Upstream = 'down' | 'refuses-include' | 'refuses-credential' | 'up';
23+
24+
/**
25+
* A provider factory shaped like connector-mcp / connector-openapi: an
26+
* unreachable upstream throws the unavailable marker, an `include` entry the
27+
* upstream does not offer throws a plain Error (the provider's configuration
28+
* fault), a refused credential throws the credential marker.
29+
*/
30+
function makeProvider(kind: 'mcp' | 'openapi', upstream: Upstream) {
31+
const state = { upstream, calls: 0 };
32+
const factory: ConnectorProviderFactory = (ctx) => {
33+
state.calls++;
34+
if (state.upstream === 'down') {
35+
throw new ConnectorUpstreamUnavailableError(
36+
kind === 'mcp'
37+
? `connector-mcp provider: connector '${ctx.name}' could not reach its MCP server: connect ECONNREFUSED`
38+
: `connector-openapi provider: connector '${ctx.name}' could not reach spec URL 'https://api.example/spec.json': fetch failed`,
39+
);
40+
}
41+
if (state.upstream === 'refuses-credential') {
42+
throw new ConnectorCredentialRejectedError(
43+
`connector-${kind} provider: connector '${ctx.name}' was refused by its upstream (HTTP 401).`,
44+
401,
45+
);
46+
}
47+
const include = (ctx.providerConfig as { include?: string[] }).include ?? [];
48+
if (state.upstream === 'refuses-include' || include.includes('nope')) {
49+
throw new Error(
50+
kind === 'mcp'
51+
? `connector-mcp provider: connector '${ctx.name}' providerConfig.include names 'nope', which the MCP server does not list.`
52+
: `connector-openapi provider: connector '${ctx.name}' providerConfig.include names 'nope', which the spec does not define.`,
53+
);
54+
}
55+
return {
56+
def: {
57+
name: ctx.name,
58+
label: ctx.label,
59+
type: 'api',
60+
authentication: { type: 'none' },
61+
actions: [{ key: 'ping', label: 'Ping' }],
62+
} as unknown as Connector,
63+
handlers: { ping: async () => ({ ok: true }) },
64+
};
65+
};
66+
return { factory, state };
67+
}
68+
69+
function entry(name: string, provider: string, include: string[] = ['nope']) {
70+
return { name, label: name, type: 'api', provider, providerConfig: { url: 'https://upstream.example', include } };
71+
}
72+
73+
/** Boot under fake timers: the boot reconcile and every reload are awaited, no timer tick needed. */
74+
async function boot(initial: unknown[], provider: string, factory: ConnectorProviderFactory) {
75+
const state = { declared: initial };
76+
let captured: any;
77+
const kernel = new LiteKernel({ logger: { level: 'silent' } } as never);
78+
kernel.use(new AutomationServicePlugin());
79+
kernel.use({
80+
name: 'test.harness',
81+
type: 'standard' as const,
82+
version: '1.0.0',
83+
dependencies: ['com.objectstack.service-automation'],
84+
async init(ctx: any) {
85+
captured = ctx;
86+
ctx.registerService('objectql', {
87+
registry: { listItems: (type: string) => (type === 'connector' ? state.declared : []) },
88+
});
89+
ctx.getService('automation').registerConnectorProvider(provider, factory);
90+
},
91+
async start() {},
92+
} as never);
93+
await kernel.bootstrap();
94+
const reload = async (next: unknown[]) => {
95+
state.declared = next;
96+
await captured.trigger('metadata:reloaded');
97+
};
98+
return { kernel, engine: kernel.getService('automation') as AutomationEngine, reload };
99+
}
100+
101+
function descriptorOf(engine: AutomationEngine, name: string) {
102+
return engine.getConnectorDescriptors().find((d) => d.name === name);
103+
}
104+
105+
describe('#27992 — a configuration fault met on a degraded instance\'s backoff retry ends the retry', () => {
106+
afterEach(() => {
107+
vi.useRealTimers();
108+
});
109+
110+
for (const kind of ['mcp', 'openapi'] as const) {
111+
it(`${kind}: the instance takes the configuration reason and is not retried again`, async () => {
112+
vi.useFakeTimers();
113+
const { factory, state } = makeProvider(kind, 'down');
114+
const { kernel, engine } = await boot([entry('upstream', kind)], kind, factory);
115+
expect(state.calls).toBe(1);
116+
expect(engine.getConnectorDegradedReason('upstream')).toMatch(/could not reach/);
117+
118+
// The upstream answers now, and refuses the entry's include.
119+
state.upstream = 'refuses-include';
120+
await vi.advanceTimersByTimeAsync(DECLARATIVE_RETRY_BASE_MS);
121+
expect(state.calls).toBe(2); // the backoff retry ran and met the fault
122+
123+
// GET /connectors reads these descriptors: the stale reason is gone.
124+
const desc = descriptorOf(engine, 'upstream');
125+
expect(desc?.degradedReason).toMatch(/providerConfig\.include names 'nope'/);
126+
expect(desc?.degradedReason).not.toMatch(/could not reach/);
127+
expect(desc?.actions).toEqual([]);
128+
expect(engine.resolveConnectorAction('upstream', 'ping')).toBeUndefined();
129+
130+
// No retry is scheduled: a long wait reaches the factory no more.
131+
await vi.advanceTimersByTimeAsync(DECLARATIVE_RETRY_BASE_MS * 64);
132+
expect(state.calls).toBe(2);
133+
134+
await kernel.shutdown();
135+
});
136+
}
137+
138+
it('an unchanged reload does not re-attempt it; an edit of the declaration does, and can recover it', async () => {
139+
vi.useFakeTimers();
140+
const { factory, state } = makeProvider('mcp', 'down');
141+
const { kernel, engine, reload } = await boot([entry('upstream', 'mcp')], 'mcp', factory);
142+
state.upstream = 'up';
143+
await vi.advanceTimersByTimeAsync(DECLARATIVE_RETRY_BASE_MS);
144+
expect(state.calls).toBe(2);
145+
expect(engine.getConnectorDegradedReason('upstream')).toMatch(/include names 'nope'/);
146+
147+
await reload([entry('upstream', 'mcp')]);
148+
expect(state.calls).toBe(2);
149+
150+
await reload([entry('upstream', 'mcp', ['ping'])]);
151+
expect(state.calls).toBe(3);
152+
expect(descriptorOf(engine, 'upstream')?.state).toBe('ready');
153+
expect(engine.getConnectorDegradedReason('upstream')).toBeUndefined();
154+
155+
await kernel.shutdown();
156+
});
157+
158+
it('a changed entry pending a retry: the old connector keeps serving and the retry stops', async () => {
159+
vi.useFakeTimers();
160+
const { factory, state } = makeProvider('mcp', 'up');
161+
const { kernel, engine, reload } = await boot([entry('upstream', 'mcp', ['ping'])], 'mcp', factory);
162+
expect(descriptorOf(engine, 'upstream')?.state).toBe('ready');
163+
164+
state.upstream = 'down';
165+
await reload([entry('upstream', 'mcp', ['ping', 'pong'])]);
166+
expect(state.calls).toBe(2);
167+
168+
state.upstream = 'refuses-include';
169+
await vi.advanceTimersByTimeAsync(DECLARATIVE_RETRY_BASE_MS);
170+
expect(state.calls).toBe(3);
171+
expect(descriptorOf(engine, 'upstream')?.state).toBe('ready');
172+
expect(engine.resolveConnectorAction('upstream', 'ping')).toBeDefined();
173+
174+
await vi.advanceTimersByTimeAsync(DECLARATIVE_RETRY_BASE_MS * 64);
175+
expect(state.calls).toBe(3);
176+
177+
await kernel.shutdown();
178+
});
179+
180+
it('control: an upstream still unreachable on the retry keeps degrading and is retried again', async () => {
181+
vi.useFakeTimers();
182+
const { factory, state } = makeProvider('mcp', 'down');
183+
const { kernel, engine } = await boot([entry('upstream', 'mcp')], 'mcp', factory);
184+
185+
await vi.advanceTimersByTimeAsync(DECLARATIVE_RETRY_BASE_MS);
186+
expect(state.calls).toBe(2);
187+
expect(engine.getConnectorDegradedReason('upstream')).toMatch(/could not reach/);
188+
189+
await vi.advanceTimersByTimeAsync(DECLARATIVE_RETRY_BASE_MS * 2);
190+
expect(state.calls).toBe(3);
191+
192+
await kernel.shutdown();
193+
});
194+
195+
it('control: a credential refused on the retry keeps its hold path', async () => {
196+
vi.useFakeTimers();
197+
const { factory, state } = makeProvider('mcp', 'down');
198+
const { kernel, engine } = await boot([entry('upstream', 'mcp')], 'mcp', factory);
199+
200+
state.upstream = 'refuses-credential';
201+
await vi.advanceTimersByTimeAsync(DECLARATIVE_RETRY_BASE_MS);
202+
expect(state.calls).toBe(2);
203+
expect(engine.getConnectorDegradedReason('upstream')).toMatch(/refused by its upstream \(HTTP 401\)/);
204+
205+
// Held: a credential written into the declaration waits for its next change.
206+
await vi.advanceTimersByTimeAsync(DECLARATIVE_RETRY_BASE_MS * 64);
207+
expect(state.calls).toBe(2);
208+
209+
await kernel.shutdown();
210+
});
211+
});

‎packages/services/service-automation/src/plugin.ts‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,10 @@ export class AutomationServicePlugin implements Plugin {
742742
* it. A refused value the door does not hold (an environment variable,
743743
* the host's resolver) is never held: it can change where this process
744744
* cannot see it, so its instance retries on the backoff.
745+
*
746+
* [#27992] A degraded instance whose provider answers with a configuration
747+
* fault is held the same way, with the configuration reason: only a change
748+
* of its declaration or a restart re-attempts it.
745749
*/
746750
private degradedInstances = new Map<string, { attempts: number; signature: string; reason: string; held?: boolean }>();
747751
/**
@@ -2355,6 +2359,21 @@ export class AutomationServicePlugin implements Plugin {
23552359
});
23562360
continue;
23572361
}
2362+
// [#27992] A configuration fault met by an instance that is
2363+
// degraded — its backoff retry, or a reload — ends the retry:
2364+
// ADR-0097 does not retry a configuration fault.
2365+
if (!opts.fatal && this.degradedInstances.has(name)) {
2366+
this.failDegradedConnectorInstance(engine, ctx, {
2367+
name,
2368+
entry,
2369+
provider,
2370+
signature,
2371+
hasLive: existing !== undefined,
2372+
reason: thrownMessageText(thrown),
2373+
cause: thrown,
2374+
});
2375+
continue;
2376+
}
23582377
// #5575 — a provider factory is third-party code ADR-0097 invites
23592378
// people to write, and the first one that validates its
23602379
// `providerConfig` with Zod throws a message whose first line is
@@ -2517,6 +2536,52 @@ export class AutomationServicePlugin implements Plugin {
25172536
);
25182537
}
25192538

2539+
/**
2540+
* [#27992] End the retry of a degraded instance whose provider now answers
2541+
* with a configuration fault (an `include` naming what the upstream does
2542+
* not offer): ADR-0097 does not retry a configuration fault, and the
2543+
* "could not reach" reason the husk carried is no longer true. The
2544+
* instance is held — neither the retry timer nor an unchanged reload
2545+
* re-attempts it; a change of its declaration or a restart does, as for a
2546+
* refused credential — and its husk (unless a previously-materialized
2547+
* connector keeps serving) now carries the configuration reason.
2548+
*/
2549+
private failDegradedConnectorInstance(
2550+
engine: AutomationEngine,
2551+
ctx: PluginContext,
2552+
info: {
2553+
name: string;
2554+
entry: DeclaredConnectorItem;
2555+
provider: string;
2556+
signature: string;
2557+
hasLive: boolean;
2558+
reason: string;
2559+
cause: unknown;
2560+
},
2561+
): void {
2562+
this.degradedInstances.set(info.name, { attempts: 1, signature: info.signature, reason: info.reason, held: true });
2563+
if (!info.hasLive) {
2564+
try {
2565+
engine.registerDegradedConnector(this.buildDegradedHuskDef(info.name, info.entry), info.reason, 'declarative', info.cause);
2566+
} catch (err) {
2567+
ctx.logger.warn(
2568+
`[Automation] could not register degraded husk for '${info.name}' — the instance stays absent from the ` +
2569+
'connector registry until its declaration changes.',
2570+
describeThrownForLog(err),
2571+
);
2572+
}
2573+
}
2574+
// #5575 — the provider's message rides `meta`, never the message.
2575+
ctx.logger.error(
2576+
`[Automation] failed to materialize connector instance '${info.name}' via provider '${info.provider}' (ADR-0097): ` +
2577+
'a configuration fault, so the instance is no longer retried. ' +
2578+
(info.hasLive ? 'The previously-materialized connector keeps serving. ' : '') +
2579+
'Fix its declaration; an edit or a restart re-attempts it.',
2580+
undefined,
2581+
describeThrownForLog(info.cause),
2582+
);
2583+
}
2584+
25202585
/**
25212586
* [#27237] The `credentialRef` chain this plugin resolves through: env →
25222587
* the store → the host's resolver. Under a walled tenancy posture

0 commit comments

Comments
 (0)