Repository navigation
fix(service-automation): a connection marked reconnect required says the cause that actually happened (#28863) - #28955
Conversation
…the cause that actually happened reconnect_required is set four ways: the authorization server refuses the grant on refresh, it issued no refresh token, the connection keeps no client to refresh with, or the upstream refuses the token at MCP tool discovery. The stored reason on the no-refresh-token and no-client paths opened with a refusal no server made, and said the access token had expired, which a refresh on a cache miss or a 401 does not imply. reconnectRequiredReason takes the cause (refused by default, its sentence unchanged), and each refresh path passes its own. The connection's status field help, the markReconnectRequired docblock and connectors.mdx name all four causes, in the spec contract's words. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift Check10 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4030a6c8c4ea36c0793dfc843bee0bd59bb7615a && git checkout 4030a6c8c4ea36c0793dfc843bee0bd59bb7615a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ff88105061012c8b14f7ec937d7b3113e404e922 7cf0294a870b814629328c120c3f3b2a37fb5a40 && git checkout -B drift-repro ff88105061012c8b14f7ec937d7b3113e404e922 && git merge --no-ff 7cf0294a870b814629328c120c3f3b2a37fb5a40
node scripts/docs-audit/affected-docs.mjs --json ff88105061012c8b14f7ec937d7b3113e404e922 |
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC 席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #28863. 6 files (+116 / −20): reconnectRequiredReason and its three call sites in refreshOnce (oauth2-grant/grant-handle.ts), the markReconnectRequired docblock, the sys_connector_connection.status description (it lives in service-automation, not platform-objects as the order said), connectors.mdx (the L689 paragraph and the L868 row), 3 tests and a service-automation patch changeset. Metadata-protocol: none. No packages/spec/**, no gate, no error code, no status value. |
| Rule | reconnectRequiredReason takes an optional cause ('refused' by default, with its sentence unchanged; 'no_refresh_token'; 'no_client'). Each refresh path passes its own, so the stored status_reason and the 503 name what happened: the two paths that never ask the server claim no refusal, and none says "expired". The new ReconnectRequiredCause type and the parameter are not exported from the package root (the seat read src/index.ts), so nothing is widened. |
| Evidence | Base: 2 of the 3 new cases red (the two non-refusal paths); head 22/22; the refusal control green both ways. service-automation 3284 and platform-objects 1503 tests, typecheck in full, scoped lint, and 89/89 derived gates (docs included), 0 NOT-MEASURED. No consumer in objectstack, objectui or hotcrm reads the reason text. CI on head is running; auto-merge waits for it. |
| Carried | The spec JSDoc half of item 5 (automation-service.ts L1145–1146, "expired") → #28961 (domain:spec, p3). The dev's two same-class leftovers (the refusal of an already-marked row uses the default refused sentence, and the onGrantRefused log) → #28963 (domain:server, p3). Both filed by this seat. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, with every check green or an expected skip. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #28863
Clause-②: no
Metadata-protocol: none
Skills-docs: none — no file under
skills/**teaches the connection'sreconnect_requiredcauses (0 hits forreconnect_required,reconnect requiredandrefused the grant).维护者速读
status_reason、sys_connector_connection.status字段说明、markReconnectRequired注释和connectors.mdx以前都只说第一种。现在三处文字列出全部四种;「没有 refresh token」和「没有 client」两条路径存下的原因各自说真正发生的事,不再先声称服务器拒绝,也不再说访问令牌「已过期」(缓存未命中或调度 401 也会触发刷新,令牌可能仍有效)。packages/services/service-automation/src/oauth2-grant/grant-handle.ts的reconnectRequiredReason与refreshOnce;同目录grant-handle.test.ts末尾的[#28863]三条。pnpm --filter @objectstack/service-automation exec vitest run src/oauth2-grant/grant-handle.test.ts,预期Tests 22 passed (22)(main 上新加的 2 条红,对照两边都绿)。What changes
reconnectRequiredReason(connector, organizationId, cause?)(oauth2-grant/grant-handle.ts) takes an optionalReconnectRequiredCause:'refused'(the default, so every existing caller keeps the old sentence byte for byte),'no_refresh_token'or'no_client'. The sentence's cause clause is the one that happened:refreshOncepasses each path's cause tograntRefused, so the storedstatus_reasonand the dispatch's 503 message are that one sentence. Before, the two non-refusal paths stored the refusal sentence with the real cause appended, and the no-refresh-token one said "an expired access token".statusfield description insys-connector-connection.object.ts(it lives inservice-automation, notplatform-objectsas the order's path guessed), theConnectorConnectionStore#markReconnectRequireddocblock, andcontent/docs/automation/connectors.mdx(the paragraph at L689 and the boot-behaviour table row at L868). Each points atstatus_reason/statusReasonfor which cause happened..changeset/28863-reconnect-required-cause.md:patchon@objectstack/service-automation, both declaration lines. The docs page ships in no package.packages/spec/**.Why
Clause-②: no. No accept set, status value, code or export changes meaning. What changes is prose and the reason text two refresh paths store and answer with (the 503 stays a 503SERVICE_UNAVAILABLE).Tests
Three new cases at the end of
packages/services/service-automation/src/oauth2-grant/grant-handle.test.ts([#28863]), on the file's in-memory connection store, each reading the row's storedstatusReasonand the dispatch refusal:refusednorexpired, equals the 503 message, and no token request is made.refusednorexpired, equals the 503 message, and no token request is made.invalid_grant): the reason still names the refusal, carries no token, and equals the 503 message.Red on base, green on head
grant-handle.tsfrom5e1b9b0a, the new tests at head):Tests 2 failed | 20 passed (22). The two reds:expected 'connector instance 'crm' needs to b…' to contain 'issued no refresh token, so an access…'and the same for'keeps no client to refresh with, so a…'. The refusal control is green both ways. Restored withgit checkout HEAD -- grant-handle.ts,git diff HEADempty.Tests 22 passed (22).Acceptance notes
packages/spec/src/contracts/automation-service.tsL1145–1146, theConnectorConnectionState.statusJSDoc from PR docs(spec): reconnect_required docblock names every way the grant stops working #28862: "the connection keeps nothing to renew an / expired access token with". "expired" there is narrower than the code, for the same reason as above. The runtime wording here, "an access token it has to renew", is the proposed replacement, so the two can agree.grant-handle.test.tsL322 matchesissued no refresh token, which the new sentence keeps, so it stays green.connector-connections.test.tsL148–157 passes its own reason string to the store.connector-connection-door.test.tsL576 matches the plugin'sonGrantRefusedlog line ("…refused the grant of organization…"), which this PR does not touch. No other consumer reads the reason text (searched objectstack, objectui and hotcrm forno usable refresh,refused the grant,expired access tokenandissued no refresh token).onGrantRefusedwarn line (plugin.ts≈L3697) says "the authorization server refused the grant" on all three refresh paths;selectingrant-handle.ts≈L238,refreshOnce's first check, and the plugin's hold-reason fallback ≈L3189) uses the default refused sentence whatever cause the row stores. Answering with the row's ownstatusReasonwould make those honest too.Verification
All readings at head
7cf0294a(the change790e473c, merged once withorigin/main, a clean merge). Fullpnpm turbo run build --concurrency=2: 118/118.Package suites, typecheck, lint
pnpm --filter @objectstack/service-automation test:Test Files 255 passed (255),Tests 3284 passed (3284), lock VERDICT command-exit 0. Itstypecheck(tsc + test-typecheck): exit 0.pnpm --filter @objectstack/platform-objects test:Test Files 108 passed (108),Tests 1503 passed (1503), command-exit 0. Itstypecheck: exit 0. (No file of it changed; run as the order asks.)pnpm lintcommand scoped topackages/services/service-automation: exit 0.Derived gates (`dispatch-gates.mjs --commands`, 89 families)
7cf0294a, the docs families included (check-affected-docs,check-drift-comment,check:docs-*,check:corpus-claim-drift);dispatch-gates --ran:89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN. Record:pm-orders/server/28863-gates-ran.txt.check:type-check-debt: OK,--re-measure: OK.pnpm lintand the CI-only families are CI's; see the PR's check-runs.Dogfood battery: not owed (no
!, no narrowing arm) and not run.Generated by Claude Code