Skip to content

Commit fc29c74

Browse files
os-warrenclaude
andauthored
feat(spec)!: retire connector.connectionTimeoutMs — carried everywhere, applied nowhere (#19657)
Fixes #19580 `Clause-②: yes (narrowing)` Ruled: comment `5770606746`, batch #211 item 1, **letter A** — retire `connector.connectionTimeoutMs` (ADR-0049 enforce-or-remove; the standing 2026-09-10 「以协议为准」 ruling; the 2026-08-27 「no staged retirement」). Removal route: the `spec-property-retirement` playbook. ## What this removes A **fully authorable** key — bounded (`min(1000).max(300000)`), defaulted (`30000`), `.describe()`d, writable on `ConnectorSchema` and, through `DeclarativeConnectorEntrySchema`, on `stack.connectors[]` and `PUT /meta/connector/:name`, and served back by `/meta/connector`. Every signal an authoring surface can give said it worked. Plus the `ConnectorProviderContext.connectionTimeoutMs` member handed to every provider factory. `requestTimeoutMs` is the replacement: the deadline the platform actually keeps, applied as `resilientFetch`'s per-attempt timeout. ## ⭐ The measurement the ruling left to the dev — D2 or D3 The ruling prescribed a **D3 semantic** entry and said a **D2 conversion** is owed **only if a stored connector row can carry the key** — 「the dev measures」. It can, so **both** ship. Measured first-hand on this branch, before the tombstone landed, against the built `packages/spec/dist`: | leg | reading | | --- | --- | | `getMetadataTypeSchema('connector')` bound? | `true` — it resolves `DeclarativeConnectorEntrySchema`, the shape `PUT /api/v1/meta/connector/:name` validates against | | door parses a body carrying the key? | `true` | | does its **output retain** the authored value? | **`4321`** — so the number reaches `sys_metadata` | | control on the same instrument | the already-retired sibling `errorMapping` on the same schema is **refused** — the door discriminates rather than accepting everything | | `applyConversionsToStoredItem('connector', row)` live for this type? | **yes** — it fired `connector-error-mapping-removed` and stripped that key from a stored row | | control on the same seam | the same row's `requestTimeoutMs` survived untouched, so the strip is attributable | **What would have made it the other answer:** `getMetadataTypeSchema('connector')` returning `undefined` (no write door ⇒ no stored row), or the door's output dropping the key, or the rehydration seam never reaching `connector` rows. All three fired the other way, so a D2 conversion is owed and a D3-only kit would have left 17.x rows carrying a key the schema now refuses. Both dispositions are re-measured by pins in `packages/spec/src/integration/connector-connection-timeout-retirement.test.ts`, so the answer cannot rot into an assumption. ## ⚠️ The card is STALE, not wrong — and this section previously said otherwise ⛔ This section's earlier premise is known false and is replaced rather than patched. It claimed the card's Leg-2 table 「was already false when this retirement was taken」. It was not. **At the SHA the card cites and dates — `0870fb5418`** — `git grep -n connectionTimeoutMs SHA -- . ':!packages/spec'` returns **exactly five** non-spec source hits, and **all five are `connectionTimeoutMs: 30000,`**: the card's table, line for line. ⇒ the card was **correct when measured**. What moved it is `b929e0a662` (#19388) — the very PR the card itself flagged as pending. **At `origin/main`** the same instrument returns **thirteen** non-test source occurrences over **seven files** in **five packages**: six reads, four type declarations, and three surviving pure hardcoded `30000` writes (`connector-mcp/src/mcp-connector.ts`, `connector-slack/src/slack-connector.ts`, `service-automation/src/plugin.ts`). ⚠️ Seven files, not five — five is the count of *packages*, and conflating the two is how the earlier number was reached. | site | what it is | | --- | --- | | `services/service-automation/src/plugin.ts:307` | reads `entry.connectionTimeoutMs` into the **materialization fingerprint** | | `services/service-automation/src/plugin.ts:1589` | reads it onto `ConnectorProviderContext` | | `connectors/connector-rest/src/rest-provider.ts:64` | reads `ctx.connectionTimeoutMs` | | `connectors/connector-openapi/src/openapi-provider.ts:193` | reads `ctx.connectionTimeoutMs` | | `connector-rest/src/rest-connector.ts:134`, `connector-openapi/src/openapi-connector.ts:242` | `?? 30000` — read the opts and deposit the value on the reported def | **The ruling's premise survives, and the mechanism is unchanged.** Every read is a **pass-through**. The value's only termini are (a) the def `GET /connectors` echoes and (b) the fingerprint that decides whether to re-materialize. `connectorFetchOptions()` (`integration/connector-fetch-policy.ts`) is handed `{ retryConfig, requestTimeoutMs }` only, and a pin has asserted since #18975 that nothing aliases this key onto `timeoutMs`. **Carrying a number is not honouring it** — the parsed-unmarked-unenforced state ADR-0049 forbids, wearing a longer route. Nor was the `实现` arm available: a WHATWG `fetch` exposes one `AbortSignal` over the whole operation and never the connect phase, so bounding time-to-response with this key would kill a slow-but-connected upstream the author meant to allow with a large `requestTimeoutMs`. ## Zero-enforcement verification, with its control - **Claim:** nothing applies the value as a deadline. **Instrument:** `git grep -n connectionTimeoutMs` over the whole worktree (45 hits, hand-read, not counted) plus the source of `connectorFetchOptions()`. **Radius:** the monorepo. **Control:** `requestTimeoutMs` — same schema, same census, same files — resolves to a real read (`opts.timeoutMs = policy.requestTimeoutMs`), so the instrument is not dead. - **Pinned sibling checkout:** `git grep connectionTimeoutMs` at objectui `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` (the `.objectui-sha` pin) → **exit 1, zero hits**; control `connector` on the same command and scope returns **458 lines across 66 files**; and `requestTimeoutMs` is exit 1 / 0 lines there, so it is **not** a usable control in that repo (it is in objectstack). ⇒ the `Console Pin Gate` needs no sibling fix and no pin bump with this removal. ⚠️ **That clearance is about the sibling BUILD, and it is not the whole picture.** The residue stage makes both carriers `z.preprocess` pipes, and objectui's `packages/app-shell/src/views/metadata-admin/clientValidation.optOuts.test.ts:468` asserts `checks(DeclarativeConnectorEntrySchema) > 0` — **1** against `main`, **0** here, because a pipe def has no `checks` array. The SPA still builds and `Console Pin Gate` never runs that suite, so no gate in either repo sees it. objectui resolves `@objectstack/spec` from the registry at `^17.0.0`, so ⛔ `main` does not go red on merge — the break lands at objectui's next spec bump. Tracked at **objectui#10211**; the gate-reach gap at **objectstack#19692**. ⛔ A `.objectui-sha` bump is never a rider on another PR, so neither rides here. - **tsc is the real sweeper.** `retiredKey()` types the key `never`, so every authoring site in the monorepo fails to compile. All six affected packages typecheck green after the cleanup, which is what says the census is complete rather than the grep. ## The retirement kit - `retiredKey()` tombstone on the non-strict `ConnectorSchema` (a bare delete would be a silent strip, ADR-0104), inherited by `DeclarativeConnectorEntrySchema`. - `RETIRED_KEYS_BY_MAJOR[18]` × 2 — `integration/Connector:connectionTimeoutMs` and `integration/DeclarativeConnectorEntry:connectionTimeoutMs` — as one-file-per-entry under `migrations/entries/retired-keys/`. - **D2** `connector-connection-timeout-ms-removed` in `conversions/registry.ts`, wired into the step-18 chain. - ⭐ **ADR-0087 residue stage** `acceptRetiredDefaultResidue` on **both** carriers with `{ connectionTimeoutMs: 30000 }`. A D2 does **not** discharge this: the ruled precedent `18.security__ObjectPermission__allowPurge` carries **both** a D2 (`permission-allow-restore-purge-removed`) **and** the residue stage, so D2 coverage cannot be the discriminator. The discriminator is whether a released toolchain MATERIALIZED the default — a 17.x toolchain emits `connectionTimeoutMs: 30000` into every connector entry, authored or not — and the second door is `AutomationEngine.registerConnector`, which parses `ConnectorSchema` for a def a plugin builds **in code**, where no conversion runs. Without the stage a 17.x connector package fails registration on a value its author never typed. Head now accepts-and-strips `30000` while still refusing `15000`, `1000` and `"30000"`. The preprocess pipe this introduces moves **five** ADR-0097 refinement sites onto its OUT side, so `dropped-refinements.baseline.json` moves with them — exactly the moves the build gate printed, no additions. - **D3 semantic** `connector-provider-context-connection-timeout-ms-retired` for the withdrawn `ConnectorProviderContext` member — a provider factory is code, so there is no authored source for a conversion to rewrite. - `liveness/connector.json`: the row **stays** `dead` with a `REMOVED` note, because `retiredKey()` keeps the key in the walked shape (the `rls.priority` precedent). Its stale 「every occurrence outside `packages/spec` is a WRITE」 claim is corrected there, with the reads named. - Baselines: `authorable-surface/integration.json` gains two `[RETIRED]` rows, `authorable-defaults/integration.json` loses the two `= 30000` rows. `api-surface/` and `json-schema.manifest/` are **byte-identical** — the correct reading for a key-only tombstone that retires no def, not a missed regeneration. - Consumers cleaned: the four connector packages and `service-automation` (fingerprint, declared-item shape, context build, degraded husk). - **Declared widening, round 4:** `packages/spec/liveness/README.md`'s `connector` row asserted, present tense, that the entry schema **is** `ConnectorSchema.superRefine(...)` — and rested its byte-identical-key-set conclusion **on** that attachment. Both halves are corrected: the mechanism is now the pipe's read-through `shape`, and the conclusion is re-measured rather than inherited (30 keys each carrier, byte-identical, zero entry-only, zero base-only). ⚠️ Hand-edited on purpose, ⛔ never regenerated: `.gitattributes:71-77` splits `liveness/state-counts.md` (driver-managed numbers) from `liveness/README.md` (hand-written Notes prose), because 「regenerating a Note would fabricate a verdict」. - ⚠️ **A SECOND, declared narrowing: the `ZodObject` combinators leave both published exports.** Wrapping `ConnectorSchema` and `DeclarativeConnectorEntrySchema` in the residue stage makes them `z.preprocess` **pipes**, so `.extend()`, `.omit()`, `.pick()`, `.partial()`, `.merge()`, `.strict()`, `.keyof()` and `.safeExtend()` no longer exist on them. Build on the object and re-wrap — `acceptRetiredDefaultResidue(<the extended object>, { connectionTimeoutMs: 30000 })`, the `EffectiveObjectPermissionSchema` route. ⚠️ `.superRefine()` still **exists** on a pipe and is callable, but returns a schema with **no read-through `shape`** — which is exactly what the schema walkers duck-test — so refine before wrapping, never after. Parsing, `z.input` / `z.infer` and the read-through `.shape` are unchanged. The changeset's FROM → TO carries this row; the docblock at `connector.zod.ts` and the superseded sentence it replaces carry it in the source. - Changeset `Clause-②: yes (narrowing)`, `minor` on `@objectstack/spec` (the launch-window gate refuses `major`), `patch` on the five consumer packages, with the FROM → TO table and the ADR-0087 disposition marker. ## Tests and gates run locally | run | verdict | | --- | --- | | `pnpm --filter @objectstack/spec build` | exit 0 | | dependency-closure build of the five consumer packages | exit 0 | | `typecheck` × 6 (`spec`, `connector-rest`, `connector-openapi`, `connector-mcp`, `connector-slack`, `service-automation`) | exit 0 | | `test` × 5 consumer packages | 1786 passed | | spec `src/integration src/conversions src/migrations` + the migrate-sentence and cron pins | 569 passed / 16 files | | spec `test:repo` | **600 passed** | | full `pnpm build` + the re-derived 112-command gate sweep on this head | **112 / 112**, and on the latest round with **zero** prerequisite failures, because the full build ran first (earlier rounds had three first-pass non-zeros, all `PREREQUISITE NOT MET` from unbuilt packages ⇒ read as NOT MEASURED, ⛔ never as failures, and re-run green after the build) | | the new retirement pin under `--project repo` | **15 passed** | | `check:generated` | **15 of 15** green, `check:docs` and `check:liveness` included | | the 14 source audits `check:generated` names as not run | all exit 0, each captured separately | | `check:nul-bytes`, `check:cross-package-test-inputs`, `check:adr-0087-registration`, `check:changeset-no-major` | exit 0 | Every exit code above was captured before any pipe. The repo-wide gate farm is CI's run, not this PR's local obligation. ## Acceptance notes - **Scope deviation, declared.** The dispatch fenced `content/docs/**` off. `content/docs/references/integration/connector.mdx` is an **auto-generated** baseline whose gate (`check:docs`) is inside the required `TypeScript Type Check` job, and it goes stale on this change alone. Measured across all 19 open PRs (283 file rows, 0 unreadable): **zero** hold that path, so the fence's stated reason — "open PRs hold files there" — does not apply to it; the instrument discriminates, returning `content/docs` rows for seven other PRs. It is regenerated here, exactly as the sibling retirement #19618 regenerates four of the same tree's pages. No hand-written `content/docs/**` prose is touched, and `skills/**` and `.claude/**` are untouched — this diff hits **no governed surface**. - **Scope, mechanically forced.** The tombstone types the key `never`, so the four connector packages and `service-automation` must stop writing it or the monorepo does not compile. Those paths are outside the claim's declared file surface and are held by **zero** open PRs on the same census. - **The two-writer surface materialised as declared** — see the report. - `packages/spec/vitest.repo-tests.json` gains one line: the new tree-scoped absence pin's walk radius, which `check:cross-package-test-inputs` demanded by name. No new glob; the radius was already declared for this package. - The `connectionTimeoutMs`-is-never-mapped pin in `connector-fetch-policy.test.ts` is **kept** after the retirement, deliberately: it is what makes a re-introduction as a silent alias onto `timeoutMs` fail. - `health.circuitBreaker` remains `dead` on this schema and is **not** touched here — a different set of rows on the same ADR-0049 worklist. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e07843b commit fc29c74

31 files changed

Lines changed: 1429 additions & 134 deletions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/connector-rest': patch
4+
'@objectstack/connector-openapi': patch
5+
'@objectstack/connector-mcp': patch
6+
'@objectstack/connector-slack': patch
7+
'@objectstack/service-automation': patch
8+
---
9+
10+
feat(spec)!: retire `connector.connectionTimeoutMs` — declared, bounded, defaulted, served back, and never applied as a deadline
11+
12+
**BREAKING** — `connector.connectionTimeoutMs` is removed. ADR-0049
13+
enforce-or-remove; maintainer ruling 2026-09-22, letter A. It is the narrower
14+
**second** decision this key was owed: the earlier ruling that made its nine
15+
liveness siblings live (`retryConfig.*`, `requestTimeoutMs`) left this one dead
16+
on a stated reason rather than by oversight, and `packages/spec/liveness/connector.json`
17+
has been asking for this decision since.
18+
19+
The key was bounded (`min(1000).max(300000)`), defaulted (`30000`),
20+
`.describe()`d, authorable on both carriers and served back by
21+
`/meta/connector`. Every signal an authoring surface can give said it worked.
22+
23+
### FROM → TO
24+
25+
| removed | what to write instead |
26+
| --- | --- |
27+
| `connector.connectionTimeoutMs` (on `Connector` and on `DeclarativeConnectorEntry`, so `stack.connectors[]` and `PUT /meta/connector/:name`) | `requestTimeoutMs` — the deadline the platform keeps, applied as `resilientFetch`'s per-attempt timeout. For a connect-only bound, configure it at a connector provider or upstream gateway on a transport that can separate the phases. |
28+
| `ConnectorProviderContext.connectionTimeoutMs` (handed to every `ConnectorProviderFactory`) | `ctx.requestTimeoutMs`, or the factory's own `providerConfig` where the provider owns the vocabulary. |
29+
| The `ZodObject` combinators on `ConnectorSchema` and `DeclarativeConnectorEntrySchema` — `.extend()`, `.omit()`, `.pick()`, `.partial()`, `.merge()`, `.strict()`, `.keyof()`, `.safeExtend()` | Both exports are now `z.preprocess` **pipes** (the residue stage below), so those methods no longer exist on them. **Build on the object and re-wrap:** `acceptRetiredDefaultResidue(<your extended object>, { connectionTimeoutMs: 30000 })`, the `EffectiveObjectPermissionSchema` route. ⚠️ `.superRefine()` still *exists* on a pipe but returns a schema with no read-through `shape`, so refine before wrapping, not after. Parsing, `z.input` / `z.infer`, and the read-through `.shape` are unchanged. |
30+
31+
**The one-line fix: delete the key** — and, for a custom provider factory, stop
32+
reading `ctx.connectionTimeoutMs`. `os migrate meta --from 17` lists the
33+
mechanical edits for existing sources; apply them by hand.
34+
35+
⚠️ Runtime behaviour is **unchanged for every shipped provider**, because none
36+
ever applied the value: a connector that authored `connectionTimeoutMs: 1000`
37+
made exactly the same calls, with exactly the same deadlines, as one that did
38+
not. What does change is observable and intended: the def served by
39+
`GET /connectors` no longer echoes a connect deadline nobody keeps.
40+
41+
### ⭐ This is NOT the zero-mention retirement shape
42+
43+
Measured with `git grep -n connectionTimeoutMs SHA -- . ':!packages/spec'` at
44+
`origin/main`: **thirteen** non-test source occurrences over seven files in five
45+
packages — **six reads** (`openapi-connector.ts:242`, `openapi-provider.ts:193`,
46+
`rest-connector.ts:134`, `rest-provider.ts:64`, `plugin.ts:307`,
47+
`plugin.ts:1589`), **four type declarations**, and **three** surviving hardcoded
48+
`30000` writes. Reading the retirement as "nothing referenced it" loses the
49+
finding. Measured across all six reads, every one is a **pass-through**: the
50+
value's only termini were the def `GET /connectors` echoes and the fingerprint
51+
that decides whether to re-materialize. `connectorFetchOptions()` — the one
52+
mapping from authored policy onto the platform's outbound `fetch` — was handed
53+
`{ retryConfig, requestTimeoutMs }` only. Carrying a number is not honouring it,
54+
and ADR-0049 forbids the parsed-unmarked-unenforced state whether the inert
55+
value travels or sits still.
56+
57+
Nor was the `实现` arm available. A connector's outbound call is a WHATWG
58+
`fetch`, whose only cancellation surface is ONE `AbortSignal` covering the whole
59+
operation; nothing in that interface observes the connection phase. Bounding
60+
"time until the response arrives" with this key would kill a slow-but-connected
61+
upstream the author meant to allow with a large `requestTimeoutMs` — breaking
62+
the very promise the key makes. (undici's `connectTimeout` needs a custom
63+
dispatcher: Node-only, and a new subsystem underneath every connector, which the
64+
ruling that made the siblings live forbids.)
65+
66+
### The retirement kit
67+
68+
- The **authorable key** is a `retiredKey()` tombstone on `ConnectorSchema`,
69+
registered as `integration/Connector:connectionTimeoutMs` and
70+
`integration/DeclarativeConnectorEntry:connectionTimeoutMs` in
71+
`RETIRED_KEYS_BY_MAJOR[18]`. The schema is not `.strict()`, so a bare deletion
72+
would strip an authored key in silence (ADR-0104): the tombstone is audible in
73+
both channels — `tsc` (input type `never`) and the parse, which raises the
74+
prescription itself. `DeclarativeConnectorEntrySchema` carries it too — both
75+
published carriers wrap the same private `ConnectorBaseSchema` — so
76+
`stack.connectors[]` and the `/meta/connector` door refuse it too.
77+
- **A D2 conversion, `connector-connection-timeout-ms-removed`** — one strip per
78+
`connectors[]` entry, a pure lossless delete. ⭐ The ruling left whether one was
79+
owed to be **measured** ("a D2 conversion only if a stored connector row can
80+
carry the key"). It can, and both legs were measured before the tombstone
81+
landed: `getMetadataTypeSchema('connector')` — what `PUT /meta/connector/:name`
82+
validates against — parsed a body carrying the key and its output **retained**
83+
the authored value, so the number reached `sys_metadata`; and
84+
`applyConversionsToStoredItem('connector', …)` is live for this type. Rows
85+
written on 17.x therefore replay clean.
86+
- **A D3 semantic entry,
87+
`connector-provider-context-connection-timeout-ms-retired`**, for the withdrawn
88+
`ConnectorProviderContext` member. A provider factory is code: there is no
89+
authored source and no `sys_metadata` row for a conversion to rewrite, so the
90+
removal reaches a factory author as a `tsc` error and as that entry.
91+
- **No def leaves.** The key was a bare `z.number()`, never a `ConfigSchema`
92+
shape, so `RETIRED_DEFS_BY_MAJOR[18]` gains nothing — and `api-surface/` and
93+
`json-schema.manifest/` are byte-identical, which is the correct reading for a
94+
key-only tombstone rather than a missed regeneration.
95+
- `authorable-surface/integration.json` gains two `[RETIRED]` rows;
96+
`authorable-defaults/integration.json` loses the two `= 30000` rows.
97+
- The liveness row **stays** `dead` with a `REMOVED` note, because `retiredKey()`
98+
keeps the key in the walked shape. Its previous note claimed "every occurrence
99+
outside `packages/spec` is a WRITE". That reading was **correct at the SHA the
100+
card cited and dated** (`0870fb5418` — exactly five non-spec source hits, all
101+
five `connectionTimeoutMs: 30000,`) and was superseded by `b929e0a662`, the PR
102+
the card itself flagged as pending. It is **stale, not false**, and the row now
103+
carries both readings with their trees rather than one undated claim.
104+
- **An `acceptRetiredDefaultResidue` stage** (#12840), `{ connectionTimeoutMs: 30000 }`
105+
on both carriers. The key was `.optional().default(30000)`, so a 17.x parse
106+
materialized it into **every** connector — measured across two builds: the base
107+
build emits it for an entry that authored only `name`/`label`/`type`, and the
108+
tombstoned build refuses that exact object at `connectors.0.connectionTimeoutMs`.
109+
The D2 does **not** discharge the obligation, and the precedent shows it:
110+
`ObjectPermission:allowPurge` carries a D2 **and** the residue stage, for its
111+
own reason (a released toolchain materialized its default into every built
112+
artifact's entries). The reason *here* is a different one — this schema has a
113+
second door: `AutomationEngine.registerConnector` parses `ConnectorSchema` for
114+
a def a plugin or provider factory builds **in code**, where no conversion
115+
ever runs, and all four shipped connector packages put the materialized value
116+
straight into that def literal. So the emitted `30000`
117+
is accepted-and-stripped while `15000` keeps the tombstone's refusal, and
118+
nothing is un-retired: `z.input` stays `never` and the `[RETIRED]` row stays.
119+
- **No deprecation window** (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」),
120+
and no staged retirement.
121+
122+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
123+
is published, so this is breaking for consumers no download, dependent or source
124+
telemetry was consulted for. The pinned sibling checkout **was** measured: zero
125+
occurrences of the name at objectui `87af769e`, against a lit control on the same
126+
command and scope, so no sibling fix or pin bump rides with this.
127+
128+
`Clause-②: yes (narrowing)` — a published authorable key is removed on two
129+
carriers and a published interface member leaves `ConnectorProviderContext`, so
130+
the accept set a consumer writes against narrows. Nothing is widened and nothing
131+
is renamed. Contract-review tier.
132+
133+
<!-- adr-0087: registered connector-connection-timeout-ms-removed, connector-provider-context-connection-timeout-ms-retired -->

‎content/docs/references/integration/connector.mdx‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,20 @@ construction. ⚠️ Retrying a `429` is not throttling it: a retry policy
5555
spaces out the calls you already made, it does not cap the rate, so the
5656
sentence above about rate limiting stands unchanged.
5757

58-
⛔ **Two exceptions, both still inert and both still `dead` in
59-
`packages/spec/liveness/connector.json`.** `health.circuitBreaker`: every
60-
sub-key is unread and no breaker ever opens — implement circuit breaking in
61-
the connector provider. `connectionTimeoutMs`: it is carried to a provider
62-
factory but the platform does not enforce it, because a WHATWG `fetch`
58+
⛔ **One exception remains, still inert and still `dead` in
59+
`packages/spec/liveness/connector.json`:** `health.circuitBreaker` — every
60+
sub-key is unread and no breaker ever opens; implement circuit breaking in
61+
the connector provider.
62+
63+
`connectionTimeoutMs` used to be the second exception and is now **removed**
64+
(ADR-0049, the narrower second decision that surface was owed): it was
65+
carried to a provider factory but never applied as a deadline anywhere, and
66+
it is not implementable where it was declared, because a WHATWG `fetch`
6367
exposes one `AbortSignal` over the whole operation and never the connection
64-
phase alone; `requestTimeoutMs` is the bound the platform can keep, and
65-
ADR-0049 owes this one key a narrower decision. The full removal reasoning
66-
for the rate-limit shape is recorded at the removal site: the "REMOVED:
67-
outbound rate limiting" block in `integration/connector.zod.ts`, and
68-
`packages/spec/docs/SYNC_ARCHITECTURE.md`.
68+
phase alone. `requestTimeoutMs` is the bound the platform can keep. The full
69+
removal reasoning is recorded at each removal site: the "REMOVED:
70+
`connectionTimeoutMs`" and "REMOVED: outbound rate limiting" blocks in
71+
`integration/connector.zod.ts`, and `packages/spec/docs/SYNC_ARCHITECTURE.md`.
6972

7073
**Field mapping does not transform values.** This header used to offer "field
7174
mapping and transformations"; only the first half was ever true.
@@ -200,7 +203,7 @@ Circuit breaker configuration
200203
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations |
201204
| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
202205
| **retryConfig** | `{ strategy: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts: number; initialDelayMs: number; maxDelayMs: number; … }` | optional | Retry configuration |
203-
| **connectionTimeoutMs** | `number` | optional (default: `30000`) | Connection timeout in ms |
206+
| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
204207
| **requestTimeoutMs** | `number` | optional (default: `30000`) | Request timeout in ms |
205208
| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional (default: `"inactive"`) | Connector status |
206209
| **enabled** | `boolean` | optional (default: `true`) | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor. |
@@ -684,7 +687,7 @@ Connector type
684687
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations |
685688
| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
686689
| **retryConfig** | `{ strategy: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts: number; initialDelayMs: number; maxDelayMs: number; … }` | optional | Retry configuration |
687-
| **connectionTimeoutMs** | `number` | optional (default: `30000`) | Connection timeout in ms |
690+
| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
688691
| **requestTimeoutMs** | `number` | optional (default: `30000`) | Request timeout in ms |
689692
| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional (default: `"inactive"`) | Connector status |
690693
| **enabled** | `boolean` | optional (default: `true`) | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor. |

‎packages/connectors/connector-mcp/src/mcp-connector.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,9 @@ export async function createMcpConnector(opts: McpConnectorOptions): Promise<Mcp
244244
// the (post-parse) Connector output type.
245245
status: 'active',
246246
enabled: true,
247-
connectionTimeoutMs: 30000,
247+
// `connectionTimeoutMs` — REMOVED with the spec key (ADR-0049): it was
248+
// written here only so the literal satisfied the post-parse type, and
249+
// the platform never applied it as a connect deadline.
248250
requestTimeoutMs: 30000,
249251
actions: selected.map((tool) => ({
250252
key: tool.name,

‎packages/connectors/connector-openapi/src/openapi-connector.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,12 @@ export interface OpenApiConnectorConfig {
127127
* (ADR-0049 · #18975). Omitted ⇒ the wrapper's own defaults.
128128
*/
129129
retryConfig?: RetryConfig;
130-
/**
131-
* Declared connect deadline (ms). Carried onto the def so `GET /connectors`
132-
* reports what the author declared; ⚠️ not enforced — one `fetch` signal
133-
* cannot bound the connection phase alone (`connector-fetch-policy.ts`).
134-
*/
135-
connectionTimeoutMs?: number;
130+
// `connectionTimeoutMs` — REMOVED with the spec key (ADR-0049). It was
131+
// accepted here only to be carried onto the def `GET /connectors` echoes:
132+
// one `fetch` signal cannot bound the connection phase alone, so it never
133+
// reached `connectorFetchOptions` and never bounded a call. Echoing a
134+
// deadline nobody keeps is what the retirement withdraws (mirrors
135+
// connector-rest).
136136
/** Per-request deadline (ms) — `resilientFetch`'s per-attempt timeout. */
137137
requestTimeoutMs?: number;
138138
/** Injected fetch implementation (defaults to global `fetch`). */
@@ -239,7 +239,6 @@ export function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiC
239239
// the (post-parse) Connector output type (mirrors connector-rest/mcp).
240240
status: 'active',
241241
enabled: true,
242-
connectionTimeoutMs: config.connectionTimeoutMs ?? 30000,
243242
requestTimeoutMs: config.requestTimeoutMs ?? 30000,
244243
...(config.retryConfig === undefined ? {} : { retryConfig: config.retryConfig }),
245244
actions,

‎packages/connectors/connector-openapi/src/openapi-provider.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,6 @@ export function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): Co
190190
// ADR-0049 · #18975 — the authored resilience policy, already resolved by
191191
// the materializer, reaches the transport this bundle closes over.
192192
retryConfig: ctx.retryConfig,
193-
connectionTimeoutMs: ctx.connectionTimeoutMs,
194193
requestTimeoutMs: ctx.requestTimeoutMs,
195194
fetchImpl: deps.fetchImpl,
196195
});

0 commit comments

Comments
 (0)