Skip to content

feat(service-push)!: push sends through the hosted push relay, authenticated by the server's cloud binding - #30749

Merged
os-zhuang merged 10 commits into
mainfrom
claude/project-thread-yvu7tv
Nov 6, 2026
Merged

os-zhuang merged 10 commits into
mainfrom
claude/project-thread-yvu7tv

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #29920

Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — the narrowing is on the server-to-relay wire and the relay's error answer, which no metadata author or skill writes; the operator page content/docs/kernel/runtime-services/push-service.mdx is updated here

domain:server#5 card thread · session session_016WV4cznEfnAJ8CyWBdiL2G · branch claude/project-thread-yvu7tv (the card thread's platform-assigned branch, named in claim amendment 6671581752). Resumes os-bill's released WIP (Release 6670797151, claude/project-thread-tk5wos@634f4145); that work is carried here commit for commit, with one origin/main merge and two gate fixes on top.

维护者速读

  • 改了什么:移动推送默认走云端托管中继——服务器用自己的云绑定凭据把每条推送以「只带 ID」的形式发到 cloud.objectos.ai/api/v1/cloud/push/send(或 OS_CLOUD_URL、或配置里的 endpoint),标题和正文不出服务器;原来这种配置在启动时直接被拒。
  • 为什么改:push: {} 本来就是规格里的默认(中继),但发送端一直没做,推送等于不能用;按裁决 Q1 A / Q2 A / Q3 A / Q4 C 实现。
  • 风险与回滚:BREAKING——中继报文的 data 改为严格(多余键被拒而不是被丢),限流错误必须 retryable: true,导出 PUSH_RELAY_NOT_SERVED_MESSAGE 改名为 PUSH_RELAY_AIR_GAPPED_MESSAGE;未绑定的服务器照常启动、推送暂停(走站内信),声明无控制面(OS_CLOUD_URL=off)的部署配中继会在启动时拒绝;direct 直连通道不变。回滚即还原本 PR。
  • 先看:packages/services/service-push/src/transports/relay.ts(发送、401/429/invalid 的映射);packages/cloud-connection/src/cloud-binding.ts + packages/spec/src/contracts/cloud-binding-service.ts(新的 cloud-binding 服务与接口);packages/spec/src/api/push-relay.zod.ts(严格 data、retryable)。
  • 两分钟验证:pnpm --filter @objectstack/service-push exec vitest run src/transports/relay.test.ts src/push-plugin.test.ts,预期 2 个文件 32 例全绿,其中未绑定时 vendors() 为空、401 不重试、OS_CLOUD_URL=off 时启动被拒。
  • 需要你做的:本 PR 为 BREAKING,入队前需要你对本 PR 的明确同意(席位先做契约档复核)。

What changes

  • @objectstack/service-push sends through the hosted relay (transports/relay.ts, RelayPushSender + relayTransport). One PushRelayRequestSchema POST per push, one delivery per device token, parsed before it leaves the server. The endpoint is the relay block's endpoint, else the control-plane base URL plus PUSH_RELAY_PATH (/api/v1/cloud/push/send, the constant lives here and not in packages/spec, ruling Q3 A, comment 6670221824). The bearer is read from the cloud-binding service on every send and goes into the Authorization header only.
  • Answers: ok is sent; invalid answers invalid_token, so the channel retires the device row as for the direct transports; retry is a retryable failure; 401 fails non-retryable, naming the lapsed binding; 429 follows the relay's retryable (RATE_LIMIT_EXCEEDED retryable, QUOTA_EXCEEDED not); 5xx retryable; any other status not.
  • Init (ruling Q2 A): PushServicePlugin.init and resolvePushCapabilityArg refuse a relay configuration only when the deployment declared it has no control plane (isControlPlaneDeclined()), with PUSH_RELAY_AIR_GAPPED_MESSAGE naming the in-app inbox and the direct transport. Anywhere else the relay boots, bound or not. While unbound the relay transport is unavailable (IPushTransport.available()), vendors() lists nothing, the push channel reports transport_not_configured through its existing isAvailable reading, and the plugin warns once at kernel:ready. A bind done while the server runs takes effect without a restart.
  • @objectstack/cloud-connection registers cloud-binding in init() (ruling Q1 A): ICloudBindingService (credential(), bound(), controlPlaneUrl()), new in packages/spec/src/contracts/cloud-binding-service.ts, additive. Its own kernel:ready control-plane routes now read the credential through the same instance, so the credential rule keeps one owner.
  • @objectstack/core gains CLOUD_DECLINED_SPELLINGS, isControlPlaneDeclinedSpelling and isControlPlaneDeclined beside resolveControlPlaneApiKey; cloud-connection reads them from there and re-exports isControlPlaneDeclined unchanged. Moved so service-push can read the air-gap declaration without depending on cloud-connection (which would close a cycle).
  • The notice line (ruling Q4 C): the push channel (service-messaging/src/push-channel.ts) picks the generic line by resolveRecipientLocale (sys_user.locale, then the deployment default, then English) and passes it as PushMessage.notice; messaging-service-plugin.ts hands the push channel getData / getDefaultTemplateLocale as it does for sms and email (one line).
  • Spec (push-relay.zod.ts, push-config.zod.ts, stack.zod.ts): the not-served markers flip to served; PushRelayPayloadSchema.data is judged strictly (a content key is refused with the ID-only guidance instead of stripped); RATE_LIMIT_EXCEEDED carries retryable: true (default true); the docblock sentence carried in by comment 6606506582 and the text describe ("in the recipient's language") are corrected. Regenerated: reference docs, api-surface, export-origins, the strictness ledger counts.
  • Changeset .changeset/29920-push-relay-cloud-binding.md: five packages at patch (Metadata-protocol: none), feat(service-push)!, BREAKING section with the FROM/TO for the retired export, ADR-0087 not-required (runtime-export-retired …).

