Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d164f4d
wip(spec): tombstone connector.connectionTimeoutMs and withdraw the p…
claude Sep 22, 2026
5d1e9e0
feat(spec)!: retire connector.connectionTimeoutMs — D2 conversion, D3…
claude Sep 22, 2026
d7917c1
chore(spec): regenerate migration registry, authorable baselines and …
claude Sep 22, 2026
a0eef99
test(spec): narrow the tree-scoped absence matcher to authoring shapes
claude Sep 22, 2026
dbcc890
docs(changeset): declare the connector.connectionTimeoutMs retirement
claude Sep 22, 2026
d0db2dd
test(spec): declare the retirement pin's walk radius on the repo project
claude Sep 22, 2026
9a86511
Merge remote-tracking branch 'origin/main' into claude/issue-19580-re…
claude Sep 22, 2026
f8c23b8
fix(spec): cite the live record for requestTimeoutMs instead of a del…
claude Sep 22, 2026
6f14491
fix(spec): accept the retired connectionTimeoutMs default as residue;…
claude Sep 22, 2026
4ca3957
docs(spec): retire two false claims the residue pipe left behind
claude Sep 22, 2026
8cdcafc
docs(spec): correct the connector row's walk mechanism in the livenes…
claude Sep 22, 2026
c33220b
docs(spec): re-measure the whole connector liveness row against this …
claude Sep 22, 2026
14fdebd
docs(spec): the connector row's falsification note was overtaken and …
claude Sep 22, 2026
0d119d5
docs(spec): pin the connector row's retryConfig census to its tree an…
claude Sep 22, 2026
6577881
docs(spec): correct the connector ledger's key-reach and authenticati…
claude Sep 22, 2026
9e2843a
docs(spec): re-derive the connector ledger's claims against their ins…
claude Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions .changeset/19580-retire-connector-connection-timeout-ms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
'@objectstack/spec': minor
'@objectstack/connector-rest': patch
'@objectstack/connector-openapi': patch
'@objectstack/connector-mcp': patch
'@objectstack/connector-slack': patch
'@objectstack/service-automation': patch
---

feat(spec)!: retire `connector.connectionTimeoutMs` — declared, bounded, defaulted, served back, and never applied as a deadline

**BREAKING** — `connector.connectionTimeoutMs` is removed. ADR-0049
enforce-or-remove; maintainer ruling 2026-09-22, letter A. It is the narrower
**second** decision this key was owed: the earlier ruling that made its nine
liveness siblings live (`retryConfig.*`, `requestTimeoutMs`) left this one dead
on a stated reason rather than by oversight, and `packages/spec/liveness/connector.json`
has been asking for this decision since.

The key was bounded (`min(1000).max(300000)`), defaulted (`30000`),
`.describe()`d, authorable on both carriers and served back by
`/meta/connector`. Every signal an authoring surface can give said it worked.

### FROM → TO

