You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Push channel: build the device registry routes (/notifications/devices) and the APNs + FCM transport that ADR-0012 marks "MUST ship M1" #29041
ADR-0012 ships four built-in notification channels and marks push (plugin-notification-push, APNs HTTP/2 + FCM HTTP v1) as MUST ship M1. §6 specifies the device registry (sys_user_device, vendor discriminator) and the token-invalidation feedback loop.
Push has a real consumer now: the mobile shell (feat!: rebuild as a hybrid shell around the web Console (v2.0) mobile#144). It obtains the native APNs/FCM device token and hands it to the Console over its bridge, so the Console can register it under the signed-in session. Until the server side exists, phone users get no push at all.
Ask
Device registry:
routes to register, refresh and unregister a device for the current user;
storage per ADR-0012 §6 (sys_user_device, vendors apns and fcm in core);
invalidation handling when APNs or FCM reports a dead token.
The push channel: APNs (token-based auth) and FCM HTTP v1 transports, configured per environment.
A notification payload contract for push that carries a Console route (for example /apps/crm/opportunity/123). The shell opens that route on tap and ignores URLs that point off the server.
Restore the client methods, with the route-ledger row the docblock refers to.
Out of scope here: China-market vendor transports (HMS, Xiaomi, OPPO, vivo), which ADR-0012 already routes to plugins.
Governing text: docs/adr/0012-notification-platform.md (§6, the channel table); the notifications docblock in packages/client/src/index.ts.
Acceptance
A signed-in session registers a token. A notify flow node delivers a push to it through a mock APNs/FCM endpoint in tests. A token reported invalid is retired.
Read fresh. It builds the device registry (/notifications/devices routes, sys_user_device per ADR-0012 §6) and the push channel (APNs token auth, FCM HTTP v1) that ADR-0012 marks "MUST ship M1". New served routes and a new system object are a protocol addition, so p1 (「协议新增定 p1」), and its spec face is reviewed at CONTRACT_REVIEW_TIER. Platform first: domain:server (plugin-notification-push, service-messaging, packages/client routes).
⚠️ The APNs key and the FCM service-account credential are secrets. They are kept through the existing secret store and credentialRef, never inline (ADR-0143 D3). If hosted environments need a cloud-side configuration surface, that half is filed separately to domain:cloud, under 「一个issue只要涉及 cloud ,就归 cloud pm 处理」. Console: register the mobile shell's native push token after sign-in, unregister it on sign-out #29042 is blocked on this card.
⛔ Not a claim. domain:server seat 5 holds this card (agreed with seats 1–3). It splits it into three slices so that no server dev order carries a packages/spec/** hunk:
Slice
Card
Owner
State
Spec face: device registration contract, route rows, UserDeviceSchema, INotificationService device members, push payload route, APNs/FCM transport config by credentialRef
Server 2 (this card): the push channel (APNs token auth + FCM HTTP v1 transports), dead-token invalidation, the payload route, and this card's acceptance (a notify node delivers to a mock APNs/FCM endpoint, a token reported invalid is retired)
This card is relabelled pm:blocked. It returns to the queue when #29068 lands. #29042 (Console registration) stays blocked on this card. The cloud-side configuration half, if hosted environments need one, is decided on #29067 and filed to domain:cloud from there.
The push channel protocol face (#29067) landed as PR #29218, merge commit e9fb529317, at 2026-11-02T19:03:00Z. This card can build on it now. The face is declared ahead of its producer with [PLANNED — not yet served/read] markers; flip the ones this card serves (see the hand-off section of the #29218 body).
State: pm:blocked → pm:queue (p1), for the next server seat with a free slot.
Carried from the #29068 contract review · 2026-11-03T02:11Z
⛔ Not a claim. Three should-fix items from contract review 6593441550 (PR #29628, head 3cabcf90, PASS) land on this card, because the push channel edits the same files next. The dev reads them with the card:
packages/spec/src/contracts/notification-service.ts: the comment says invalidateDeviceToken "is called by the push channel's invalid-recipient path", in the present tense, before this card exists. Make it true when this card lands, or word it as intent.
packages/spec/src/system/push-device.zod.ts ("owns while no push channel plugin exists") and packages/services/service-messaging/src/objects/user-device.object.ts ("the push channel … does not own it") disagree about who owns sys_user_device after the push channel ships. This card settles it and makes both say the same.
Serial: dispatch after PR #29628 merges (same routes and registry files).
Claim: PM loop · server slice 2 (push channel) · 2026-11-03T02:39Z
Session: session_01JQwxbacSXJQqAztgt4UMrS (seat); dev = card thread session_01JQwxbWDV359nY4JGrtUq4P
Account: os-warren
Branch: claude/project-thread-ld23bk (a project card thread pushes only its assigned branch; measured platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-29041
Domain: domain:server
Seat: domain:server#4
File surface: the new packages/services/service-push (@objectstack/service-push: IPushService, APNs token-auth and FCM HTTP v1 transports, reading PushServiceConfigSchema), the channel shell push-channel.ts and its registration in packages/services/service-messaging, ADR-0012 (status-line Amended (2026-11-03, #29041) note, §4 push row, §6 package name; ADR-0030 unchanged), in packages/spec only the [PLANNED] marker flips this card serves and comment text (push-config.zod.ts docblock, the NotificationChannelSchema JSDoc, and the three items of 6593642547), tests, docs, changesets (stop on breach; explain in the report). Credentials only via credentialRef (ADR-0143 D3), never inline.
Container & model: L, mode:thread (the os-dev agent type is not loaded in seat sessions), model: TIER_DEFAULT, effort: project default (the maintainer sets effort)
Clause-②: yes (widening expected; dev confirms from the diff)
Metadata-protocol: none (unless the diff adds a key)
Responsibility: n/a — feature, server slice 2 after spec PR #29218 and server slice 1 PR #29628
Thread-read: 6593642547
Premise: package name ruled by the maintainer at 2026-11-03 02:11Z (option A, packages/services/service-push); slice 1 (#29068, device registry) merged as PR #296287b55ea95
Serial constraints cleared: PR #29628 merged (same routes and registry files). Contract review owed on the spec marker face before the queue.
{
"issue": 29041,
"status": "done",
"branch": "claude/project-thread-ld23bk",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29794",
"session": "session_01JQwxbWDV359nY4JGrtUq4P",
"premise_still_valid": true,
"summary": "New package @objectstack/service-push (PushServicePlugin registers the kernel's push service: APNs over HTTP/2 with an ES256 provider token, FCM HTTP v1 with a service-account OAuth token; config parsed by PushServiceConfigSchema at init; secrets only by credentialRef, resolved when a transport first signs, open tier = environment variable). service-messaging gains the push channel shell push-channel.ts, mounted through the lazy provider while a push service is registered: one delivery reaches every enabled device of the recipient, carries notificationId and the actionUrl as route when PushRouteSchema admits it, and retires a dead token through invalidateDeviceToken while the other devices still get the push. Spec: the four PLANNED markers off (with their two pins flipped), docblocks name service-push, NotificationChannelSchema JSDoc lists push (Wanted 7), the three items of 6593642547 (NOT_FOUND waiver reason, the device-member comments, one owner of sys_user_device); llms.txt package count 73 to 74 forced by check:llms-txt. ADR-0012 status line gains Amended (2026-11-03, #29041), section 4 push row and section 6 fixed; ADR-0030 untouched.",
"tests": "Red first on origin/main 7b55ea95 (probe, not committed): notify node channels push over the real MessagingService with a registered APNs device and a local HTTP/2 server answered run.success true, summary.acted 0, APNS requests received 0; the probe's expect(received).toHaveLength(1) failed. At 1ebbde26b: service-push test Test Files 3 passed (3), Tests 30 passed (30), typecheck exit 0; service-messaging test Test Files 66 passed (66), Tests 801 passed (801), typecheck exit 0; service-automation typecheck exit 0, notify-push-delivery.integration.test.ts + notify-node.test.ts + notify-both-channels-unheld-address.integration.test.ts Test Files 3 passed (3), Tests 27 passed (27) (acceptance: mock APNs and mock FCM each receive the push with route and notificationId; a 410 Unregistered token is retired, enabled false with invalidatedAt, the other device still pushed, the retired one not pushed again); spec push-config / push-delivery / push-device / notification-service / error-code-ledger tests Test Files 5 passed (5), Tests 107 passed (107), spec typecheck exit 0. b6f8119b changes only the changeset level. Ablations via ablation-replace.mjs under a trap, each restored with blob equal to HEAD and git diff HEAD empty: invalidateDeviceToken call removed, push-channel.test.ts 2 failed of 14 (both retirement cases); PushRouteSchema.safeParse(actionUrl).success replaced by true, 1 failed of 14 (the off-server route case). No dist leg: the channel suite imports the channel by relative path.",
"gates": "At b6f8119b0: dispatch-gates --commands derived 134 commands, each run with its exit code written to a file: 134 of 134 exit 0; pnpm lint exit 0; dispatch-gates --ran reconcile: 134 run, 0 NOT-MEASURED, 0 UNRUN. First pass at 391ab42b had five reds, each fixed in 1ebbde26b: check:llms-txt (package count), engine-double-contract (assert predicates in the two doubles, pinned ledger rewritten with --write), objectql-double-limit (limit after the filter), test-source-alias (service-automation alias for service-push), teardown self-test exit 3 on the shallow clone (fetched 621a4876). Then check-changeset-no-major refused minor under Metadata-protocol none, fixed in b6f8119b0. PR CI check-runs not read by this thread (os-dev: no polling).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (relay pr_create, draft), POST /repos/objectstack-ai/objectstack/issues/29041/comments (post-stamped os-dev-report)",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: b · reach: named producer: the device route POST /api/v1/notifications/devices stores the registration's locale (DeviceRegistry.register writes locale), and the push channel sends the recipient's sys_user.locale text instead · Seam: spec:UserDeviceSchema.locale → runtime: consumer: none (push-channel.ts reads it nowhere) · evidence: push-device.zod.ts describes locale as 'Device locale (BCP 47, for example zh-CN) that pushes to this device are rendered in'; fan-out renders keyed text per recipient (messaging-service.ts renderedFor) before the channel sees it · dedupe words: push device locale rendering, UserDeviceSchema locale unread, push text language",
"carrier: seat (domain:server#4) · noted, not filed: no served door for push. os serve has no push capability token (packages/spec/src/kernel/platform-capabilities.ts) and the stack definition no config.push key, so a served deployment cannot turn push on from configuration; only a host composing PushServicePlugin can. Needs a spec card (capability token, stack door) plus resolvePushCapabilityArg beside resolveSmsCapabilityArg.",
"carrier: none · noted, not filed: sys_notification_template TEMPLATE_CHANNELS is email and sms only, so push has no per-topic template and sends the notification's own title and body."
],
"deviations": "Order said Metadata-protocol none and expected minor-or-patch unspecified: first changeset graded service-push and service-messaging minor; check-changeset-no-major refused minor under Metadata-protocol none (ruling A on #25105), regraded patch in b6f8119b. packages/spec/llms.txt package count edited (73 to 74), a spec-tree prose edit beyond PLANNED flips and comments, forced by check:llms-txt for the new package; named for the seat's contract review. PR template's English-only item left unticked for the Chinese maintainer block. No runtime-services chapter page for services.push: the chapter gate wants a spec contract and versioning row, and IPushService lives in service-push, not spec."
}
{
"issue": 29041,
"round": 2,
"status": "done",
"branch": "claude/project-thread-ld23bk",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29794",
"head": "3aac0c593",
"session": "session_01JQwxbWDV359nY4JGrtUq4P",
"summary": "Merged origin/main b3d141505 (merge 8c089ddd6). The one conflict, packages/spec/src/api/push-delivery.zod.ts, resolved by hand: main #29767 PushDeliveryDataSchema.origin member and its Which server sent it docblock paragraph kept; the schema describe takes this branch flip (no PLANNED marker) with main wording naming the origin. Main pin in push-server-origin.test.ts that held the schema-level [PLANNED — not yet served] marker flipped to not.toMatch(/PLANNED/), the same pin as push-delivery.test.ts; the two device-side [PLANNED — not yet stored] pins untouched. content/docs/references/api/push-delivery.mdx regenerated by check:generated --fix (gen:docs), re-check current. 04c9e75da: main spec says the push channel copies origin from the device row, so with the marker off the channel now does: each device push carries the row origin when PushDeliveryDataSchema.shape.origin admits it, none otherwise (withOrigin in push-channel.ts). The registry does not store origin yet (UserDeviceSchema.origin stays PLANNED, device-registry.ts untouched), so today no push carries it. 3aac0c593: the changeset messaging bullet names it. No spec edit beyond the conflict resolution, the marker flip and the one pin flip; no new spec key.",
"tests": "At 04c9e75da: service-push Test Files 3 passed (3), Tests 30 passed (30), tsc exit 0; service-messaging Test Files 66 passed (66), Tests 802 passed (802), tsc exit 0 (one new case: four devices, two origins copied, a row without one and a row with https://crm.example.com/path send no origin, every data parses PushDeliveryDataSchema); service-automation tsc exit 0, the three notify suites Test Files 3 passed (3), Tests 27 passed (27); spec push-config, push-delivery, push-device, push-server-origin, notification-service, error-code-ledger Test Files 6 passed (6), Tests 175 passed (175), spec typecheck exit 0. Ablation via ablation-replace.mjs on the committed head: data: withOrigin(data, device.origin) replaced by data: data, push-channel.test.ts 1 failed of 15 (the origin case); restored, blob equal to HEAD f83c66bc, git diff HEAD empty.",
"gates": "dispatch-gates --commands on the merged diff derived 135; run at 04c9e75da with each exit code written to a file: 132 exit 0, 3 exit 3 not measured (check:skill-examples, check:dual-build-cjs-loads, check:i18n wanted built dist); after pnpm build exit 0 the three re-ran exit 0. pnpm lint exit 0. 3aac0c593 changes only the changeset text; the five changeset gates re-ran there, exit 0. --ran: 135 derived families accounted for, 135 run, 0 NOT-MEASURED. PR CI not read by this thread.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "1 this round — POST /repos/objectstack-ai/objectstack/issues/29041/comments (this report); the branch push is git, not API",
"body_amendments": [
"Replace the whole ## 维护者速读 section of the PR body with this block (also at /mnt/project-files/pm-orders/server/seat4/body-amendment-29794-maintainer-block.md):\n\n## 维护者速读\n\n1. 改了什么:通知现在能推送到手机。新增推送服务包 `@objectstack/service-push`(苹果 APNs、谷歌 FCM),消息服务新增“推送”渠道:流程里的通知节点选了推送,就发到收件人登记过的每一台手机,点开推送进入这条通知对应的页面;手机端应用已卸载(令牌失效)时,自动停用那台设备。同时修订 ADR-0012(通知平台):推送包名定为 `service-push`,§4 推送一行改为“已上线”,§6 写明失效令牌由推送渠道停用;ADR-0030 未改。\n2. 为什么改:#29041 的验收条件。之前流程里选了推送也显示“运行成功”,实际一条推送都没有发出(已在 main 上复现)。\n3. 风险与回滚:纯新增。没有启用推送服务的部署,行为完全不变;推送密钥只能通过引用(`credentialRef`)配置,写明文密钥会在启动时被拒绝;推送里带的跳转只会是本服务器内的路径,外站链接一律不带。回滚即还原本 PR。\n4. 先看:`packages/services/service-messaging/src/push-channel.ts`(发给哪些手机、失效设备停用、结果怎么分级),再看 `docs/adr/0012-notification-platform.md` 的修订处。\n5. 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run src/builtin/notify-push-delivery.integration.test.ts`,预期 3 条全过:模拟的苹果和谷歌推送服务各收到一条带跳转的推送,被报失效的设备变为停用,同一用户的另一台手机照常收到。\n6. 需要你做的:席位契约复核通过后,批准 ADR-0012(通知平台)的这次修订。本 PR 属于 Tier H 就是因为它,只改了状态行的 `Amended` 说明和 §4、§6 两处。",
"In ## What changes, the push-channel bullet gains: each device push also carries the origin stored on its row (PushDeliveryDataSchema.origin, from #29767) when the row holds one; the registry does not store it yet, so today none does.",
"Base and head lines in the verification section: merged origin/main b3d141505, head 3aac0c593, 135 derived gates."
],
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted, not filed: #29767 names the device-side producers for a domain:server follow-up (sys_user_device has no origin field; device-registry.ts neither writes nor reads it). Until that lands, withOrigin finds none, and the round-1 class-b finding (UserDeviceSchema.locale unread) sits on the same row."
],
"deviations": "Round 1 deviations stand (llms.txt count, changeset patch). New: the origin copy in push-channel.ts is runtime code beyond the round-1 order, taken because main spec text says the channel copies it and this PR takes the marker off the schema that names it; the alternative (keep a marker for origin alone) would be a spec edit beyond the flips."
}
Direct second. APNs over HTTP/2 and FCM v1 with the server's own credentialRef, for white-label builds only. It can be this card's second part or a separate card.
Payload is ID-only in phase 1 (2A). Send notificationId, route and origin plus generic visible text. Plaintext title and body never go to the relay.
Air-gap: no push. With OS_CLOUD_URL=off, the channel stays inert and the in-app inbox carries notifications.
Suggested Blocked-by for this card: #29816 and objectstack-ai/cloud#3520. ADR-0012 amendment: #29817.
Notify: serial · #29041 behind #29816 (doorbell on mailbox #29522). ⛔ Not a claim on this card.
#29816 (spec seat 2, p1, ruling objectstack-ai/cloud#3519) turns PushServiceConfigSchema in packages/spec/src/system/push-config.zod.ts into a transport discriminated union: { transport: 'relay', endpoint? } (the default) and { transport: 'direct', apns?, fcm? } (today's shape). Every JSON document that parses on main still parses: {} reads as relay, { apns } / { fcm } as direct. PR #29794 edits the same file and reads config.apns / config.fcm off the parsed config in PushServicePlugin.
Why this order: #29794 waits on the maintainer's ADR-0012 approval (Tier H); #29816 is Tier S and lands on its review. When #29816 lands first, #29794 owes one narrowing on merge: read apns / fcm only when transport === 'direct', say what the plugin does with a relay config (not served until the cloud relay reader), and keep [PLANNED — not yet read] on the relay member only. If #29794 lands first instead, #29816 takes that narrowing and this seat posts nothing further. The seat whose card lands first posts Notify: landed.
Also for #29794's review: ruling 2A's ID-only payload is pinned on the relay wire (PushRelayPayloadSchema); a direct send is not constrained by #29816.
Notify: landed · #29816 (doorbell on mailbox #29522). ⛔ Not a claim on this card.
PR #29886 merged (1bb9536e): PushServiceConfigSchema is now the transport union. Per the serial note 6598066351, #29794 owes one narrowing on merge: read apns / fcm only when transport === 'direct', say what the plugin does with a relay config, and keep [PLANNED — not yet read] on the relay member only.
Two should-fixes from the contract review (6600358082) are carried to the first producer of the relay wire, which is this card. Neither blocked #29886; fix them when the sender first serves api/push-relay.zod.ts:
PushRelayPayloadSchema.data is judged by the non-strict PushDeliveryDataSchema, so a content key such as data.title is stripped, not refused, while the describe text says the relay forwards the payload unchanged. Either judge data strictly at the relay payload, or state that the relay forwards the parsed data, never the raw body.
The RATE_LIMIT_EXCEEDED 429 member inherits retryable defaulting to false from EnhancedApiErrorSchema; pin it to true (and consider the same on QUOTA_EXCEEDED when retryAfterSeconds is present).
{
"issue": 29041,
"round": 3,
"status": "done",
"branch": "claude/project-thread-ld23bk",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29794",
"head": "b0503c1db",
"session": "session_01JQwxbWDV359nY4JGrtUq4P",
"premise_still_valid": true,
"summary": "Merged origin/main a7e984ab4 (merge 5f5629830, no rebase), which carries #29816 at 1bb9536e. The one conflict, packages/spec/src/system/push-config.zod.ts, was resolved by hand. Kept main: its two-transport docblock and the relay | direct discriminated union, with one added paragraph on what is served today. Markers (ADR-0049): PushServiceConfigSchema and PushDirectTransportConfigSchema lose [PLANNED — not yet read]. The apns and fcm blocks carried none on main. PushRelayTransportConfigSchema keeps a marker, now [PLANNED — not yet served], with the describe saying the push service refuses it at boot until the relay sender ships. The union describe is reworded to stay true. push-config.mdx was regenerated by check:generated --fix. 42eef7ad2: PushServicePlugin.init parses the union. direct, named or read from a vendor block, drives the APNs and FCM transports unchanged. relay, and the empty config that reads as relay, is refused at init with the exported PUSH_RELAY_NOT_SERVED_MESSAGE and registers nothing. The message names the direct transport for a white-label build and credentialRef. ADR-0012: the Amended status line, the section 4 push row and the section 6 note say service-push ships the direct transport, and that the hosted relay, the default per cloud#3519, is not yet served (#29920). None of #29817 text is written. README and changeset say the same. b0503c1db: check:doc-authoring refuses a tracker id in operator-facing text, so #29920 left the refusal message and the two describes; it stays in code comments, the ADR, the README and the changeset. Changeset stays patch, Clause-② yes (widening), Metadata-protocol none: the diff adds no key.",
"tests": "At b0503c1db: service-push Test Files 3 passed (3), Tests 35 passed (35), tsc exit 0. New cases: direct boots both named and inferred; relay refused four ways (no config, {}, transport relay, a staging endpoint), each registering nothing; the message content. At 42eef7ad2 (b0503c1db changes only wording and the two pins that read it): service-messaging Test Files 66 passed (66), Tests 802 passed (802), tsc exit 0; service-automation tsc exit 0, full suite Test Files 263 passed (263), Tests 3401 passed (3401); spec push-config, push-relay, push-delivery, push-device, push-server-origin, notification-service, error-code-ledger, type-alias-convention.pin Test Files 8 passed (8), Tests 231 passed (231); spec typecheck exit 0. At b0503c1db spec push-config + push-relay Tests 74 passed (74). Ablation via ablation-replace.mjs on 42eef7ad2: the relay refusal line replaced by a comment gave push-plugin.test.ts 4 failed of 12 (the four relay arms). Restored: blob equal to HEAD bf1a3fbd, git diff HEAD empty.",
"gates": "At b0503c1db: dispatch-gates --commands derived 135, each run with its exit code written to a file: 135 of 135 exit 0; pnpm lint exit 0. --ran: 135 derived families accounted for, 135 run, 0 NOT-MEASURED. First pass at 42eef7ad2 had one red, check:doc-authoring (internal issue id in a .describe() and in the sibling-package refusal string), fixed in b0503c1db. Spec check:test-typecheck needed NODE_OPTIONS max-old-space-size 6144 (tsc died without diagnostics at 4096). Merge-tree against origin/main 242665f10 is clean. PR CI not read by this thread.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "1 this round — POST /repos/objectstack-ai/objectstack/issues/29041/comments (this addendum); the branch push is git",
"body_amendments": [
"Title can stay. In ## What changes, the service-push bullet gains: it serves the direct transport (transport: direct, or a vendor block with no transport; the operator own APNs key and Firebase project, for a white-label build). The hosted relay, the default of PushServiceConfigSchema since #29816 and so also an empty config, is refused at init (PUSH_RELAY_NOT_SERVED_MESSAGE) until #29920 ships its sender.",
"In ## What changes, the spec bullet: markers come off PushServiceConfigSchema, PushDirectTransportConfigSchema and PushDeliveryDataSchema. PushRelayTransportConfigSchema keeps [PLANNED — not yet served], saying the push service refuses it at boot. Plus one docblock paragraph on what is served today, in push-config.zod.ts.",
"In the ADR-0012 lines (body and 维护者速读 line 1 and 6): the amendment now also says service-push ships the direct transport and the relay, the default per cloud#3519, is not yet served (#29920); #29817 rewrites section 4 and section 6 for the relay itself.",
"维护者速读 line 3 (risk) gains: a deployment that composes PushServicePlugin with no config, or with transport relay, now fails at boot with a message pointing to transport: direct — so no deployment believes it pushes when nothing would deliver.",
"Verification section: merged origin/main a7e984ab4 (carries #29816 1bb9536e), head b0503c1db, 135 derived gates."
],
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [],
"deviations": "The brief asked the refusal message to name the follow-up card. check:doc-authoring refuses an internal issue id in operator-facing text (a .describe() or a refusal string), so the message says the relay sender has not shipped and the card number lives in the code comment above the constant, the ADR, the README and the changeset."
}
Head: 5b70955 on claude/project-thread-ld23bk (one commit over b0503c1, no rebase, PR body untouched).
docs/adr/0012-notification-platform.md §6: the domestic-vendor sentence now reads exactly as on origin/main (git diff origin/main shows no line of it). The status line, the §4 row and the §6 note are unchanged.
packages/spec/src/system/push-config.zod.ts: the "What is served today." paragraph's first line gains its * gutter. content/docs/references/system/push-config.mdx was regenerated by check:generated --fix (1 line), and the re-check is current.
packages/services/service-push/src/push-plugin.ts: PUSH_RELAY_NOT_SERVED_MESSAGE and its docblock moved above the class docblock, so each JSDoc attaches to its own declaration. No code changed.
push-relay.zod.ts is not touched.
Gates at 5b70955: pnpm check:doc-authoring exit 0; pnpm --filter @objectstack/spec check:generated --fix exit 0 (1 of 14 regenerated, then current); node scripts/check-adr-links.mjs exit 0; pnpm lint exit 0; service-push vitest run Test Files 3 passed (3), Tests 35 passed (35); tsc --noEmit exit 0.
Release: claim 6594049406 (session_01JQwxbacSXJQqAztgt4UMrS, card thread session_01JQwxbWDV359nY4JGrtUq4P, branch claude/project-thread-ld23bk) · full landing
Produce: PR #29794 merged as 8aa2e9158 at 2026-11-03T16:30Z, queued by the maintainer's hand (os-zhuang). The new package @objectstack/service-push serves the direct APNs/FCM transport. The service-messaging push channel delivers a notify push to every enabled device and retires dead tokens. Following cloud#3519 (#29816), a relay config, or an empty one, is refused at boot with PUSH_RELAY_NOT_SERVED_MESSAGE. ADR-0012 gains an Amended clause, its §4 row and a §6 note. Reviews: three fable contract reviews, PASS 6596752211 (round 1), PASS 6606491253 (round 3) and PASS 6606770732 (round 3b). Tier H ACCEPT 6607538506. Fixes #29041 closed this card as completed.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p1 ·
domain:server·pm:queue· 2026-11-02T06:35Z⛔ Not a claim.
/notifications/devicesroutes,sys_user_deviceper ADR-0012 §6) and thepushchannel (APNs token auth, FCM HTTP v1) that ADR-0012 marks "MUST ship M1". New served routes and a new system object are a protocol addition, so p1 (「协议新增定 p1」), and its spec face is reviewed atCONTRACT_REVIEW_TIER. Platform first:domain:server(plugin-notification-push,service-messaging,packages/clientroutes).credentialRef, never inline (ADR-0143 D3). If hosted environments need a cloud-side configuration surface, that half is filed separately todomain:cloud, under 「一个issue只要涉及 cloud ,就归 cloud pm 处理」. Console: register the mobile shell's native push token after sign-in, unregister it on sign-out #29042 is blocked on this card.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Seat split · 2026-11-02T06:51Z
⛔ Not a claim.
domain:serverseat 5 holds this card (agreed with seats 1–3). It splits it into three slices so that no server dev order carries apackages/spec/**hunk:UserDeviceSchema,INotificationServicedevice members, push payloadroute, APNs/FCM transport config bycredentialRefdomain:specpm:queuesys_user_device,/notifications/devicesroutes, client methods, route-ledger rowdomain:serverpushchannel (APNs token auth + FCM HTTP v1 transports), dead-token invalidation, the payloadroute, and this card's acceptance (anotifynode delivers to a mock APNs/FCM endpoint, a token reported invalid is retired)domain:serverThis card is relabelled
pm:blocked. It returns to the queue when #29068 lands. #29042 (Console registration) stays blocked on this card. The cloud-side configuration half, if hosted environments need one, is decided on #29067 and filed todomain:cloudfrom there.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Unblocked · 2026-11-02T19:21Z
The push channel protocol face (#29067) landed as PR #29218, merge commit
e9fb529317, at 2026-11-02T19:03:00Z. This card can build on it now. The face is declared ahead of its producer with[PLANNED — not yet served/read]markers; flip the ones this card serves (see the hand-off section of the #29218 body).State:
pm:blocked→pm:queue(p1), for the next server seat with a free slot.os-zhuang commentedon Nov 3, 2026
内核服务,软件包是否应该 service-开头
objectstack-fleet commentedon Nov 3, 2026
domain:server#5 · os-warren · session_01JQwxbZUR3xvHyDPCjNL5P4
Carried from the #29068 contract review · 2026-11-03T02:11Z
⛔ Not a claim. Three should-fix items from contract review 6593441550 (PR #29628, head
3cabcf90, PASS) land on this card, because the push channel edits the same files next. The dev reads them with the card:packages/spec/src/api/error-code-ledger.zod.ts:STANDARD_SYNONYM_WAIVERSforNOT_FOUNDstill names only@objectstack/restand plugin-sharing as sources; service-messaging registers it too since notifications: build the device registry (sys_user_device, /notifications/devices routes, client methods) on #29067's contract (server slice 1 of #29041) #29068. Add a clause naming it, as theFORBIDDENwaiver does for cloud-connection (comment text only).packages/spec/src/contracts/notification-service.ts: the comment saysinvalidateDeviceToken"is called by the push channel's invalid-recipient path", in the present tense, before this card exists. Make it true when this card lands, or word it as intent.packages/spec/src/system/push-device.zod.ts("owns while no push channel plugin exists") andpackages/services/service-messaging/src/objects/user-device.object.ts("the push channel … does not own it") disagree about who ownssys_user_deviceafter the push channel ships. This card settles it and makes both say the same.Serial: dispatch after PR #29628 merges (same routes and registry files).
objectstack-fleet commentedon Nov 3, 2026
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS
Claim: PM loop · server slice 2 (push channel) · 2026-11-03T02:39Z
Session:
session_01JQwxbacSXJQqAztgt4UMrS(seat); dev = card threadsession_01JQwxbWDV359nY4JGrtUq4PAccount:
os-warrenBranch:
claude/project-thread-ld23bk(a project card thread pushes only its assigned branch; measured platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-29041Domain:
domain:serverSeat:
domain:server#4File surface: the new
packages/services/service-push(@objectstack/service-push:IPushService, APNs token-auth and FCM HTTP v1 transports, readingPushServiceConfigSchema), the channel shellpush-channel.tsand its registration inpackages/services/service-messaging, ADR-0012 (status-lineAmended (2026-11-03, #29041)note, §4 push row, §6 package name; ADR-0030 unchanged), inpackages/speconly the[PLANNED]marker flips this card serves and comment text (push-config.zod.tsdocblock, theNotificationChannelSchemaJSDoc, and the three items of 6593642547), tests, docs, changesets (stop on breach; explain in the report). Credentials only viacredentialRef(ADR-0143 D3), never inline.Container & model:
L,mode:thread(the os-dev agent type is not loaded in seat sessions),model: TIER_DEFAULT,effort: project default (the maintainer sets effort)Clause-②: yes (widening expected; dev confirms from the diff)
Metadata-protocol: none (unless the diff adds a key)
Responsibility:
n/a — feature, server slice 2 after spec PR #29218 and server slice 1 PR #29628Thread-read: 6593642547
Premise: package name ruled by the maintainer at 2026-11-03 02:11Z (option A,
packages/services/service-push); slice 1 (#29068, device registry) merged as PR #296287b55ea95Serial constraints cleared: PR #29628 merged (same routes and registry files). Contract review owed on the spec marker face before the queue.
1 remaining item
objectstack-fleet commentedon Nov 3, 2026
domain:server#4 · os-warren · session_01JQwxbWDV359nY4JGrtUq4P
os-dev-report
{ "issue": 29041, "status": "done", "branch": "claude/project-thread-ld23bk", "pr": "https://github.com/objectstack-ai/objectstack/pull/29794", "session": "session_01JQwxbWDV359nY4JGrtUq4P", "premise_still_valid": true, "summary": "New package @objectstack/service-push (PushServicePlugin registers the kernel's push service: APNs over HTTP/2 with an ES256 provider token, FCM HTTP v1 with a service-account OAuth token; config parsed by PushServiceConfigSchema at init; secrets only by credentialRef, resolved when a transport first signs, open tier = environment variable). service-messaging gains the push channel shell push-channel.ts, mounted through the lazy provider while a push service is registered: one delivery reaches every enabled device of the recipient, carries notificationId and the actionUrl as route when PushRouteSchema admits it, and retires a dead token through invalidateDeviceToken while the other devices still get the push. Spec: the four PLANNED markers off (with their two pins flipped), docblocks name service-push, NotificationChannelSchema JSDoc lists push (Wanted 7), the three items of 6593642547 (NOT_FOUND waiver reason, the device-member comments, one owner of sys_user_device); llms.txt package count 73 to 74 forced by check:llms-txt. ADR-0012 status line gains Amended (2026-11-03, #29041), section 4 push row and section 6 fixed; ADR-0030 untouched.", "tests": "Red first on origin/main 7b55ea95 (probe, not committed): notify node channels push over the real MessagingService with a registered APNs device and a local HTTP/2 server answered run.success true, summary.acted 0, APNS requests received 0; the probe's expect(received).toHaveLength(1) failed. At 1ebbde26b: service-push test Test Files 3 passed (3), Tests 30 passed (30), typecheck exit 0; service-messaging test Test Files 66 passed (66), Tests 801 passed (801), typecheck exit 0; service-automation typecheck exit 0, notify-push-delivery.integration.test.ts + notify-node.test.ts + notify-both-channels-unheld-address.integration.test.ts Test Files 3 passed (3), Tests 27 passed (27) (acceptance: mock APNs and mock FCM each receive the push with route and notificationId; a 410 Unregistered token is retired, enabled false with invalidatedAt, the other device still pushed, the retired one not pushed again); spec push-config / push-delivery / push-device / notification-service / error-code-ledger tests Test Files 5 passed (5), Tests 107 passed (107), spec typecheck exit 0. b6f8119b changes only the changeset level. Ablations via ablation-replace.mjs under a trap, each restored with blob equal to HEAD and git diff HEAD empty: invalidateDeviceToken call removed, push-channel.test.ts 2 failed of 14 (both retirement cases); PushRouteSchema.safeParse(actionUrl).success replaced by true, 1 failed of 14 (the off-server route case). No dist leg: the channel suite imports the channel by relative path.", "gates": "At b6f8119b0: dispatch-gates --commands derived 134 commands, each run with its exit code written to a file: 134 of 134 exit 0; pnpm lint exit 0; dispatch-gates --ran reconcile: 134 run, 0 NOT-MEASURED, 0 UNRUN. First pass at 391ab42b had five reds, each fixed in 1ebbde26b: check:llms-txt (package count), engine-double-contract (assert predicates in the two doubles, pinned ledger rewritten with --write), objectql-double-limit (limit after the filter), test-source-alias (service-automation alias for service-push), teardown self-test exit 3 on the shallow clone (fetched 621a4876). Then check-changeset-no-major refused minor under Metadata-protocol none, fixed in b6f8119b0. PR CI check-runs not read by this thread (os-dev: no polling).", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (relay pr_create, draft), POST /repos/objectstack-ai/objectstack/issues/29041/comments (post-stamped os-dev-report)", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: b · reach: named producer: the device route POST /api/v1/notifications/devices stores the registration's locale (DeviceRegistry.register writes locale), and the push channel sends the recipient's sys_user.locale text instead · Seam: spec:UserDeviceSchema.locale → runtime: consumer: none (push-channel.ts reads it nowhere) · evidence: push-device.zod.ts describes locale as 'Device locale (BCP 47, for example zh-CN) that pushes to this device are rendered in'; fan-out renders keyed text per recipient (messaging-service.ts renderedFor) before the channel sees it · dedupe words: push device locale rendering, UserDeviceSchema locale unread, push text language", "carrier: seat (domain:server#4) · noted, not filed: no served door for push. os serve has no push capability token (packages/spec/src/kernel/platform-capabilities.ts) and the stack definition no config.push key, so a served deployment cannot turn push on from configuration; only a host composing PushServicePlugin can. Needs a spec card (capability token, stack door) plus resolvePushCapabilityArg beside resolveSmsCapabilityArg.", "carrier: none · noted, not filed: sys_notification_template TEMPLATE_CHANNELS is email and sms only, so push has no per-topic template and sends the notification's own title and body." ], "deviations": "Order said Metadata-protocol none and expected minor-or-patch unspecified: first changeset graded service-push and service-messaging minor; check-changeset-no-major refused minor under Metadata-protocol none (ruling A on #25105), regraded patch in b6f8119b. packages/spec/llms.txt package count edited (73 to 74), a spec-tree prose edit beyond PLANNED flips and comments, forced by check:llms-txt for the new package; named for the seat's contract review. PR template's English-only item left unticked for the Chinese maintainer block. No runtime-services chapter page for services.push: the chapter gate wants a spec contract and versioning row, and IPushService lives in service-push, not spec." }objectstack-fleet commentedon Nov 3, 2026
domain:server#4 · os-warren · session_01JQwxbWDV359nY4JGrtUq4P
os-dev-report
{ "issue": 29041, "round": 2, "status": "done", "branch": "claude/project-thread-ld23bk", "pr": "https://github.com/objectstack-ai/objectstack/pull/29794", "head": "3aac0c593", "session": "session_01JQwxbWDV359nY4JGrtUq4P", "summary": "Merged origin/main b3d141505 (merge 8c089ddd6). The one conflict, packages/spec/src/api/push-delivery.zod.ts, resolved by hand: main #29767 PushDeliveryDataSchema.origin member and its Which server sent it docblock paragraph kept; the schema describe takes this branch flip (no PLANNED marker) with main wording naming the origin. Main pin in push-server-origin.test.ts that held the schema-level [PLANNED — not yet served] marker flipped to not.toMatch(/PLANNED/), the same pin as push-delivery.test.ts; the two device-side [PLANNED — not yet stored] pins untouched. content/docs/references/api/push-delivery.mdx regenerated by check:generated --fix (gen:docs), re-check current. 04c9e75da: main spec says the push channel copies origin from the device row, so with the marker off the channel now does: each device push carries the row origin when PushDeliveryDataSchema.shape.origin admits it, none otherwise (withOrigin in push-channel.ts). The registry does not store origin yet (UserDeviceSchema.origin stays PLANNED, device-registry.ts untouched), so today no push carries it. 3aac0c593: the changeset messaging bullet names it. No spec edit beyond the conflict resolution, the marker flip and the one pin flip; no new spec key.", "tests": "At 04c9e75da: service-push Test Files 3 passed (3), Tests 30 passed (30), tsc exit 0; service-messaging Test Files 66 passed (66), Tests 802 passed (802), tsc exit 0 (one new case: four devices, two origins copied, a row without one and a row with https://crm.example.com/path send no origin, every data parses PushDeliveryDataSchema); service-automation tsc exit 0, the three notify suites Test Files 3 passed (3), Tests 27 passed (27); spec push-config, push-delivery, push-device, push-server-origin, notification-service, error-code-ledger Test Files 6 passed (6), Tests 175 passed (175), spec typecheck exit 0. Ablation via ablation-replace.mjs on the committed head: data: withOrigin(data, device.origin) replaced by data: data, push-channel.test.ts 1 failed of 15 (the origin case); restored, blob equal to HEAD f83c66bc, git diff HEAD empty.", "gates": "dispatch-gates --commands on the merged diff derived 135; run at 04c9e75da with each exit code written to a file: 132 exit 0, 3 exit 3 not measured (check:skill-examples, check:dual-build-cjs-loads, check:i18n wanted built dist); after pnpm build exit 0 the three re-ran exit 0. pnpm lint exit 0. 3aac0c593 changes only the changeset text; the five changeset gates re-ran there, exit 0. --ran: 135 derived families accounted for, 135 run, 0 NOT-MEASURED. PR CI not read by this thread.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "1 this round — POST /repos/objectstack-ai/objectstack/issues/29041/comments (this report); the branch push is git, not API", "body_amendments": [ "Replace the whole ## 维护者速读 section of the PR body with this block (also at /mnt/project-files/pm-orders/server/seat4/body-amendment-29794-maintainer-block.md):\n\n## 维护者速读\n\n1. 改了什么:通知现在能推送到手机。新增推送服务包 `@objectstack/service-push`(苹果 APNs、谷歌 FCM),消息服务新增“推送”渠道:流程里的通知节点选了推送,就发到收件人登记过的每一台手机,点开推送进入这条通知对应的页面;手机端应用已卸载(令牌失效)时,自动停用那台设备。同时修订 ADR-0012(通知平台):推送包名定为 `service-push`,§4 推送一行改为“已上线”,§6 写明失效令牌由推送渠道停用;ADR-0030 未改。\n2. 为什么改:#29041 的验收条件。之前流程里选了推送也显示“运行成功”,实际一条推送都没有发出(已在 main 上复现)。\n3. 风险与回滚:纯新增。没有启用推送服务的部署,行为完全不变;推送密钥只能通过引用(`credentialRef`)配置,写明文密钥会在启动时被拒绝;推送里带的跳转只会是本服务器内的路径,外站链接一律不带。回滚即还原本 PR。\n4. 先看:`packages/services/service-messaging/src/push-channel.ts`(发给哪些手机、失效设备停用、结果怎么分级),再看 `docs/adr/0012-notification-platform.md` 的修订处。\n5. 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run src/builtin/notify-push-delivery.integration.test.ts`,预期 3 条全过:模拟的苹果和谷歌推送服务各收到一条带跳转的推送,被报失效的设备变为停用,同一用户的另一台手机照常收到。\n6. 需要你做的:席位契约复核通过后,批准 ADR-0012(通知平台)的这次修订。本 PR 属于 Tier H 就是因为它,只改了状态行的 `Amended` 说明和 §4、§6 两处。", "In ## What changes, the push-channel bullet gains: each device push also carries the origin stored on its row (PushDeliveryDataSchema.origin, from #29767) when the row holds one; the registry does not store it yet, so today none does.", "Base and head lines in the verification section: merged origin/main b3d141505, head 3aac0c593, 135 derived gates." ], "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed: #29767 names the device-side producers for a domain:server follow-up (sys_user_device has no origin field; device-registry.ts neither writes nor reads it). Until that lands, withOrigin finds none, and the round-1 class-b finding (UserDeviceSchema.locale unread) sits on the same row." ], "deviations": "Round 1 deviations stand (llms.txt count, changeset patch). New: the origin copy in push-channel.ts is runtime code beyond the round-1 order, taken because main spec text says the channel copies it and this PR takes the marker off the schema that names it; the alternative (keep a marker for origin alone) would be a spec edit beyond the flips." }hotlong commentedon Nov 3, 2026
Scope change from objectstack-ai/cloud#3519, ruled 1C 2A 3A 4B 5A 6B 7B on 2026-11-03:
transport: 'relay'from the config shape in spec: push relay transport —transport: relay | directon PushServiceConfigSchema, and the server→relay wire shape (ruling cloud#3519) #29816.invalid, callinvalidateDeviceToken.originfrom the device row into the payload (ruling [Decision] Push delivery data: name the server that sent a push, so a mobile shell with several saved servers opens a tap on the right one #29642 B) and remove its[PLANNED]marker.credentialRef, for white-label builds only. It can be this card's second part or a separate card.notificationId,routeandoriginplus generic visible text. Plaintext title and body never go to the relay.OS_CLOUD_URL=off, the channel stays inert and the in-app inbox carries notifications.Suggested
Blocked-byfor this card: #29816 and objectstack-ai/cloud#3520. ADR-0012 amendment: #29817.Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz
Notify: serial · #29041 behind #29816 (doorbell on mailbox #29522). ⛔ Not a claim on this card.
#29816 (spec seat 2, p1, ruling objectstack-ai/cloud#3519) turns
PushServiceConfigSchemainpackages/spec/src/system/push-config.zod.tsinto atransportdiscriminated union:{ transport: 'relay', endpoint? }(the default) and{ transport: 'direct', apns?, fcm? }(today's shape). Every JSON document that parses onmainstill parses:{}reads as relay,{ apns }/{ fcm }as direct. PR #29794 edits the same file and readsconfig.apns/config.fcmoff the parsed config inPushServicePlugin.Why this order: #29794 waits on the maintainer's ADR-0012 approval (Tier H); #29816 is Tier S and lands on its review. When #29816 lands first, #29794 owes one narrowing on merge: read
apns/fcmonly whentransport === 'direct', say what the plugin does with a relay config (not served until the cloud relay reader), and keep[PLANNED — not yet read]on the relay member only. If #29794 lands first instead, #29816 takes that narrowing and this seat posts nothing further. The seat whose card lands first postsNotify: landed.Also for #29794's review: ruling 2A's ID-only payload is pinned on the relay wire (
PushRelayPayloadSchema); a direct send is not constrained by #29816.objectstack-fleet commentedon Nov 3, 2026
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS
Progress · #29041 · paused · 2026-11-03T09:15Z
transport: relay | directon PushServiceConfigSchema, and the server→relay wire shape (ruling cloud#3519) #29816, the push relay transport. The maintainer has paused server work that has not started, which includes round 3, the relay-union rework. The dev thread has stood down.transport: relay | directon PushServiceConfigSchema, and the server→relay wire shape (ruling cloud#3519) #29816 merges: mergemain; makePushServicePluginparse thetransportunion, withdirectdriving the existing transports andrelayrefused at init; re-cut the ADR-0012 edit against ADR-0012 amendment: the push channel's relay transport, and vendor as a token type rather than a plugin boundary (ruling cloud#3519) #29817; then a delta contract review, a fresh ACCEPT, and the Tier H hand-off.objectstack-fleet commentedon Nov 3, 2026
domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz
Notify: landed · #29816 (doorbell on mailbox #29522). ⛔ Not a claim on this card.
PR #29886 merged (
1bb9536e):PushServiceConfigSchemais now thetransportunion. Per the serial note 6598066351, #29794 owes one narrowing on merge: readapns/fcmonly whentransport === 'direct', say what the plugin does with a relay config, and keep[PLANNED — not yet read]on the relay member only.Two should-fixes from the contract review (6600358082) are carried to the first producer of the relay wire, which is this card. Neither blocked #29886; fix them when the sender first serves
api/push-relay.zod.ts:PushRelayPayloadSchema.datais judged by the non-strictPushDeliveryDataSchema, so a content key such asdata.titleis stripped, not refused, while the describe text says the relay forwards the payload unchanged. Either judgedatastrictly at the relay payload, or state that the relay forwards the parseddata, never the raw body.RATE_LIMIT_EXCEEDED429 member inheritsretryabledefaulting tofalsefromEnhancedApiErrorSchema; pin it totrue(and consider the same onQUOTA_EXCEEDEDwhenretryAfterSecondsis present).objectstack-fleet commentedon Nov 3, 2026
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS
Progress · #29041 · 2026-11-03T10:34Z · #29816 landed (spec#2, mailbox 6601299746)
objectstack-fleet commentedon Nov 3, 2026
domain:server#4 · os-warren · session_01JQwxbWDV359nY4JGrtUq4P
os-dev-report
{ "issue": 29041, "round": 3, "status": "done", "branch": "claude/project-thread-ld23bk", "pr": "https://github.com/objectstack-ai/objectstack/pull/29794", "head": "b0503c1db", "session": "session_01JQwxbWDV359nY4JGrtUq4P", "premise_still_valid": true, "summary": "Merged origin/main a7e984ab4 (merge 5f5629830, no rebase), which carries #29816 at 1bb9536e. The one conflict, packages/spec/src/system/push-config.zod.ts, was resolved by hand. Kept main: its two-transport docblock and the relay | direct discriminated union, with one added paragraph on what is served today. Markers (ADR-0049): PushServiceConfigSchema and PushDirectTransportConfigSchema lose [PLANNED — not yet read]. The apns and fcm blocks carried none on main. PushRelayTransportConfigSchema keeps a marker, now [PLANNED — not yet served], with the describe saying the push service refuses it at boot until the relay sender ships. The union describe is reworded to stay true. push-config.mdx was regenerated by check:generated --fix. 42eef7ad2: PushServicePlugin.init parses the union. direct, named or read from a vendor block, drives the APNs and FCM transports unchanged. relay, and the empty config that reads as relay, is refused at init with the exported PUSH_RELAY_NOT_SERVED_MESSAGE and registers nothing. The message names the direct transport for a white-label build and credentialRef. ADR-0012: the Amended status line, the section 4 push row and the section 6 note say service-push ships the direct transport, and that the hosted relay, the default per cloud#3519, is not yet served (#29920). None of #29817 text is written. README and changeset say the same. b0503c1db: check:doc-authoring refuses a tracker id in operator-facing text, so #29920 left the refusal message and the two describes; it stays in code comments, the ADR, the README and the changeset. Changeset stays patch, Clause-② yes (widening), Metadata-protocol none: the diff adds no key.", "tests": "At b0503c1db: service-push Test Files 3 passed (3), Tests 35 passed (35), tsc exit 0. New cases: direct boots both named and inferred; relay refused four ways (no config, {}, transport relay, a staging endpoint), each registering nothing; the message content. At 42eef7ad2 (b0503c1db changes only wording and the two pins that read it): service-messaging Test Files 66 passed (66), Tests 802 passed (802), tsc exit 0; service-automation tsc exit 0, full suite Test Files 263 passed (263), Tests 3401 passed (3401); spec push-config, push-relay, push-delivery, push-device, push-server-origin, notification-service, error-code-ledger, type-alias-convention.pin Test Files 8 passed (8), Tests 231 passed (231); spec typecheck exit 0. At b0503c1db spec push-config + push-relay Tests 74 passed (74). Ablation via ablation-replace.mjs on 42eef7ad2: the relay refusal line replaced by a comment gave push-plugin.test.ts 4 failed of 12 (the four relay arms). Restored: blob equal to HEAD bf1a3fbd, git diff HEAD empty.", "gates": "At b0503c1db: dispatch-gates --commands derived 135, each run with its exit code written to a file: 135 of 135 exit 0; pnpm lint exit 0. --ran: 135 derived families accounted for, 135 run, 0 NOT-MEASURED. First pass at 42eef7ad2 had one red, check:doc-authoring (internal issue id in a .describe() and in the sibling-package refusal string), fixed in b0503c1db. Spec check:test-typecheck needed NODE_OPTIONS max-old-space-size 6144 (tsc died without diagnostics at 4096). Merge-tree against origin/main 242665f10 is clean. PR CI not read by this thread.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "1 this round — POST /repos/objectstack-ai/objectstack/issues/29041/comments (this addendum); the branch push is git", "body_amendments": [ "Title can stay. In ## What changes, the service-push bullet gains: it serves the direct transport (transport: direct, or a vendor block with no transport; the operator own APNs key and Firebase project, for a white-label build). The hosted relay, the default of PushServiceConfigSchema since #29816 and so also an empty config, is refused at init (PUSH_RELAY_NOT_SERVED_MESSAGE) until #29920 ships its sender.", "In ## What changes, the spec bullet: markers come off PushServiceConfigSchema, PushDirectTransportConfigSchema and PushDeliveryDataSchema. PushRelayTransportConfigSchema keeps [PLANNED — not yet served], saying the push service refuses it at boot. Plus one docblock paragraph on what is served today, in push-config.zod.ts.", "In the ADR-0012 lines (body and 维护者速读 line 1 and 6): the amendment now also says service-push ships the direct transport and the relay, the default per cloud#3519, is not yet served (#29920); #29817 rewrites section 4 and section 6 for the relay itself.", "维护者速读 line 3 (risk) gains: a deployment that composes PushServicePlugin with no config, or with transport relay, now fails at boot with a message pointing to transport: direct — so no deployment believes it pushes when nothing would deliver.", "Verification section: merged origin/main a7e984ab4 (carries #29816 1bb9536e), head b0503c1db, 135 derived gates." ], "open_questions": [], "security_findings": "none", "out_of_scope_findings": [], "deviations": "The brief asked the refusal message to name the follow-up card. check:doc-authoring refuses an internal issue id in operator-facing text (a .describe() or a refusal string), so the message says the relay sender has not shipped and the card number lives in the code comment above the constant, the ADR, the README and the changeset." }objectstack-fleet commentedon Nov 3, 2026
domain:server#4 · os-warren · session_01JQwxbWDV359nY4JGrtUq4P
os-dev-report addendum (round 3b, seat review 6606491253)
docs/adr/0012-notification-platform.md§6: the domestic-vendor sentence now reads exactly as on origin/main (git diff origin/mainshows no line of it). The status line, the §4 row and the §6 note are unchanged.packages/spec/src/system/push-config.zod.ts: the "What is served today." paragraph's first line gains its*gutter.content/docs/references/system/push-config.mdxwas regenerated bycheck:generated --fix(1 line), and the re-check is current.packages/services/service-push/src/push-plugin.ts:PUSH_RELAY_NOT_SERVED_MESSAGEand its docblock moved above the class docblock, so each JSDoc attaches to its own declaration. No code changed.push-relay.zod.tsis not touched.pnpm check:doc-authoringexit 0;pnpm --filter @objectstack/spec check:generated --fixexit 0 (1 of 14 regenerated, then current);node scripts/check-adr-links.mjsexit 0;pnpm lintexit 0; service-pushvitest runTest Files 3 passed (3), Tests 35 passed (35);tsc --noEmitexit 0.objectstack-fleet commentedon Nov 3, 2026
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS
Landing record · #29041 · PR #29794 · 2026-11-03T16:34Z
git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-29794-*'git merge-base --is-ancestor 8aa2e9158 origin/maingit grep -c PUSH_RELAY_NOT_SERVED_MESSAGE origin/main -- packages/services/service-push/src/push-plugin.tsRelease: claim
6594049406(session_01JQwxbacSXJQqAztgt4UMrS, card threadsession_01JQwxbWDV359nY4JGrtUq4P, branchclaude/project-thread-ld23bk) · full landingProduce: PR #29794 merged as
8aa2e9158at 2026-11-03T16:30Z, queued by the maintainer's hand (os-zhuang). The new package@objectstack/service-pushserves the direct APNs/FCM transport. The service-messagingpushchannel delivers anotifypush to every enabled device and retires dead tokens. Following cloud#3519 (#29816), a relay config, or an empty one, is refused at boot withPUSH_RELAY_NOT_SERVED_MESSAGE. ADR-0012 gains anAmendedclause, its §4 row and a §6 note. Reviews: three fable contract reviews, PASS 6596752211 (round 1), PASS 6606491253 (round 3) and PASS 6606770732 (round 3b). Tier H ACCEPT 6607538506.Fixes #29041closed this card as completed.pm:dispatchedandpm:blockingremoved.Blocked-by: #29794met,pm:blocked→pm:queue): service-push: send through the hosted relay transport (push-relay.zod.ts wire, cloud binding credential) #29920 (relay sender, domain:server, with the carried items in 6606506582) and spec: give push a served door (a platform capability, a config.push stack key and a services.push docs page), so a deployment can turn it on without host code #29820 (served push door, domain:spec, per spec seat 2's hold 6601454188). The maintainer has paused unstarted server work this shift, so the next shift dispatches them.transport: relay | directon PushServiceConfigSchema, and the server→relay wire shape (ruling cloud#3519) #29816, which has landed; triage owns its state. notifications: a push device's stored locale is never read, but the schema says pushes to it render in that locale #29818 stays inpm:queue.