Why

PushServiceConfigSchema reads {} as the hosted relay, and until now the boot refused it, so a stack that required push with the default configuration could not push at all. The card's ask, as amended by the maintainer's rulings on it (Q1 A, Q2 A, Q3 A, Q4 C; summary 6670237128, correction 6670407970).

Tests

  • service-push/src/transports/relay.test.ts (new, 13): POST shape and bearer, explicit endpoint wins, English default line, credential read per send, invalid / retry / 401 / 429 / 5xx mapping, unbound and no-address failures, content key refused before sending, vendors listed only while bound.
  • service-push/src/push-plugin.test.ts: relay boots for no config, {}, transport: 'relay' and a staging endpoint; bound sends one POST and no log line carries the credential; unbound warns once at kernel:ready and the bind takes effect without a restart; OS_CLOUD_URL=off refuses each relay form and still boots a direct block. capability-arg.test.ts follows the same split.
  • cloud-connection/src/cloud-binding.test.ts (new): the credential rule (service key over the bound oscc_ bearer, '' unbound), per-call reads, controlPlaneUrl() and the declined spellings, the service registered in init(). core/src/cloud-managed.test.ts: the declined-spelling reader.
  • service-messaging/src/push-channel.test.ts: zh-CN line for a zh-CN recipient; deployment default then English; a failed locale read never costs the push.
  • spec/src/api/push-relay.test.ts, system/push-config.test.ts, stack-email-sms.test.ts: served markers, strict data, retryable pinned on the rate-limit answer.
  • cli/src/commands/serve-stack-email-sms-door.contract.test.ts and verify/src/harness.served-composition.test.ts: the served composition boots the relay instead of refusing it.

Acceptance notes

  • Contract review owed on the schema face before ACCEPT (BREAKING): packages/spec/src/contracts/cloud-binding-service.ts, packages/spec/src/api/push-relay.zod.ts, packages/spec/src/system/push-config.zod.ts, packages/spec/src/stack.zod.ts, packages/core/src/stack-auth.ts, the service-push index exports, packages/cloud-connection/src/cloud-binding.ts and the changeset. Queueing needs the maintainer's per-PR word.
  • Changeset rewritten in this PR (gate fix, commit 2e35f60). As released, the marker was red on check:adr-0087-registration predicate 3: runtime-export-retired reads every FROM line as a retired export, so the package name in parentheses and the two wire lines (data, RATE_LIMIT_EXCEEDED / retryable) were read as unnamed operands. The FROM/TO now names only PUSH_RELAY_NOT_SERVED_MESSAGE; the two wire narrowings are stated as plain instructions. No category covers a *.zod.ts wire narrowing other than registered, and registering one would mean a packages/spec/src/migrations/ entry outside this card's surface; the marker's reason says why the ledger has nothing to rewrite (no author writes the wire, nothing stores it). The reviewer should confirm that reading.
  • Test alias added in this PR (gate fix, commit 1b82707). check:test-source-alias was red: the new relay tests import @objectstack/spec/api and the plugin reads @objectstack/core, both resolving to dist/. Two anchored aliases in service-push/vitest.config.ts.
  • CLOUD_BINDING_SERVICE ('cloud-binding') is spelled once in cloud-connection and once in service-push, because service-push does not depend on cloud-connection; the interface lives in spec contracts. Noted, not filed.
  • When a 200 answer has no result for the delivery's token, the sender reads the first result (find(…) ?? results[0]); with one delivery per request the two coincide. Noted, not filed.
  • Docs: content/docs/kernel/runtime-services/push-service.mdx (hand-written operator page) is updated for the served relay; it sits outside the claim's listed file surface, carried from the released branch. ADR-0012 §6.1's "device locale" wording stays for ADR-0012 §6.1: the hosted push relay picks the generic notice line in the recipient's language, not "the device locale" (follow-up to #29920 Q4 C) #30670.
  • A deployment with OS_CLOUD_URL unset but the cloud-connection plugin's own controlPlaneUrl option set to off boots the relay (the init check reads the environment) and every send then fails relay_address_not_published. Noted, not filed.

Verification

Readings against objectstack-ai/objectstack. Code head 2d054bbe (the origin/main merge at 4bf9a2d7) for the package suites, ablations and dogfood; the two later commits change only the changeset text (2e35f600) and the service-push vitest alias (1b82707d), and the service-push suite and the derived gates were re-run on 1b82707d.

Package suites and typecheck (os-verify-lock, after the dependency-closure build)
  • pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/commands/serve-stack-email-sms-door.contract.test.ts: 1 file, 10 passed.
  • pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2 src/harness.served-composition.test.ts: 1 file, 10 passed.
  • pnpm --filter @objectstack/spec test (full suite): 817 files, 25796 passed, 1 todo.
  • pnpm --filter @objectstack/service-push test: 5 files, 74 passed (re-run on 1b82707d with the source aliases: 74 passed).
  • pnpm --filter @objectstack/service-messaging test: 66 files, 849 passed.
  • pnpm --filter @objectstack/cloud-connection test: 71 files, 896 passed.
  • pnpm --filter @objectstack/core test: 136 files, 3087 passed.
  • typecheck exit 0 for spec, core, cloud-connection, service-push (also on 1b82707d), service-messaging, cli, verify.
  • pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date.
  • node scripts/check-platform-checklist.mjs --changed-from origin/main: OK, 11 anchors judged, 0 lost.
  • pnpm lint (eslint . --no-inline-config): exit 0 at 2e35f600; the one file 1b82707d adds lines to, service-push/vitest.config.ts, re-linted: exit 0.
  • Each lock run's VERDICT line read command-exit 0.