| removed | what to write instead |
| --- | --- |
| `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. |
| `ConnectorProviderContext.connectionTimeoutMs` (handed to every `ConnectorProviderFactory`) | `ctx.requestTimeoutMs`, or the factory's own `providerConfig` where the provider owns the vocabulary. |
| 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. |

**The one-line fix: delete the key** — and, for a custom provider factory, stop
reading `ctx.connectionTimeoutMs`. `os migrate meta --from 17` lists the
mechanical edits for existing sources; apply them by hand.

⚠️ Runtime behaviour is **unchanged for every shipped provider**, because none
ever applied the value: a connector that authored `connectionTimeoutMs: 1000`
made exactly the same calls, with exactly the same deadlines, as one that did
not. What does change is observable and intended: the def served by
`GET /connectors` no longer echoes a connect deadline nobody keeps.

### ⭐ This is NOT the zero-mention retirement shape

Measured with `git grep -n connectionTimeoutMs SHA -- . ':!packages/spec'` at
`origin/main`: **thirteen** non-test source occurrences over seven files in five
packages — **six reads** (`openapi-connector.ts:242`, `openapi-provider.ts:193`,
`rest-connector.ts:134`, `rest-provider.ts:64`, `plugin.ts:307`,
`plugin.ts:1589`), **four type declarations**, and **three** surviving hardcoded
`30000` writes. Reading the retirement as "nothing referenced it" loses the
finding. Measured across all six reads, every one is a **pass-through**: the
value's only termini were the def `GET /connectors` echoes and the fingerprint
that decides whether to re-materialize. `connectorFetchOptions()` — the one
mapping from authored policy onto the platform's outbound `fetch` — was handed
`{ retryConfig, requestTimeoutMs }` only. Carrying a number is not honouring it,
and ADR-0049 forbids the parsed-unmarked-unenforced state whether the inert
value travels or sits still.

Nor was the `实现` arm available. A connector's outbound call is a WHATWG
`fetch`, whose only cancellation surface is ONE `AbortSignal` covering the whole
operation; nothing in that interface observes the connection phase. Bounding
"time until the response arrives" with this key would kill a slow-but-connected
upstream the author meant to allow with a large `requestTimeoutMs` — breaking
the very promise the key makes. (undici's `connectTimeout` needs a custom
dispatcher: Node-only, and a new subsystem underneath every connector, which the
ruling that made the siblings live forbids.)

### The retirement kit

- The **authorable key** is a `retiredKey()` tombstone on `ConnectorSchema`,
registered as `integration/Connector:connectionTimeoutMs` and
`integration/DeclarativeConnectorEntry:connectionTimeoutMs` in
`RETIRED_KEYS_BY_MAJOR[18]`. The schema is not `.strict()`, so a bare deletion
would strip an authored key in silence (ADR-0104): the tombstone is audible in
both channels — `tsc` (input type `never`) and the parse, which raises the
prescription itself. `DeclarativeConnectorEntrySchema` carries it too — both
published carriers wrap the same private `ConnectorBaseSchema` — so
`stack.connectors[]` and the `/meta/connector` door refuse it too.
- **A D2 conversion, `connector-connection-timeout-ms-removed`** — one strip per
`connectors[]` entry, a pure lossless delete. ⭐ The ruling left whether one was
owed to be **measured** ("a D2 conversion only if a stored connector row can
carry the key"). It can, and both legs were measured before the tombstone
landed: `getMetadataTypeSchema('connector')` — what `PUT /meta/connector/:name`
validates against — parsed a body carrying the key and its output **retained**
the authored value, so the number reached `sys_metadata`; and
`applyConversionsToStoredItem('connector', …)` is live for this type. Rows
written on 17.x therefore replay clean.
- **A D3 semantic entry,
`connector-provider-context-connection-timeout-ms-retired`**, for the withdrawn
`ConnectorProviderContext` member. A provider factory is code: there is no
authored source and no `sys_metadata` row for a conversion to rewrite, so the
removal reaches a factory author as a `tsc` error and as that entry.
- **No def leaves.** The key was a bare `z.number()`, never a `ConfigSchema`
shape, so `RETIRED_DEFS_BY_MAJOR[18]` gains nothing — and `api-surface/` and
`json-schema.manifest/` are byte-identical, which is the correct reading for a
key-only tombstone rather than a missed regeneration.
- `authorable-surface/integration.json` gains two `[RETIRED]` rows;
`authorable-defaults/integration.json` loses the two `= 30000` rows.
- The liveness row **stays** `dead` with a `REMOVED` note, because `retiredKey()`
keeps the key in the walked shape. Its previous note claimed "every occurrence
outside `packages/spec` is a WRITE". That reading was **correct at the SHA the
card cited and dated** (`0870fb5418` — exactly five non-spec source hits, all
five `connectionTimeoutMs: 30000,`) and was superseded by `b929e0a662`, the PR
the card itself flagged as pending. It is **stale, not false**, and the row now
carries both readings with their trees rather than one undated claim.
- **An `acceptRetiredDefaultResidue` stage** (#12840), `{ connectionTimeoutMs: 30000 }`
on both carriers. The key was `.optional().default(30000)`, so a 17.x parse
materialized it into **every** connector — measured across two builds: the base
build emits it for an entry that authored only `name`/`label`/`type`, and the
tombstoned build refuses that exact object at `connectors.0.connectionTimeoutMs`.
The D2 does **not** discharge the obligation, and the precedent shows it:
`ObjectPermission:allowPurge` carries a D2 **and** the residue stage, for its
own reason (a released toolchain materialized its default into every built
artifact's entries). The reason *here* is a different one — this schema has a
second door: `AutomationEngine.registerConnector` parses `ConnectorSchema` for
a def a plugin or provider factory builds **in code**, where no conversion
ever runs, and all four shipped connector packages put the materialized value
straight into that def literal. So the emitted `30000`
is accepted-and-stripped while `15000` keeps the tombstone's refusal, and
nothing is un-retired: `z.input` stays `never` and the `[RETIRED]` row stays.
- **No deprecation window** (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」),
and no staged retirement.

⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
is published, so this is breaking for consumers no download, dependent or source
telemetry was consulted for. The pinned sibling checkout **was** measured: zero
occurrences of the name at objectui `87af769e`, against a lit control on the same
command and scope, so no sibling fix or pin bump rides with this.

`Clause-②: yes (narrowing)` — a published authorable key is removed on two
carriers and a published interface member leaves `ConnectorProviderContext`, so
the accept set a consumer writes against narrows. Nothing is widened and nothing
is renamed. Contract-review tier.

<!-- adr-0087: registered connector-connection-timeout-ms-removed, connector-provider-context-connection-timeout-ms-retired -->
27 changes: 15 additions & 12 deletions content/docs/references/integration/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,20 @@ construction. ⚠️ Retrying a `429` is not throttling it: a retry policy
spaces out the calls you already made, it does not cap the rate, so the
sentence above about rate limiting stands unchanged.

⛔ **Two exceptions, both still inert and both still `dead` in
`packages/spec/liveness/connector.json`.** `health.circuitBreaker`: every
sub-key is unread and no breaker ever opens — implement circuit breaking in
the connector provider. `connectionTimeoutMs`: it is carried to a provider
factory but the platform does not enforce it, because a WHATWG `fetch`
⛔ **One exception remains, still inert and still `dead` in
`packages/spec/liveness/connector.json`:** `health.circuitBreaker` — every
sub-key is unread and no breaker ever opens; implement circuit breaking in
the connector provider.

`connectionTimeoutMs` used to be the second exception and is now **removed**
(ADR-0049, the narrower second decision that surface was owed): it was
carried to a provider factory but never applied as a deadline anywhere, and
it is not implementable where it was declared, because a WHATWG `fetch`
exposes one `AbortSignal` over the whole operation and never the connection
phase alone; `requestTimeoutMs` is the bound the platform can keep, and
ADR-0049 owes this one key a narrower decision. The full removal reasoning
for the rate-limit shape is recorded at the removal site: the "REMOVED:
outbound rate limiting" block in `integration/connector.zod.ts`, and
`packages/spec/docs/SYNC_ARCHITECTURE.md`.
phase alone. `requestTimeoutMs` is the bound the platform can keep. The full
removal reasoning is recorded at each removal site: the "REMOVED:
`connectionTimeoutMs`" and "REMOVED: outbound rate limiting" blocks in
`integration/connector.zod.ts`, and `packages/spec/docs/SYNC_ARCHITECTURE.md`.

**Field mapping does not transform values.** This header used to offer "field
mapping and transformations"; only the first half was ever true.
Expand Down Expand Up @@ -200,7 +203,7 @@ Circuit breaker configuration
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations |
| **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. |
| **retryConfig** | `{ strategy: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts: number; initialDelayMs: number; maxDelayMs: number; … }` | optional | Retry configuration |
| **connectionTimeoutMs** | `number` | optional (default: `30000`) | Connection timeout in ms |
| **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. |
| **requestTimeoutMs** | `number` | optional (default: `30000`) | Request timeout in ms |
| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional (default: `"inactive"`) | Connector status |
| **enabled** | `boolean` | optional (default: `true`) | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor. |
Expand Down Expand Up @@ -684,7 +687,7 @@ Connector type
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations |
| **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. |
| **retryConfig** | `{ strategy: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts: number; initialDelayMs: number; maxDelayMs: number; … }` | optional | Retry configuration |
| **connectionTimeoutMs** | `number` | optional (default: `30000`) | Connection timeout in ms |
| **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. |
| **requestTimeoutMs** | `number` | optional (default: `30000`) | Request timeout in ms |
| **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional (default: `"inactive"`) | Connector status |
| **enabled** | `boolean` | optional (default: `true`) | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor. |
Expand Down
4 changes: 3 additions & 1 deletion packages/connectors/connector-mcp/src/mcp-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,9 @@ export async function createMcpConnector(opts: McpConnectorOptions): Promise<Mcp
// the (post-parse) Connector output type.
status: 'active',
enabled: true,
connectionTimeoutMs: 30000,
// `connectionTimeoutMs` — REMOVED with the spec key (ADR-0049): it was
// written here only so the literal satisfied the post-parse type, and
// the platform never applied it as a connect deadline.
requestTimeoutMs: 30000,
actions: selected.map((tool) => ({
key: tool.name,
Expand Down
13 changes: 6 additions & 7 deletions packages/connectors/connector-openapi/src/openapi-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,12 @@ export interface OpenApiConnectorConfig {
* (ADR-0049 · #18975). Omitted ⇒ the wrapper's own defaults.
*/
retryConfig?: RetryConfig;
/**
* Declared connect deadline (ms). Carried onto the def so `GET /connectors`
* reports what the author declared; ⚠️ not enforced — one `fetch` signal
* cannot bound the connection phase alone (`connector-fetch-policy.ts`).
*/
connectionTimeoutMs?: number;
// `connectionTimeoutMs` — REMOVED with the spec key (ADR-0049). It was
// accepted here only to be carried onto the def `GET /connectors` echoes:
// one `fetch` signal cannot bound the connection phase alone, so it never
// reached `connectorFetchOptions` and never bounded a call. Echoing a
// deadline nobody keeps is what the retirement withdraws (mirrors
// connector-rest).
/** Per-request deadline (ms) — `resilientFetch`'s per-attempt timeout. */
requestTimeoutMs?: number;
/** Injected fetch implementation (defaults to global `fetch`). */
Expand Down Expand Up @@ -239,7 +239,6 @@ export function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiC
// the (post-parse) Connector output type (mirrors connector-rest/mcp).
status: 'active',
enabled: true,
connectionTimeoutMs: config.connectionTimeoutMs ?? 30000,
requestTimeoutMs: config.requestTimeoutMs ?? 30000,
...(config.retryConfig === undefined ? {} : { retryConfig: config.retryConfig }),
actions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ export function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): Co
// ADR-0049 · #18975 — the authored resilience policy, already resolved by
// the materializer, reaches the transport this bundle closes over.
retryConfig: ctx.retryConfig,
connectionTimeoutMs: ctx.connectionTimeoutMs,
requestTimeoutMs: ctx.requestTimeoutMs,
fetchImpl: deps.fetchImpl,
});
Expand Down
Loading
Loading