Ablations (scripts/ablation-replace.mjs; each restored to blob == HEAD, git diff HEAD empty)

Seven legs at 2d054bbe; each red where expected, then restored (blob == HEAD).

  • A1 401 answered retryable: true: 1 failed / 12 passed (relay.test, the 401 case).
  • A2 invalid no longer maps to invalid_token: 1 failed / 12 passed (the dead-token case).
  • A3 unbound guard removed (if (!credential) made false): 2 failed / 11 passed (unbound send; bind-at-runtime).
  • A4 init air-gap refusal removed in PushServicePlugin.init: 4 failed / 15 passed (each relay form under OS_CLOUD_URL=off).
  • A5 vendors() ignores available(): 6 failed / 26 passed (push-plugin + relay.test, every unbound case).
  • A6 PushRelayPayloadSchema.data back to the stripping PushDeliveryDataSchema: 1 failed / 39 passed (spec push-relay.test, the strict-data case).
  • A7 the notice line ignores the recipient locale (Q4 C): 2 failed / 17 passed (push-channel.test, zh-CN line; default chain).
  • The service-push tests import the mutated sources relatively, and the spec test reads spec source, so no leg needed a rebuild. Tree clean after the last leg (git status --porcelain empty).
Full dogfood (BREAKING)
  • pnpm --filter @objectstack/dogfood test under os-verify-lock at 2d054bbe (no OS_TEST_SHARD, no path), after the dogfood dependency-closure build: Test Files 421 passed, 3 skipped (424); Tests 3513 passed, 36 skipped (3549); VERDICT command-exit 0. No failures.
  • The two commits after it touch no code a dogfood test loads (changeset text; a vitest alias in service-push's own config).
Derived gates (dispatch-gates --commands)
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 129 families derived; the list is identical at 2d054bbe and 1b82707d.
  • First pass, at 2d054bbe: two real reds, both fixed in this PR — check-adr-0087-registration (the changeset, see Acceptance notes) and check:test-source-alias (service-push aliases). Three exited 3 (prerequisite): check-plugin-teardown-shape --self-test (pinned fixture commit fetched), check:skill-examples and check:dual-build-cjs-loads (whole-tree pnpm build, 118 tasks green).
  • Second pass, at 1b82707d, after the whole-tree build: all 129 exit 0.
  • --ran reconciliation at 1b82707d: 129 derived, 129 run, 0 NOT-MEASURED, 0 UNRUN.
  • check:commit-card-trailers passed on each push.
  • Families CI runs beyond the derived set: read from this PR's check-runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Generated by Claude Code

…pellings move to core

Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN
Co-authored-by: Claude <noreply@anthropic.com>
…ud-binding contract

Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN
Co-authored-by: Claude <noreply@anthropic.com>
…eclined-spelling reader

Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN
Co-authored-by: Claude <noreply@anthropic.com>
…rict data and rate-limit retryable

WIP handover: the seat released the card at 22:01Z. These pins were written,
but the full gate set has not run yet; the handover note on the card lists it.

Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN
Co-authored-by: Claude <noreply@anthropic.com>
… the two wire narrowings as prose

check:adr-0087-registration reads every FROM line of a runtime-export-retired
changeset as a retired export. The package name in parentheses and the two
wire-schema lines were read as operands the marker does not name.

Claude-Session: https://claude.ai/code/session_016WV4cznEfnAJ8CyWBdiL2G
Co-authored-by: Claude <noreply@anthropic.com>
…from source

The relay tests parse with PushRelayRequestSchema and the plugin reads
isControlPlaneDeclined from core; unaliased, both resolve to dist and the
verdict follows build state (check:test-source-alias).

Claude-Session: https://claude.ai/code/session_016WV4cznEfnAJ8CyWBdiL2G
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:system tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cloud-connection, @objectstack/core, @objectstack/service-messaging, @objectstack/service-push, @objectstack/spec, touching 65 documentable anchor(s). ⚠️ 6 changed file(s) yielded no anchor (packages/services/service-push/README.md, packages/services/service-push/package.json, packages/services/service-push/vitest.config.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

43 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11.

⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/services/service-push/README.md, packages/services/service-push/package.json, packages/services/service-push/vitest.config.ts, …) — pages documenting those are invisible to this run
  • 14 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 221 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 182 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d4a6cbf319aa7b2b53d0bfcec08d3851cbf5ef3a — the merge of head d14d749e6b978c11f1d959ba985a21fd864d776e into base 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d4a6cbf319aa7b2b53d0bfcec08d3851cbf5ef3a && git checkout d4a6cbf319aa7b2b53d0bfcec08d3851cbf5ef3a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11 d14d749e6b978c11f1d959ba985a21fd864d776e && git checkout -B drift-repro 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11 && git merge --no-ff d14d749e6b978c11f1d959ba985a21fd864d776e

node scripts/docs-audit/affected-docs.mjs --json 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1b82707de37511f76495017893bc60ecf63733f5
Local-runs: none
Scope: packages/spec/src/api/push-relay.zod.ts, packages/spec/src/contracts/cloud-binding-service.ts, packages/spec/src/contracts/index.ts, packages/spec/src/stack.zod.ts, packages/spec/src/system/push-config.zod.ts + packages/services/service-push/src/transports/relay.ts, packages/services/service-push/src/push-plugin.ts, packages/services/service-push/src/push-service.ts, packages/services/service-push/src/capability-arg.ts, packages/services/service-push/src/index.ts, packages/services/service-push/vitest.config.ts, packages/services/service-push/src/transports/relay.test.ts, packages/services/service-push/src/push-plugin.test.ts, packages/cloud-connection/src/cloud-binding.ts, packages/cloud-connection/src/cloud-binding.test.ts, packages/cloud-connection/src/cloud-connection-plugin.ts, packages/cloud-connection/src/cloud-url.ts, packages/core/src/stack-auth.ts, packages/core/src/cloud-managed.test.ts, packages/services/service-messaging/src/push-channel.ts, packages/services/service-messaging/src/messaging-service-plugin.ts, packages/spec/src/api/push-delivery.zod.ts, packages/spec/src/api/errors.zod.ts, packages/spec/src/shared/strict-object.ts, packages/spec/src/shared/lazy-schema.ts, packages/spec/src/system/core-services.zod.ts, packages/spec/src/api/push-relay.test.ts, packages/spec/src/system/push-config.test.ts, packages/spec/src/stack-email-sms.test.ts, packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json, .changeset/29920-push-relay-cloud-binding.md, content/docs/kernel/runtime-services/push-service.mdx, packages/cli/src/commands/serve.ts, docs/adr/0012-notification-platform.md, docs/adr/0087-metadata-protocol-upgrade-contract.md, scripts/check-adr-0087-registration.mjs, scripts/check-test-source-alias.mjs

Inputs: card #29920 (body and all 14 comments, rulings Q1 A, Q2 A, Q3 A at 6670221824, Q4 C at 6670237128 with correction 6670407970, dev report 6673285338), PR #30749 body and file list (37 files, identical to the local net diff against merge-base 4bf9a2d), the net diff, and the check-runs on this head. Readings are against objectstack-ai/objectstack at 1b82707d unless a base reading is named.

① Derived judgments

Accept-set changes the diff implies, each judged:

  1. PushRelayPayloadSchema.data (api/push-relay.zod.ts): at base a PushDeliveryDataSchema object that STRIPS an undeclared key; at head PushRelayPayloadDataSchema, a strictObject over PushDeliveryDataSchema.shape that REFUSES one, with the ID-only guidance for title, body, subtitle, message, content. Narrowing. Right: carried item 1 (6606506582), and ADR-0012 §6.1 refuses those five by name; a stripped key read as carried was the hole. One source of the member list (the shape is reused, not transcribed), so no drift probe is owed.
  2. RATE_LIMIT_EXCEEDED.retryable: at base inherited z.boolean().default(false) from EnhancedApiErrorSchema; at head z.literal(true).default(true). Input accept set shrinks from true/false/absent to true/absent; the post-parse default flips from false to true. Narrowing. Right: carried item 2; a 429 rate limit is by definition retryable and a relay answering retryable: false on it was a lie the consumer would have obeyed. QUOTA_EXCEEDED keeps the inherited false default. Right.
  3. [PLANNED — not yet served] removed from the seven relay object schemas' describes and from PushRelayTransportConfigSchema's describe, plus the docblocks; PushServiceConfigSchema and the stack push key describes rewritten. Documentation-only on the public surface; no accept-set change. Right: ADR-0049 says a marker states a fact, and the sender ships in this PR; push-relay.test.ts and push-config.test.ts pin the absence.
  4. text describe: "in the device locale" becomes "in the recipient's language". Right: ruling Q4 C, and that describe was named in the ruling as the spec half.
  5. PushServiceConfigSchema, PushRelayTransportConfigSchema, the stack push key: the accept set is unchanged (relay with optional endpoint; direct with vendor blocks; inline secrets refused). Right: the ask was to serve what the schema already accepts, not to reshape it.
  6. ICloudBindingService (new, contracts/cloud-binding-service.ts, exported through contracts/index.ts): credential(), bound(), controlPlaneUrl(). Additive; api-surface/contracts.json and export-origins/contracts.json regenerated with exactly that one row. Right: Q1 A as corrected (the controlPlaneUrl() member is the correction's).
  7. @objectstack/service-push public surface: PUSH_RELAY_NOT_SERVED_MESSAGE leaves; PUSH_RELAY_AIR_GAPPED_MESSAGE, RelayPushSender, relayTransport, PUSH_RELAY_PATH, CLOUD_BINDING_SERVICE, DEFAULT_PUSH_NOTICE, RELAY_NOT_BOUND, RELAY_ADDRESS_NOT_PUBLISHED, RELAY_BINDING_REFUSED, RelayPushSenderOptions arrive; IPushTransport.available?() and PushMessage.notice? are new optional members; IPushService.vendors() now lists only AVAILABLE transports. The retirement is declared with its FROM → TO; the rest is additive. Right.
  8. @objectstack/core: CLOUD_DECLINED_SPELLINGS, isControlPlaneDeclinedSpelling, isControlPlaneDeclined added beside resolveControlPlaneApiKey; @objectstack/cloud-connection re-exports isControlPlaneDeclined unchanged and resolveCloudUrl keeps its answers. Additive; one definition of the four spellings, read from two packages. Right (the cycle it avoids is real: cloud-connection depends on runtime).
  9. CloudConnectionPlugin registers cloud-binding in init() and declares providesServices (ADR-0116); its own kernel:ready routes read this.binding.credential() instead of a local closure. Right: the one-owner half of Q1 A.
  10. Metadata-protocol: none. Right. Nothing a metadata author writes gains, loses or changes meaning; the narrowings are on the server-to-relay request and the relay's error answer.

② Semver level

  • Changeset .changeset/29920-push-relay-cloud-binding.md: five packages (service-push, cloud-connection, service-messaging, spec, core) at patch, title feat(service-push)!, a BREAKING section, both declaration lines line-initial. The PR body carries the same two lines. Matches the diff: every package the diff moves is listed; nothing listed is untouched.
  • Clause-②: yes (narrowing) — right, and BREAKING by the closed pair. The three narrowings it names (strict data, RATE_LIMIT_EXCEEDED retryable, the retired export) are the three the diff makes; the widening it also describes (relay configs now boot) is stated in prose and does not change the arm.
  • patch on a BREAKING changeset: right by the Post-Task rule — the level follows the kind, Metadata-protocol: none takes patch and keeps the BREAKING banner and the ADR-0087 disposition.
  • ADR-0087 marker: not-required (runtime-export-retired @objectstack/service-push#PUSH_RELAY_NOT_SERVED_MESSAGE) with a reason. Judged right (the reasoning is ③ item 1). Predicates as coded in check-adr-0087-registration.mjs: 1 retired-from-entry holds (exported from service-push/src/index.ts at base, absent at head); 2 no-metadata-surface holds (declared in no *.zod.ts, not under spec/src/contracts/, in no object definition at base); 3 from-operands-named holds (the one FROM line names exactly that operand; the two wire narrowings are instructions the detector does not read).
  • The migration the BREAKING section owes is present: FROM PUSH_RELAY_NOT_SERVED_MESSAGE TO PUSH_RELAY_AIR_GAPPED_MESSAGE with the one-line fix, and one instruction each for the two wire narrowings.
  • Changeset names the newly served relay transport, as the card's acceptance asks.

③ Boundary flags

Open question 1 — the ADR-0087 marker with the wire narrowings in prose (commit 2e35f60). Accept A. Reasons: (a) the ledger is the data source of objectstack migrate meta, spec-changes.json and the upgrade guide, all of which describe STORED metadata; the relay wire is built per send by RelayPushSender and read once by the cloud relay, authored by nobody and persisted nowhere, so a registered entry would be a conversion with nothing to convert, projected into two machine-readable artifacts as a migration that does not exist (Route and surface ownership rule 4); (b) the changeset stock's registered narrowings of a spec *.zod.ts are all authorable metadata keys (validation, picklist binding, permission, field sortable, flow condition) — a different class; (c) the gate's purpose is that the question is answered in writing, and the marker's reason does that for the wire; (d) the category's three predicates hold as coded (② above). Alternative B is refused: it would also widen the surface into packages/spec/src/migrations/ for an entry the ledger should not carry. No surface amendment needed.

Open question 2 — two anchored source aliases in service-push/vitest.config.ts. Accept. KNOWN_UNALIASED_TEST_IMPORTS carries no service-push row and is shrink-only, so the new VALUE imports of @objectstack/spec/api (relay.ts, relay.test.ts) and @objectstack/core (push-plugin.ts, capability-arg.ts) had to be aliased, not registered; the anchored regex array form is the shape the gate's own header prescribes against the ENOTDIR trap; the crossing into core/src feeds rule 5 only. The mechanical verdict is the Lint · repo & tooling gates check on this head, in progress at review time (the dev reports it green locally at 1b82707d).

Open question 3 — content/docs/kernel/runtime-services/push-service.mdx edited outside the claim's listed surface. Accept. The kernel/ tree is hand-written (Documentation Guardrails: every tree except references/ and releases/), no page is added so no meta.json is owed, and the page documented an init refusal this PR removes: leaving it would advertise a refusal the runtime no longer makes (Prime Directive 10's corollary, inverted). Content read against the diff: the authentication, address, ID-only, unbound and air-gap paragraphs and the availability table row match relay.ts, push-plugin.ts and push-channel.ts. The seat records the surface widening on the card in one line; no further action.

Other flags from the dev report, answered:

  • CLOUD_BINDING_SERVICE spelled in cloud-connection and service-push: a should-fix on the contract file (④ item 2), not noted-and-closed.
  • results.find(…) ?? results[0]: not a finding — PushRelayResponseSchema promises one result per delivery in request order and the request carries one delivery, so index 0 IS the delivery's answer.
  • OS_CLOUD_URL unset but the plugin option controlPlaneUrl: 'off': accepted as noted. The ruling's text is isControlPlaneDeclined(), which reads the environment; under objectstack serve the option is resolveCloudUrl()'s own answer so the two cannot disagree; a hand-composed host hits a loud, per-send, non-retryable relay_address_not_published. The related seam the dev did not note is ④ item 2's second finding.
  • ADR-0012 at head still says the relay is "not yet served" (status line, §4 table row, the §6 amendment note, §6.1's "what main serves") and names PUSH_RELAY_NOT_SERVED_MESSAGE by path. docs/adr/** is excluded by the claim and is Tier H. These are dated readings ("at this amendment", "at 6a4737a"), not decisions this PR reverses, so Prime Directive 13 is not breached; the staleness goes to ADR-0012 §6.1: the hosted push relay picks the generic notice line in the recipient's language, not "the device locale" (follow-up to #29920 Q4 C) #30670 (ADR-0012 §6.1 docs card) or a sibling docs-only PR. Should-fix, routed, not blocking.
  • Startup-registry-verdict rule: the kernel:ready warn in PushServicePlugin.start() reads the credential store (not a filling registry), after every init(), says the state can change, and records nothing; the per-send ctx.getService('cloud-binding') read is a read-only probe. Clear.

④ Design judgment

packages/spec/src/api/push-relay.zod.ts — holds. Against ADR-0012 §6.1: the five content keys are refused by name at both the payload and the data layer, text is the only visible line, and the docblock now names the producer (RelayPushSender), the parse before send and the cloud control plane as the receiver (cloud#3519). Against the first consumer relay.ts: the request is parsed with PushRelayRequestSchema before fetch, so a content key in PushMessage.data ends as relay_payload_refused with the issue path and nothing leaves the server (pinned); the 200 answer is read with PushRelayResponseSchema; 401 and 429 with PushRelayErrorResponseSchema, and the 429 arm reads error.retryable from the parsed union — true for RATE_LIMIT_EXCEEDED whether the relay sent it or not, false for QUOTA_EXCEEDED (pinned both ways); a 429 whose body fails the parse falls to retryable, the safe direction. Break attempts: .default(true) on a literal inside a discriminatedUnion('code', …) is legal (the discriminator is code); PushDeliveryDataSchema.shape read inside a lazySchema factory resolves the proxy on first use; the non-exported PushRelayPayloadDataSchema leaves api-surface/api.json unchanged and the docs generator inlines it (push-relay.mdx regenerated). No finding.

packages/spec/src/contracts/cloud-binding-service.ts — holds against Q1 A as corrected: three members, read on every call, '' when unbound, the credential rule stated once and matching createCloudBinding (resolveControlPlaneApiKey(option) || store.read()?.runtimeToken || ''), and the secret-handling line the consumer keeps (relay.ts puts the credential in authorization and in no error text or log line; pinned). Two should-fixes:

  1. should-fix — the service NAME belongs beside the interface. Six sibling contracts export their name constant from the contract file (EMBEDDER_SERVICE, KNOWLEDGE_SERVICE, OWNERSHIP_QUEUE_SERVICE, RLS_MEMBERSHIP_RESOLVER_SERVICE, SEED_SETTLEMENT_SERVICE, FILE_ATTACHMENT_SERVICE); this PR spells CLOUD_BINDING_SERVICE = 'cloud-binding' in cloud-connection/src/cloud-binding.ts and again in service-push/src/transports/relay.ts, with no dependency between the two and no cross-package test holding them equal (push-plugin.test.ts sets the literal 'cloud-binding', which pins service-push's copy only). Export CLOUD_BINDING_SERVICE from the contract, import it in both, regenerate api-surface and export-origins.
  2. should-fix — the docblock promises controlPlaneUrl() is '' only when the deployment declared no control plane, but createCloudBinding answers resolveCloudUrl(option), whose ?? treats an explicit empty option as "not declined" and returns the production default — while the plugin's own routes read the same option as (cfg.controlPlaneUrl ?? env ?? '') and treat '' as "cloud unconfigured" (cloud-connection-plugin.ts, line 337 at head). Two readers of one option inside the one plugin the ruling made the owner, disagreeing on ''. Unreachable under objectstack serve (planMarketplaceWiring composes the plugin only when resolveCloudUrl() is non-empty, and passes that value), so not a must-fix; a hand-composed new CloudConnectionPlugin({ controlPlaneUrl: '' }) reaches it. Fix: the routes derive cloudUrl from this.binding.controlPlaneUrl() as they now do for the credential, with the binding reading '' the way the routes do, or the docblock states what '' answers.

packages/spec/src/contracts/index.ts — holds: one export * line in the barrel's order, comment names the registrar and the reader. No finding.

packages/spec/src/stack.zod.ts — holds against Q2 A: the push key's docblock and describe now say absent reads as the relay, which sends once bound and is refused at boot only when OS_CLOUD_URL declares no control plane. That is exactly what resolvePushCapabilityArg does at head (config.transport === 'relay' && isControlPlaneDeclined()), and stack-email-sms.test.ts keeps authoring accepting both transports and the absence. No stale "relay sender ships" text remains under content/docs/references/. No finding.

packages/spec/src/system/push-config.zod.ts — holds against Q2 A and its first consumer push-plugin.ts: both transports served; the relay reads the binding on every send; an unbound server boots, lists no vendor (PushService.vendors() honours available()), the channel's existing isAvailable answers transport_not_configured (the closed reason set does not grow), one warn at kernel:ready; refused at boot only when declined, with the air-gap refusal naming both ways out. The relay block's shape (endpoint optional, no credential) is unchanged and relay.ts honours endpoint over the control-plane path. One should-fix beside it: should-fix — packages/spec/src/system/push-config.test.ts lines 204–207 still head the describe with "The relay is refused at boot until its sender ships (#29920), so its block keeps a marker that says so", and the test two lines below pins the opposite; rewrite the comment to the served reading.

No must-fix found. Check-runs on 1b82707d at review time: no conclusion is red; completed success: Auto Label, Check PR Size, Check Changeset, Check Documentation Links, Flag docs affected by code changes, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Validate Package Dependencies, Dogfood Regression Gate (gate job), Plan Test Core shards, filter; skipped by paths filter: UI Published Artifacts, Dogfood Regression Gate shards, Temporal Conformance, Dogfood Verify CLI, 18.x upgrade harness, Schedule gate, Packed-tarball smoke; still in_progress: Test Core 1/8–8/8, Build Core, Build Docs, Build UI Deps, Live E2E UI, every Lint · job (ESLint; repo and tooling gates; docs and code guards; CI, release and engine gates; PM dispatch-gates; the three scoped lanes) and every Type Check · job (workspace; source gates; consumer gates; debt ledger). The required contexts therefore have no verdict yet; this record does not substitute for them, and the seat reads their conclusions before any landing step.

Implemented-by: claude/project-thread-yvu7tv
Reviewed-by: session_016WV4cyvF7SP65frVFAYLv7

VERDICT: PASS

The status line, the push row in section 4, the dead-token note in section 6
and section 6.1's served-on-main bullet said the relay was not served and
anchored the retired PUSH_RELAY_NOT_SERVED_MESSAGE export. Each amended
passage quotes what it replaces. The push-config test's heading comment
says the same.

Claude-Session: https://claude.ai/code/session_016WV4cznEfnAJ8CyWBdiL2G
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d14d749e6b978c11f1d959ba985a21fd864d776e
Local-runs: none
Scope: docs/adr/0012-notification-platform.md, packages/spec/src/system/push-config.test.ts + packages/services/service-push/src/transports/relay.ts, packages/services/service-push/src/push-plugin.ts, packages/cloud-connection/src/cloud-binding.ts, packages/cloud-connection/src/cloud-connection-plugin.ts, packages/spec/src/contracts/cloud-binding-service.ts, packages/spec/src/system/push-config.zod.ts, packages/spec/src/system/push-device.zod.ts, packages/spec/src/api/push-relay.zod.ts, packages/services/service-messaging/src/push-channel.ts, scripts/check-adr-symbol-anchors.mjs, scripts/symbol-anchors.mjs

Delta review: one commit on top of 1b82707d (record 6673473934, PASS), d14d749e "docs(adr): ADR-0012 reads the hosted push relay as served", 2 files, 8 lines each way. Inputs: the delta diff (git diff 1b82707d d14d749e), card #29920 including the claim amendment 6673499048 (surface widened to the four ADR-0012 readings this PR makes false, gate-forced by check:adr-symbol-anchors red on 1b82707d), the PR body and file list (38 files), and the 66 check-runs on this head. The earlier record's ① and ② stand unchanged for the 36 files this commit does not touch; the earlier should-fixes 3 and 4 are the ones this commit addresses.

① Derived judgments

  • No accept-set change: the commit touches a governed prose surface and a test's heading comment. No schema, export or test assertion moves. Right.
  • The ADR-0012 Status line, the §4 push row's last cell, the §6 dead-token amendment note and the §6.1 "What main serves" bullet now read the relay as served; every other line of the ADR is byte-unchanged, the §6.1 privacy bullet's "in the device locale" half-sentence included (one occurrence before and after). Right: ADR-0012 §6.1: the hosted push relay picks the generic notice line in the recipient's language, not "the device locale" (follow-up to #29920 Q4 C) #30670 owns that wording, as ruled (6670237128 item 6).
  • The four readings this commit replaces were the only ones in docs/adr/** that said the relay was unserved (the grep at head finds the four phrases only inside the quotations the amended passages carry). Right.
  • push-config.test.ts lines 202–205: the heading comment now says both transports are served and the relay is refused at boot only on a deployment that declared no control plane, which is what the describe beneath it pins. Right.

② Semver level

Unchanged from 6673473934: five packages at patch, feat(service-push)!, Clause-②: yes (narrowing) and Metadata-protocol: none line-initial in the PR body and the changeset, the ADR-0087 not-required (runtime-export-retired …) marker with its three predicates holding. A docs/adr edit and a test comment publish nothing and move no declaration. Right.

③ Boundary flags

④ Design judgment

docs/adr/0012-notification-platform.md (governed rules face) — holds. For each amended passage, against the tree at d14d749e:

  1. Status line. States: push ships as service-push with the hosted relay (the default) and the direct transports behind the kernel's push service. True at head (PushServicePlugin.init builds relayTransport('apns'|'fcm', RelayPushSender) for a relay block and the vendor transports for a direct block). Quotes the parenthesis it replaces, verbatim. Anchors: none added.
  2. §4 push row, last cell. States: relay shipped (service-push: send through the hosted relay transport (push-relay.zod.ts wire, cloud binding credential) #29920, PR feat(service-push)!: push sends through the hosted push relay, authenticated by the server's cloud binding #30749), sends through the hosted relay with the server's cloud binding credential, and PushServicePlugin refuses a relay block only on a deployment that declared no control plane (§6.1). True at head (if (config.transport === 'relay') { if (isControlPlaneDeclined()) throw …; new RelayPushSender(…) }). Quotes the cell text it replaces, verbatim. Anchors: none added.
  3. §6 dead-token note. States: service-push ships both transports; a relay invalid answer reaches the channel as a dead token and is retired the same way. True at head (relay.ts answers { status: 'invalid_token', reason: 'relay: invalid' } and push-channel.ts retires on invalid_token as for the direct transports). Quotes the sentence it replaces, verbatim. Anchors: none added; the existing push-device.zod.ts#UserDeviceSchema resolves.
  4. §6.1 "What main serves". States, in order: both transports served; the direct pair (Push channel: build the device registry routes (/notifications/devices) and the APNs + FCM transport that ADR-0012 marks "MUST ship M1" #29041); relay.ts#RelayPushSender POSTs one ID-only PushRelayRequestSchema per push to the relay block's endpoint, else the control-plane URL plus relay.ts#PUSH_RELAY_PATH; the bearer is read on every send from the cloud-binding service registered by cloud-connection (cloud-binding.ts#createCloudBinding, interface cloud-binding-service.ts#ICloudBindingService); unbound lists no vendor, the channel is suppressed as transport_not_configured, the bind takes effect without a restart; a declined deployment (off, none, local, disabled) is refused at init with push-plugin.ts#PUSH_RELAY_AIR_GAPPED_MESSAGE; the channel shell fans out and retires dead tokens; the relay schemas carry no PLANNED marker. Every clause is what the head tree does (RelayPushSender.endpoint(), send(), relayTransport().available, PushService.vendors(), CLOUD_DECLINED_SPELLINGS, CloudConnectionPlugin.init, push-relay.test.ts and push-config.test.ts pin the marker absence). All five new anchors resolve to a declaration site in the named file at head; the retired PUSH_RELAY_NOT_SERVED_MESSAGE appears only inside the quotation, as a bare backticked name beside a bare push-plugin.ts with no directory, which the anchor grammar (scripts/symbol-anchors.mjs, a path must carry a directory) reads as prose, and the passage says in its own words why the quotation drops the anchor. The quotation of the old heading is verbatim; the quotation of the old relay half is a declared paraphrase (the anchor removed, the seven-schema list elided with an ellipsis), which the ADR's convention tolerates because the alternative, a verbatim quotation, would re-anchor a retired symbol and red the gate. One should-fix: the replaced heading pinned the reading to a tree ("objectstack-ai/objectstack at 6a4737a, 2026-11-03"); the new heading names the date and the PR but no commit. A reading is a count plus the tree it was taken against (AGENTS.md, Multi-agent discipline); name the sha the bullet was read at (this head, or the merge commit once landed).

The mechanical verdict on the rules face: Lint & Repo Gates (carrying check:adr-symbol-anchors) is success on this head.

packages/spec/src/system/push-config.test.ts — holds: a four-line heading comment, now consistent with the it beneath it and with push-config.zod.ts's docblock at head; no assertion moves. No finding.

No must-fix found. Check-runs on d14d749e: 66 total, all completed, none failure. The ten required contexts: Lint & Repo Gates success, TypeScript Type Check success, Test Core success (shards 1/8–8/8 each success), Build Core success, Governed Surface Queue Guard success, Lint UI skipped (paths filter), Type Check UI success, Test UI success (16/16 shards), Build & E2E UI success, Live E2E UI success. Advisory: Build Docs, Build UI Deps, Dogfood Regression Gate, Plan Test Core shards, filter, Auto Label, Check PR Size, Check Changeset, Check Documentation Links, Flag docs, PR-body guards, Spec property liveness, Validate Package Dependencies, every Lint · and Type Check · lane — success; skipped by filter: UI Published Artifacts, Dogfood shards, Dogfood Verify CLI, Temporal Conformance, 18.x harness, Packed-tarball smoke, Schedule gate.

Implemented-by: claude/project-thread-yvu7tv
Reviewed-by: session_016WV4cyvF7SP65frVFAYLv7

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

席位核对 · ACCEPT

PR #30749 · head d14d749e · 2026-11-06T01:22Z · card #29920 (claim 6671501964, amendments 6671581752 and 6673499048) · os-dev report 6673285338 · contract review PASS 6673473934 (1b82707d) and delta PASS 6674074177 (d14d749e)

维护者速读(席位校正)

The body's six lines stand, read against the diff, with one correction: since d14d749e the PR also amends ADR-0012 (the Status line, the §4 push row, the §6 dead-token note and §6.1's "What main serves" bullet), so it is now Tier H. Before, those four passages said the relay was not served and §6.1 anchored the retired PUSH_RELAY_NOT_SERVED_MESSAGE, which turned check:adr-symbol-anchors red; the gate makes an anchor-exempt marker maintainer-only. The §6.1 "in the device locale" half-sentence is untouched, left for #30670 as ruled.

席位核对

  • Rulings: Q1 A, Q2 A, Q3 A (default path resolveCloudUrl() + /api/v1/cloud/push/send) and Q4 C, as recorded on the card; the contract review traced each against the tree.
  • BREAKING, declared: PushRelayPayloadSchema.data refuses an undeclared key (it stripped one), and RATE_LIMIT_EXCEEDED.retryable is true only; PUSH_RELAY_NOT_SERVED_MESSAGE is retired with its ADR-0087 runtime-export-retired marker. feat(service-push)!, Clause-②: yes (narrowing), Metadata-protocol: none, five packages at patch.
  • Accepted deviations (review ③): the ADR-0087 marker's narrowings stated in prose rather than as FROM operands (rec A, no ledger entry: the relay wire is stored nowhere); two anchored source aliases in service-push/vitest.config.ts (gate-forced); content/docs/kernel/runtime-services/push-service.mdx edited (it documented the refusal this PR removes); the ADR-0012 passages above (claim amendment 6673499048).
  • Should-fixes, not in this PR: the CLOUD_BINDING_SERVICE export and the empty-controlPlaneUrl reading → filed cloud-binding: name the service in its spec contract, and give controlPlaneUrl one reading of an empty option (follow-up to #29920) #30762. The §6.1 heading's tree pin (delta review should-fix 1) is a wording nit; it rides ADR-0012 §6.1: the hosted push relay picks the generic notice line in the recipient's language, not "the device locale" (follow-up to #29920 Q4 C) #30670, which edits the same section.
  • CI on d14d749e: 66 check-runs completed, none failed (ci-failure.mjs --pr 30749: GREEN).
  • Landing: check-governed-merges --pr 30749: GOVERNED, landing tier H, 1640 changed lines. It needs the maintainer's per-PR word for the BREAKING change, and an APPROVED review by a GOVERNED_APPROVERS account (or the maintainer's own merge). The seat does not flip the draft, queue or arm auto-merge before both.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

Maintainer word · BREAKING · request for governed approval

PR #30749 · head d14d749e · 2026-11-06T03:54Z · card #29920

  • Maintainer's word: the maintainer chose 「同意,等 os-zhuang/hotlong 批准」 on the seat's decision card for this PR (the BREAKING push-relay transport), at 2026-11-06T03:50Z in the seat thread. That is the per-PR word the BREAKING change needs. It does not stand in for the governed approval.
  • What is asked: one APPROVED review from a GOVERNED_APPROVERS account (os-zhuang or hotlong). The path surface hits Tier H (the ADR-0012 hunks in docs/adr/**).
  • Records on this head: seat ACCEPT 6674089209, contract review PASS 6673473934 (1b82707d) and delta PASS 6674074177 (d14d749e); all 66 check-runs green on d14d749e.
  • After approval: the seat runs the pre-landing checks on the head, marks the PR ready and arms auto-merge. Until then the PR stays draft, labelled pm:awaiting-merge.

@os-zhuang
os-zhuang marked this pull request as ready for review November 6, 2026 05:06
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 6, 2026
@objectstack-fleet objectstack-fleet Bot removed the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 6, 2026
Merged via the queue into main with commit 97e4249 Nov 6, 2026
75 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-yvu7tv branch November 6, 2026 05:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:system size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

service-push: send through the hosted relay transport (push-relay.zod.ts wire, cloud binding credential)

3 participants