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
realtime: the bell cannot prefer a live channel because discovery advertises realtime as enabled:true with no HTTP/WS surface mounted and no route to connect to #14646
GET /api/v1/discovery on a stock showcase boot reports the realtime service as enabled: true but status: "degraded", handlerReady: false, message "In-process event bus only — no HTTP/WS realtime surface is mounted", and the routes map carries no realtime entry. RealtimeServicePlugin is listed among the 48 loaded plugins at boot.
So a console client has nothing to subscribe to, and the inbox bell / approvals badge stay on polling (10 s foreground, 60 s hidden). objectui#7249 asked the bell to "prefer the realtime channel when the server advertises it"; that half is unreachable from objectui until this repo advertises a channel, and was handed here (multi-repo rule 3, contract-first: the missing piece is a server transport, not a client).
What this card asks for
Decide whether the open framework mounts an HTTP/WS (or SSE) realtime surface at all, or whether that stays a cloud/enterprise service — either way, say it in one place.
If it mounts: advertise the route in /discovery (ADR-0076 D12 — mount everything advertised, advertise only what is mounted) and expose a subscribe primitive in @objectstack/client, so the bell can subscribe and fall back to polling.
AI-error note worth pinning whichever way this goes: enabled: true is NOT the predicate a client may read as "a channel exists" — a client keying on it would subscribe to nothing and silently lose the bell. The honest predicate is handlerReady: true AND a route to connect to; discovery should make that shape unmistakable.
Source: os-dev report on objectui#7249 (open question 2, recommendation A).
Triage: needs-user-decision · domain:services · priority:p2. The card's item 1 is a product-capability call, which is on the manual floor.
Triage seat, session session_01SwJQDFKe8tVit3BXQ9EfR5, R+145, 2026-09-04T15:35Z. Routed domain:services because both branches land there (RealtimeServicePlugin, the discovery payload) with a @objectstack/client limb under B.
Why it is escalated rather than adjudicated: item 1 asks whether the open framework ships a realtime transport at all, or whether that stays cloud/enterprise. That is a product capability boundary — 功能新增类 and a tiering statement about what open core promises. Neither is a triage call, whatever the four facets say.
⛔ The card cannot be split to unblock the mechanical half. The honest-predicate repair (item 3) looks independent and is not: under A discovery becomes truthful by retracting the claim, under B by making it true. Same defect, opposite edits. So the decision gates it.
The fork
A — realtime stays out of open core. Discovery stops advertising a service with no mounted surface as enabled: true; the contract is restored by retraction. The console keeps polling (10 s foreground, 60 s hidden), which works today. Zero new capability.
B — open core mounts a realtime surface (WS or SSE), advertises the route per ADR-0076 D12, and exposes a subscribe primitive in @objectstack/client so the bell can subscribe with a polling fallback.
Maintainer ruling recorded — A: realtime stays out of open core; /discovery stops advertising a realtime service that has no mounted surface, and "what counts as a subscribable channel" becomes one explicit definition
Director seat, summon #14, session session_01LsEjuNMPitCHwEfYftZ1om (GitHub os-warren), 2026-09-04. Provenance: maintainer, live PM chat, decision batch #38 (item 4, presented with the recommendation A, fallback B), verbatim reply 「同意」. Premise: triage facets 5542203078 (ADR-0076 D12 — advertise only what is mounted, mount what is advertised — violated on both ends: enabled: true, handlerReady: false, no realtime route; one consumer, objectui#7249, whose polling fallback works today).
Ruled: A. The open framework does not mount a realtime transport in this ruling. Discovery is made truthful by retraction: RealtimeServicePlugin (or whatever registers the realtime discovery entry) advertises enabled: false when no transport surface is mounted, and the honest predicate — handlerReady: trueand a connectable route — is written down once, where discovery consumers can read it, so enabled: true can no longer be read as "there is a channel". The console keeps polling. Not taken: B (mount WS/SSE + advertise the route + a @objectstack/client subscribe primitive) — the fallback if the maintainer had judged realtime part of open core's promise; if it is ever taken, ADR-0076 D12 requires route and primitive in the same change.
Why (① ≥50%): a realtime transport is a large, permanent operational surface (connection lifecycle, socket auth, backpressure, session affinity under horizontal scale) that binds every self-hosted deployment; restoring the D12 invariant by retraction keeps the surface small and leaves transport to the layer with an operations model. ② is one consumer with a working fallback and zero reports; ④ points the same way, consistent with the 2026-09-03 「我们是一个开发工具」 ruling (#14824). ③ is paid under either option: the discovery shape must be unmisreadable.
Execution:domain:services lane, S. Re-measure /discovery on today's origin/main first (triage did not reproduce the 2026-09-03 showcase reading); pin the retraction (realtime entry enabled: false with no mounted surface; the predicate definition under test); update the discovery docs line. Clause-②: yes (the published discovery payload changes what it says for realtime) ⇒ needs:contract-review on the PR. Changeset: the package that owns the discovery entry, minor with a note (a client that keyed on enabled: true to subscribe was subscribing to nothing; it now sees false). Cross-link objectui#7249 so the bell card knows the answer is "poll".
State transition, same stroke: needs-user-decision → pm:queue. priority:p2 · domain:services unchanged. Ledger: director seat post #12708, batch #38.
Correction to the ruling comment above (5542632689): the director session id is session_01LsEjuNMPitCHwEfYftZ1um (typo in the last three characters there). The ruling, provenance and state transition are unchanged.
Claimed and dispatched — pm:queue → pm:dispatched.
Claim: domain:services execution seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ
Branch: claude/issue-14646-discovery-stops-advertising-unmounted-realtime
Clause-②: no
⚠️ This seat's reading, ⛔ not a ruling; the dev re-declares from the diff. ⭐ It can flip:/discovery's response shape is a wire contract. Changing a value it reports is a behaviour change (no); changing its shape — a new key, a new accepted value in an enum — is a published contract face ⇒ yes, needs:contract-review on PR and card, and ⚠️CONTRACT_REVIEW_TIER is quota-exhausted, so such a PR parks.
The ruling — settled, ⛔ not reopenable
Maintainer, director summon #14, recorded at 5542632689 (and its own typo correction at 5542640476 — the ruling is unchanged): A.
realtime stays out of open core;
/discovery stops advertising a realtime service that has no mounted surface;
⭐ 「what counts as a subscribable channel」 becomes one explicit definition — that third clause is the substance, ⛔ not a footnote: the bug is that enabled: true and 「no HTTP/WS surface is mounted」 are both true at once, which means the field had no single meaning.
What the card measured
On a stock showcase boot, GET /api/v1/discovery reports realtime as enabled: truebutstatus: "degraded", handlerReady: false, message 「In-process event bus only — no HTTP/WS realtime surface is mounted」, and the routes map carries no realtime entry — while RealtimeServicePlugin is among the 48 loaded plugins. ⇒ a console client has nothing to subscribe to, and the inbox bell / approvals badge stay on polling.
⚠️This seat could NOT locate the discovery handler — a grep for handlerReady returned only CHANGELOGs and unrelated packages/core/src/fallbacks/*. ⇒ finding it is the round's first step, and ⛔ this seat hands over no path, rather than a guessed one.
In-flight check
No open PR of the 33 touches a path matching discovery. Re-check against the real file set once you have located it.
{
"issue": 14646,
"status": "done",
"branch": "claude/issue-14646-discovery-stops-advertising-unmounted-realtime",
"pr": "https://github.com/objectstack-ai/objectstack/pull/15978",
"premise_still_valid": true,
"summary": "A1 answered: the handler is `discoveryHandler` in packages/rest/src/rest-server.ts (registerDiscoveryEndpoints), serving GET /api/v1/discovery by calling ObjectStackProtocolImplementation.getDiscovery() in packages/metadata-protocol/src/protocol.ts; the second producer is HttpDispatcher.getDiscoveryInfo() in packages/runtime/src/http-dispatcher.ts. The card's premise reproduced on today's origin/main: the ablation leg (both producers restored to merge base d4c2cb196) reports services.realtime.enabled true beside 'no HTTP/WS realtime surface is mounted'. Clause 3 is the deliverable: 'A subscribable channel exists only where discovery reports handlerReady: true together with a connectable route; enabled never means \"there is a channel\".' That sentence lives in isSubscribableChannel() in @objectstack/spec/api and is ENFORCED by being the computation both producers use to set services.realtime.enabled and capabilities.websockets (previously a literal false in each producer). Applied per slot via CHANNEL_SURFACE_SLOTS (realtime alone), so cache/queue/job keep their honest enabled:true with no route. The route comes from the occupant (new optional IRealtimeService.getChannelRoute), because no open-core producer mounts a realtime transport; @objectstack/service-realtime names none, which is the retraction, computed rather than hardcoded. No realtime surface, route, handler or client primitive is implemented.",
"tests": "Suites and typechecks at ff617529c; ratchet-class gates re-run at the final head 04b60ff7d after the docs commit. Exit codes captured by redirecting first, never after a pipe. RED FIRST (ablation): fix committed first, then both producer files restored to merge base d4c2cb196 with an EXIT/INT/TERM restore trap on absolute paths. Mutation proved on disk by marker counts (isSubscribableChannel 5 -> 0 in protocol.ts, 4 -> 0 in http-dispatcher.ts) and by on-disk blob hashes equalling the base blobs. No rebuild needed for either leg: both pin files import their producer from local src, and spec was deliberately left at HEAD so the ablation isolates the producers rather than the definition the pins import. RED: metadata-protocol/src/discovery-realtime-channel.pin.test.ts exit 1, 3 failed / 2 passed - 'does NOT advertise an in-process realtime bus as a channel' (expected true to be false), 'DOES advertise a realtime occupant that mounts a channel' (expected 'degraded' to be 'available'), 'answers enabled, routes.realtime and capabilities.websockets with the SAME predicate' (services.realtime.enabled: expected true to be false). runtime/src/discovery-realtime-channel.pin.test.ts exit 1, 3 failed / 1 passed, same three assertions. RESTORE: git checkout HEAD -- both paths; post-restore blob hashes equal the HEAD blob hashes and git diff HEAD over the two files is empty. GREEN: metadata-protocol full package test exit 0 (2391 passed, 10 skipped, 166 files); objectql full package test exit 0 (4714 passed, 273 files); runtime full package test exit 0 (3295 passed, 232 files); service-realtime full package test exit 0 (31 passed); rest's five discovery-*.test.ts against a rebuilt metadata-protocol dist exit 0 (40 passed). The runtime negative pin drives the REAL InMemoryRealtimeAdapter that RealtimeServicePlugin registers (devDependency plus a src alias, the idiom runtime already uses for service-job / service-package), not a stand-in. typecheck exit 0 for spec, metadata-protocol, runtime, objectql, service-realtime - runtime was red once on two TS2339 in the new pin file (the dispatcher's realtime entry inferred as a union without `route`), fixed by annotating it ServiceInfo, then 0. Gate family derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` from the real change set (not a hand list); it warned the tree is 5 commits behind origin/main on three files unrelated to this diff. Green, each exit captured: spec check:generated (0 after regenerating api-surface/, export-origins/, content/docs/references/**), check:api-surface, check:authorable-surface, check:export-origins, check:exported-any, check:dual-source-exports, check:entry-nameability, spec check:docs, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:service-providers, check:merge-driver, check:adr-anchors, check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:docs-redirects, check:docs-audit-scope, check:docs-transcript-drift, check:corpus-claim-drift, check-doc-frontmatter.mjs, check-docs-section-name.mjs, check-empty-changeset.mjs --base origin/main, check:objectui-changeset, check:override-consistency, check:published-files, check:dts-closure, check:changeset-gate-self-tests, check:role-word, check:logger-receiver-detach, check:durability-log-level, check:filter-alias-parity. The doc gates and the ratchet gates were both re-run after the docs commit at 04b60ff7d, all 0. api-surface delta: 3 added, 0 removed or narrowed. LINT NARROWING, declared: repo-wide `pnpm lint` is CI's run. (1) population from eslint's own config, not my judgement: the single root eslint.config.mjs registers no .mdx/.md/.json handling, so the lintable intersection of this diff is exactly its .ts/.mjs paths; (2) file count read from --format json: 11 files linted, 0 errors, 0 warnings, exit 0; (3) invariance for untouched files: that config's own header records, with a positive control, that it 'never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file'. Not run locally, left to CI: the full 55+ gate farm and the dogfood suite. One NOT MEASURED recorded honestly: the first spec build attempt returned the lock wrapper's exit 99 (queue-timeout, never acquired) - not a red, not a pass; re-run to exit 0 with the same OS_VERIFY_LOCK_SLOT.",
"docs_drift_verdicts": {
"method": "Each page read PER FILE at a known clean sha (ff617529c, git status --porcelain empty), never by a content/docs-wide grep; every zero-hit claim came from a command whose positive control fired on the same file and scope. The drift bot's own output was not used as evidence.",
"content/docs/protocol/kernel/realtime-protocol.mdx": "FALSIFIED, FIXED IN THIS PR. Its Connection Endpoint section printed \"enabled\": true for realtime and promised 'discovery will advertise routes.realtime again'. Rewritten: payload now shows enabled:false plus capabilities.websockets, and a new 'What counts as a subscribable channel' section carries the definition and a do-not-key-on-enabled callout. Re-scanned after the edit: all 7 remaining advertis/discovery/enabled:true hits are inside the rewritten block; control 'subscribe' = 51 hits. The rest of the page documents a PLANNED transport and stays true because nothing was mounted.",
"content/docs/kernel/services-checklist.mdx": "FALSIFIED IN THREE PLACES, ALL FIXED. (1) the realtime callout said the slot reports degraded while routes.realtime is 'never advertised'; (2) the closing capability bullet said 'websockets is hardcoded false' - it is now derived, so that sentence is simply wrong; (3) the general rule 'the services map still reports a registered stub as { enabled: true, ... }' now has exactly one exception, since a registered realtime stub reads enabled:false. All three rewritten and the exception declared rather than left implicit. The word 'hardcoded' no longer appears on the page.",
"content/docs/ai/knowledge-rag.mdx": "NULL, measured. One hit (line 76): IRealtimeService events drive KnowledgeService.handleRecordUpsert/Delete. That is in-process publish/subscribe fan-out; this change adds one optional member and alters neither delivery nor how discovery describes in-process delivery. No edit.",
"content/docs/automation/webhooks.mdx": "NULL, measured. Every hit is about IRealtimeService.publish(event) fan-out and InMemoryRealtimeAdapter being single-node and unpersisted (lines 225, 255, 332-335, 358, 440, 459, 610, 630), plus one line contrasting WebSocket fan-out as a different transport (722). All still true: no transport was wired and the adapter is unchanged. No edit.",
"content/docs/kernel/contracts/index.mdx": "NULL, measured. Single hit (line 56), a one-row table describing IRealtimeService as a publish/subscribe event system. The page does not enumerate the interface's members, and an additive optional member does not change that description. No edit.",
"content/docs/plugins/packages.mdx": "NULL, measured. Hits at 276-283 say service-realtime is in-process pub/sub, that there is NO WebSocket/SSE endpoint and no client transport, and that handleUpgrade is deliberately unimplemented. All still exactly true - this change mounts nothing, and the shipped adapter implements neither handleUpgrade nor the new getChannelRoute. No edit.",
"content/docs/releases/v16.mdx": "READ-ONLY, NOT OPENED FOR WRITING. Read-only verdict: NOT falsified, nothing for anyone to fix. Its two hits are historical - line 239 records that schema-only surfaces were labelled 'not yet enforced' in #3197 (those labels are untouched), and line 779 records that WellKnownCapabilitiesSchema gained transactionalBatch in v16. This change adds no capability key; it changes how websockets is COMPUTED, not the vocabulary."
},
"scope_against_ruling": "20 files, all inside ruling A's (a) realtime stays out of open core / (b) discovery stops advertising an unmounted realtime service / (c) one explicit definition. (c): spec/src/api/discovery.zod.ts (the predicate, the slot set, the reader, the field descriptions). (c)+(a): spec/src/contracts/realtime-service.ts (optional getChannelRoute - the seam by which a non-open-core host advertises, which is what lets open core mount nothing). (b)+(c): metadata-protocol/src/protocol.ts and runtime/src/http-dispatcher.ts (both producers derive the entry, routes.realtime and capabilities.websockets from the predicate) and their two pin files. (a): service-realtime/src/realtime-service-plugin.ts (comment only, no behaviour) and service-realtime/src/no-channel-route.pin.test.ts (pins that the shipped occupant names no channel, making (a) a decision rather than an omission). (b): the two existing pins updated from enabled:true to false (objectql/src/protocol-discovery.test.ts, runtime/src/http-dispatcher.test.ts). Test infrastructure for (b): runtime/package.json, runtime/vitest.config.ts, pnpm-lock.yaml - dev-only, to reach the real InMemoryRealtimeAdapter from the runtime pin. Published docs this change falsified: protocol/kernel/realtime-protocol.mdx, kernel/services-checklist.mdx. Generated, never hand-edited (consequence of (c)): references/api/discovery.mdx, references/api/protocol.mdx, spec/api-surface/api.json, spec/export-origins/api.json. Repo requirement: the changeset.",
"mcp_calls": "1 - every GitHub read and write went through repo-scoped REST (probed first: issue read HTTP 200; issue/PR create, label add and read-backs all 200/201). The ONE MCP call is a declared channel switch: REST /search/issues returned HTTP 403, so the pre-file duplicate check for the finding below used a single targeted mcp__github__search_issues. It returned a non-empty result (one unrelated issue, #13285), so the channel is proven live and no separate control term was needed.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #15982: the same capability bullet in content/docs/kernel/services-checklist.mdx also names the retired `features` map spelling (#4828 renamed it to `capabilities`) and the retired `workflow` slot (#4451) - both wrong before this change, so only the clause this change falsified was corrected here. Unassigned, no labels."
],
"notes": "PARKED as instructed: draft, ready NOT flipped, auto-merge NOT armed. Clause-2 re-declared from the diff as YES, which agrees with the ruling comment 5542632689's own declaration (the claim comment's 'Clause-2: no' was explicitly this-seat's-reading): the published /discovery payload changes what it says for realtime, and @objectstack/spec gains three exported members plus one optional interface member on a published contract face - additive in shape, changed in value. needs:contract-review is on PR #15978 and on this card; both label writes were read back and both are present. One platform write to note rather than correct: the PATCH that added the docs verdicts left TWO attribution footers on the PR body - my session-URL footer survived verbatim and the platform appended a bare one under it. A full byte diff of the read-back shows that append is the ONLY difference; no content was mutated. Not repaired, per the no-re-append rule."
}
Correction: this seat's claim comment declared Clause-②: no. That was wrong. The round's re-declaration of yes is correct, and this PR PARKS.
The claim comment's tier call was this seat's reading, and the implementing round re-derived it from the diff instead of inheriting it. It was right to. Measured on the branch against origin/main:
Three members added to the published api-surface, none removed or narrowed, plus one optional member on a published interface (IRealtimeService.getChannelRoute). Additive widening of a published contract face is clause-② by the settled boundary — the 2026-08-28 ruling excludes runtime permission/security behaviour from clause ②, but says 「条款②只指已发布契约面」, and this diff changes that face rather than only behaviour behind it. So yes.
The changeset is minor for @objectstack/spec, @objectstack/metadata-protocol and @objectstack/runtime — correct, and the rule that decides it is the act, not the commit type: additive widening of a published surface is at least minor regardless of a fix(...)-shaped title.
Carriers verified rather than taken on report: check-clause2-carriers --pair 15978 exits 0, and needs:contract-review is present on both this card and PR #15978. The PR is draft, auto_merge is null, and it is not in the merge queue.
Consequence, stated plainly. A Clause-②: no from this seat would have routed a published-surface widening to land with no contract review at all. The park exists because the round re-measured instead of trusting the dispatch brief. That is the second tier call this seat got wrong today in the opposite direction — on #14552 the brief wrongly predicted a park that measurement retired — and both were caught the same way, by a round treating the PM's framing as falsifiable rather than binding.
Status: PARKED, and it stays parked. The CONTRACT_REVIEW_TIER needed for a clause-② review has been rate-limited since roughly 15:10 today; real probe tasks at 15:55 and 16:44 both returned HTTP 429. The quota exemption covers dispatch only and has never covered contract review, so this does not get waved through on a green CI. ⛔ Do not flip ready and ⛔ do not arm this PR until an at-tier review passes it.
{
"issue": 14646,
"status": "done",
"branch": "claude/issue-14646-discovery-stops-advertising-unmounted-realtime",
"pr": "https://github.com/objectstack-ai/objectstack/pull/15978",
"head": "3c22dddf2",
"premise_still_valid": true,
"summary": "Unchanged from the first report: the handler is `discoveryHandler` in packages/rest/src/rest-server.ts, serving GET /api/v1/discovery via ObjectStackProtocolImplementation.getDiscovery() (packages/metadata-protocol/src/protocol.ts); the second producer is HttpDispatcher.getDiscoveryInfo() (packages/runtime/src/http-dispatcher.ts). The definition - 'A subscribable channel exists only where discovery reports handlerReady: true together with a connectable route; enabled never means \"there is a channel\"' - is isSubscribableChannel() in @objectstack/spec/api, enforced by being the computation both producers use for services.realtime.enabled and capabilities.websockets. This round repaired the two CI failures on 04b60ff7d and recorded, at both code sites and in the changeset, what the second repair cost.",
"ci_repairs": {
"1_dogfood_census": "authz-probe-blind-spot.test.ts. Cause: the comment added to realtime-service-plugin.ts moved a pinned EXACT control count - the census pattern is a bare /RealtimeService/g, so it matches inside `IRealtimeService` and PROSE about the symbol moves the symbol's count exactly as code does. Reproduced locally first (exit 1, 1 failed / 32 passed), and the count verified independently: 10 at merge base d4c2cb196, 11 at HEAD, the single new occurrence being `IRealtimeService.getChannelRoute()` in my comment. Remedy taken: RE-MEASURED THE LEDGER 10 -> 11, which the census's own header says is what the pin is for ('re-read by whoever moved it instead of aging into a stale claim'). The comment was NOT reworded to push the counter back. Green after: exit 0, 33 passed. The row now carries a note naming the phenomenon for the next person who documents that file.",
"2_type_source_resolution": "check-type-source-resolution. WHICH CASE I MEASURED: the ALREADY-COUNTED-PROGRAM case, not onboarding. Measured against origin/main rather than recalled - packages/runtime's `typecheck` script names tsconfig.test.json byte-identically before and after this diff (`tsc --noEmit && pnpm check:test-typecheck` -> `... --package packages/runtime --project tsconfig.test.json`), and the diff added one devDependency line and no script. So the (via tsconfig.test.json) program was already counted, which the registry doc-block calls 'the exposure this ratchet exists to catch'; the re-baseline limb was closed to me and widening the shrink-only entry was refused. WHICH REMEDY I TOOK: NEITHER - I removed the dist-resolved import entirely. `paths` was tried first, as the gate instructs, and measured: it cleared check-type-source-resolution (exit 0, registry unwidened) and then made `pnpm --filter @objectstack/runtime typecheck` exit 1 with 13 TS6059 from @objectstack/service-realtime's file graph billed into packages/runtime's test-typecheck ledger - the PR #12570 shape, a ledger that package cannot see - because the mandated rule pulls another package's source into a program whose rootDir is ./src, which tsconfig.test.json's own header states it will not widen. That ledger's only other remedy is MAINTAINER-ONLY. All 13 were TS6059 and none was a code error. So the devDependency, the vitest alias and the lockfile change were reverted to the merge base (verified byte-identical to d4c2cb196; `pnpm install --frozen-lockfile` exit 0 proves the restored lockfile is consistent), and the runtime pin declares its occupant locally. Green after: runtime typecheck exit 0, runtime suites 274 passed, check-type-source-resolution exit 0."
},
"pin_strength": {
"1_real_or_stand_in": "A STAND-IN, in those words. The runtime producer pin no longer drives the shipped InMemoryRealtimeAdapter.",
"2_what_it_stopped_proving": "Named assertion: 'does NOT advertise an in-process realtime bus as a channel' (the first case in packages/runtime/src/discovery-realtime-channel.pin.test.ts) no longer says anything about the SHIPPED occupant - only that the producer derives its answer from whatever occupant it is given. Measured, one mutation and two pins: giving InMemoryRealtimeAdapter a getChannelRoute() returning '/api/v1/realtime' (mutation proved on disk - marker 0 -> 1, blob 02c22cf22 -> ca40ae771; restore proved by blob equality and an empty git diff HEAD; trap on absolute paths) leaves the runtime pin GREEN on all 4 cases. Before the revert that same mutation would have reddened it. That is the whole of the loss.",
"3_is_it_recovered": "YES, and measured rather than asserted - by the same mutation. packages/services/service-realtime/src/no-channel-route.pin.test.ts goes RED under it: exit 1, 2 of its 3 cases fail ('names no channel route', 'is therefore not a subscribable channel in discovery terms'). That pin drives the real class through a relative in-package import, so it costs no cross-package type edge. The two pins divide the reading - that one pins WHAT THE SHIPPED OCCUPANT NAMES, the runtime one pins THAT THE PRODUCER DERIVES FROM WHATEVER AN OCCUPANT NAMES - and neither can go green by accident of the other, so the runtime side only ever needed the derivation. Scope of the equivalence claim, measured not assumed: the producers perform exactly two reads on an occupant (readChannelRoute, readServiceSelfInfo); both return undefined for the real adapter AND for the stand-in today. That is a statement about today, which is exactly why the shipped-occupant claim lives in the other package rather than here. Recorded where PM asked, not only in this report: the docblock of the runtime pin now states the loss and the mutation result verbatim, and the changeset carries a testing note saying the producer pins use a stand-in and why."
},
"tests": "All at head 3c22dddf2 unless stated; exit codes captured by redirecting first, never after a pipe. REPRO BEFORE FIX: dogfood census exit 1 (1 failed / 32 passed), assertion 'control \"RealtimeService\" moved ... expected 11 to be 10'. AFTER: exit 0, 33 passed. check-type-source-resolution --self-test exit 0; check-type-source-resolution exit 0 (125 tsc programs across 78 packages; 61 registered - registry NOT widened). pnpm --filter @objectstack/runtime typecheck exit 0 (was exit 1 with 13 TS6059 under the `paths` attempt). runtime pin + http-dispatcher + discovery-schema-conformance exit 0, 274 passed. service-realtime full package exit 0, 31 passed. check:cross-package-test-inputs, check:test-source-alias, check:nul-bytes, spec check:generated, spec check:api-surface, check:doc-anchors, check:docs-single-h1, check-empty-changeset --base origin/main - all exit 0 at the final head. TEETH EXPERIMENT (the answer to 'is the coverage recovered'): one mutation of the shipped adapter, run under the shared verify lock, with an EXIT/INT/TERM trap on absolute paths. Mutation proved on disk (getChannelRoute occurrences 0 -> 1; blob changed). service-realtime pin exit 1, 2 failed / 1 passed. runtime pin exit 0, 4 passed. Restore proved: blob back to 02c22cf22c7fbf15805b6fc323ab760fed7d7e03 and git diff HEAD over the file empty. No rebuild needed for either leg: both pins reach their subject through in-package source or a src-aliased spec. Earlier-round evidence unchanged and still valid at this head for the parts it covered: metadata-protocol full package 2391 passed / 10 skipped, objectql full package 4714 passed, runtime full package 3295 passed, rest's five discovery-* suites 40 passed, the red-then-green ablation of both producers.",
"dispatch_gates_process_note": "MEASURED, and it corrects the hypothesis in the brief: the stale-tree warning and the missed gate are NOT connected. `check:type-source-resolution` WAS in the first derived family - it is on line 58 of that same run's output, matched via gate source 'packages/*' through packages/runtime/package.json among others. Independently, the staleness warning named its own affected families (check-role-word.mjs, bare-root-worklist.mjs, ensure-pm-labels.sh) and this gate is not one of them. So dispatch-gates.mjs did its job and no card is warranted for it; the miss is MINE - I ran a hand-picked subset of the 53 derived gates (the glob 'packages/*' makes that derivation nearly the whole farm, so a subset was unavoidable) and my selection rule was wrong: it did not include a gate whose subject - the type resolution of a NEW cross-package dependency edge - was something my diff newly CREATED. That is the rule I would carry forward: from the derived list, always run the gates whose subject your diff newly creates, whatever else you skip.",
"mcp_calls": "1 for the whole run, unchanged - no MCP call this round. Every GitHub read and write went through repo-scoped REST. The one MCP call was the declared channel switch for the pre-file duplicate check (REST /search/issues returns 403), and it returned a non-empty result so the channel was proven live.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #15982: the capability bullet in content/docs/kernel/services-checklist.mdx also names the retired `features` map spelling (#4828) and the retired `workflow` slot (#4451) - both wrong before this change. Unassigned, no labels.",
"NOT filed, reported for your decision: after a package's test layer has been onboarded and re-baselined, a NEW cross-package source dep in its test program has no self-serve remedy in a package whose rootDir is ./src - the registry refuses the widening, `paths` produces TS6059 billed to a ledger the other package cannot see, and both the ledger expansion and the rootDir widening are closed to a dev seat. I judged this working-as-designed (the correct outcome, not taking the dep, is what the ratchets steered me to) rather than a defect, so I did not file a card. File one if you read it the other way."
],
"notes": "PARKED and untouched: draft, ready NOT flipped, auto-merge NOT armed, needs:contract-review on PR #15978 and card #14646. Clause-2 stays YES. The PR body was updated to correct its own now-false claim that the runtime pin drives the real adapter, and to carry the two-failure reading; the platform again appended a second bare attribution footer on that PATCH - a full byte diff of the read-back shows the append is the ONLY difference, and it is left alone per the no-re-append rule."
}
{
"issue": 14646,
"status": "done",
"branch": "claude/issue-14646-discovery-stops-advertising-unmounted-realtime",
"pr": "https://github.com/objectstack-ai/objectstack/pull/15978",
"head": "54fba060e",
"premise_still_valid": true,
"summary": "This round: merged origin/main (f50c394da, #16002) into the branch, validated the merged tree, pushed. The card's implementation is unchanged - isSubscribableChannel() in @objectstack/spec/api is the one definition, enforced by being the computation both discovery producers use for services.realtime.enabled and capabilities.websockets. No conflicts, nothing regenerated, nothing to escalate.",
"merge": {
"how": "git fetch origin main, then `bash scripts/pm/os-regen-merge.sh` - the repo's own sequence - exit 0. MERGED, never rebased, amended or force-pushed. Merge commit 54fba060e; origin/main was exactly f50c394da, confirmed an ancestor.",
"conflicts": "NONE, and the reason is measured rather than lucky: main touched NOT ONE file this branch edits - checked file by file over discovery.zod.ts, realtime-service.ts, protocol.ts, http-dispatcher.ts, realtime-service-plugin.ts, the census, and both hand-edited docs pages: all 'main-moved: NO'. The generated shards are disjoint too: main moved api-surface/automation.json, export-origins/automation.json and five other reference pages; this branch moved api-surface/api.json, export-origins/api.json and references/api/{discovery,protocol}.mdx. os-regen-merge step 2 therefore KEPT the branch's bytes on all four (main did not move them, so taking main's side would have been a pure revert), and step 3 recorded no additional changes. No os-regen deferral was written to $GIT_DIR/os-regen-pending. Nothing was hand-edited in any generated artifact.",
"both_sides_survive": "Asserted in both directions, per os-regen-merge step 4.3: main's three new entries (EndConfig, EndConfigParsed, EndConfigSchema) are present in api-surface/automation.json, and this branch's three (CHANNEL_SURFACE_SLOTS, isSubscribableChannel, readChannelRoute) are present in api-surface/api.json.",
"regeneration": "NOT NEEDED, and proven rather than assumed: the spec build closure was rebuilt on the merged source first (AGENTS.md sec.9/sec.10 - only a rebuild of merged source can validate a generated artifact), then `pnpm --filter @objectstack/spec run check:generated` exit 0 - zero of 15 artifacts stale. So there is no regeneration commit; the merge commit is the whole of this round's diff. Branch delta vs origin/main is unchanged: exactly 3 added / 0 removed in both api-surface/api.json and export-origins/api.json, 18 files / 784 insertions / 67 deletions - identical to pre-merge.",
"the_fix_verified_by_content": "f50c394da checked by reading it, not the API: scripts/check-regen-pending.mjs now reads the root package.json's packageManager (line 951) and writes it into the fixture (959), with the self-test case 'the fixture pins the ROOT's packageManager, so Corepack cannot resolve latest' (1042-1045); fixtureSelfTest appears 5x. Root packageManager is pnpm@10.31.0+sha512..."
},
"tests": "All on the merged tree at 54fba060e; exit codes captured by redirecting first, never after a pipe. GREEN: @objectstack/spec test exit 0 (12838 passed, 478 files); @objectstack/metadata-protocol test exit 0 (2391 passed, 10 skipped, 166 files); @objectstack/runtime test exit 0 (3295 passed, 232 files); @objectstack/service-realtime test exit 0 (31 passed); the dogfood census pin test/authz-probe-blind-spot.test.ts exit 0 (33 passed); objectql src/protocol-discovery.test.ts exit 0 (32 passed); runtime typecheck exit 0; spec typecheck exit 0. GATES, each exit captured: check:merge-driver 0 (this is the gate that was red - it runs check-regen-pending's self-test); check-regen-pending.mjs --self-test 0 and its main run 0; check-type-source-resolution --self-test 0 and main 0 (125 programs / 78 packages, registry unwidened); spec check:generated 0; check:cross-package-test-inputs 0; check:test-source-alias 0; check:service-providers 0; check:nul-bytes 0; check:doc-anchors 0; check:doc-authoring 0; check:docs-single-h1 0; check:docs-redirects 0; check:docs-audit-scope 0; check:corpus-claim-drift 0; check:docs-transcript-drift 0; check-doc-frontmatter 0; check-empty-changeset --base origin/main 0; check:objectui-changeset 0. THREE READINGS RECORDED AS **NOT MEASURED**, none of them a red: (1) two lock acquisitions returned the wrapper's exit 99 (queue-timeout, never acquired) - the box was heavily contended; the slot OS_VERIFY_LOCK_SLOT=issue-14646-dev kept the place and both were re-run to completion. (2) `pnpm check:regen-pending` returned 254 - no such script; the real entry point is `check:merge-driver`, and the script itself was then run directly. (3) metadata-protocol's first run showed 162 test FILES failed with only 40 tests executed - diagnosed as an unbuilt dependency closure in the fresh worktree ('Failed to resolve entry for package @objectstack/core' etc.), not assertion failures; after `pnpm --filter '@objectstack/runtime^...' build` it is the 2391-pass run above. Same class: check:docs-transcript-drift returned its own exit 3, whose text says 'PREREQUISITE NOT MET ... this is NOT a pass and NOT a finding: nothing was measured' - @objectstack/lint was unbuilt; after building it, 0.",
"mcp_calls": "1 for the whole card, unchanged - no MCP call this round. Every GitHub read and write went through repo-scoped REST.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #15982 (earlier round): the capability bullet in content/docs/kernel/services-checklist.mdx also names the retired `features` map spelling (#4828) and the retired `workflow` slot (#4451). Unassigned, no labels. Unchanged by this merge - main did not touch that page.",
"NOT filed, still open for your call (earlier round): after a package's test layer is onboarded and re-baselined, a NEW cross-package source dep in its test program has no self-serve remedy in a package whose rootDir is ./src. I judged it working-as-designed."
],
"notes": "PARK UNCHANGED AND UNTOUCHED: still draft, ready NOT flipped, auto-merge NOT armed, needs:contract-review on PR #15978 and card #14646, Clause-2 still YES. I did not re-run any CI job, disabled or quarantined nothing, made no empty commit, and did not widen the PR - this round's entire diff is the merge commit. PR body updated to record the merge and the re-run figures; the platform again appended a second bare attribution footer on that PATCH, left alone per the no-re-append rule. Nothing came back red that was not check:merge-driver's own subject, so there was nothing to escalate before acting."
}
Contract review (clause ②) — PASS — PR #15978 at head 54fba060
Director seat, summon #15, session_01TezFG8ZMrNH6n5VTNpPpdH (os-zhuang), 2026-09-05T22:4xZ, on the maintainer's instruction 「有很多pr等着契约复审」. Tier fuse: get_session this session reads session_context.model = last_served_model = CONTRACT_REVIEW_TIER. Readings from the PR diff, origin/main and the sibling repos; dev reports (5553790377, 5554621496) and the seat's correction (5553510620) read afterwards as cross-check.
(c) one explicit definition of "subscribable channel"
discovery.zod.ts: isSubscribableChannel(info) = handlerReady === true && typeof route === 'string' && route.length > 0; CHANNEL_SURFACE_SLOTS = {'realtime'}; readChannelRoute(svc) reads an occupant's optional getChannelRoute(). Computed in one place; enabled / websockets / routes.realtime describes rewritten to say what they do and do not mean.
correct
2
(b) /discovery stops advertising an unmounted realtime service
Both producers (metadata-protocolgetDiscovery(), runtimegetDiscoveryInfo()) import the predicate and deriveservices.realtime.enabled, capabilities.websockets and routes.realtime from it, replacing a literal false and a hardcoded undefined. On a stock boot the shipped InMemoryRealtimeAdapter implements no getChannelRoute ⇒ enabled: false, no route — retraction computed, not hardcoded. Pins: two-direction (an in-process bus is NOT advertised; a declared occupant with a route IS), red-first ablation 3/2 and 3/1 on the two producer suites.
correct
3
(a) realtime stays out of open core — no transport added
File-by-file table checked against the diff: no route, handler, client primitive or transport; service-realtime change is a comment plus a no-channel-route.pin.test.ts that pins the occupant names no channel (makes (a) a decision, not an omission).
correct
4
Accept set / public face
api-surface/api.json+3 (CHANNEL_SURFACE_SLOTS, isSubscribableChannel, readChannelRoute), 0 removed / narrowed; one optional member IRealtimeService.getChannelRoute?() on a published interface (additive; existing implementers keep compiling). The wire payload changes one value (realtime.enabled true → false on stock boots) and stops emitting routes.realtime when unmounted — both the ruled behaviour. Consumers: objectui / cloud origin/main have zero reads of services.realtime.enabled, capabilities.websockets or routes.realtime outside tests (this seat's grep); the one known consumer, the objectui bell (#7249), already polls.
Predicate applied only to CHANNEL_SURFACE_SLOTS; in-process contracts keep enabled: true with no route. Avoids the over-retraction a global "enabled means route" would have caused.
correct
6
Docs
realtime-protocol.mdx and services-checklist.mdx were falsified and are fixed; four other pages read per file and judged null; content/docs/releases/v16.mdx correctly left read-only; content/docs/references/** regenerated. Residual pre-existing errors filed as #15982, not folded in.
correct
7
CI repairs on the branch
dogfood census exact control count moved by the added comment (/RealtimeService/g matches prose) — re-measured, not fudged; check-type-source-resolution measured as the already-counted-program case. Both explained with the measurement.
accepted
② semver
@objectstack/spec, @objectstack/metadata-protocol, @objectstack/runtimeminor, with the client note the ruling asked for ("a client that keyed on enabled: true to subscribe was subscribing to nothing; it now sees false"). Additive widening + a value change on a published payload ⇒ minor is right; the fix(...) title does not lower it.
③ Boundary flags
Claim comment's Clause-②: no corrected by the seat itself (5553510620) after the round measured +3 api-surface members. Concur with the correction.
Testing limit declared in the changeset: producer pins drive a declared stand-in, not the shipped adapter (runtime cannot take a source-level dependency on service-realtime); the no-channel-route.pin.test.ts in service-realtime closes that half from the other side. Accepted.
objectui#7249 cross-linked per the ruling's execution note (verify on that card at its next pass; not a blocker here).
Evidence and landing
CI on 54fba060: 35 success / 2 skipped / 0 red; mergeable_state: clean; check-governed-merges --test 0 of 18 paths. os-regen-merge.sh drove the main merge (exit 0, nothing deferred). Closes #14646 correct.
Clearing, same stroke:needs:contract-review off this card and PR #15978 with provenance; then ready + auto-merge (SQUASH).
Activity
os-zhuang commentedon Sep 4, 2026
Triage:
needs-user-decision·domain:services·priority:p2. The card's item 1 is a product-capability call, which is on the manual floor.Triage seat, session
session_01SwJQDFKe8tVit3BXQ9EfR5, R+145, 2026-09-04T15:35Z. Routeddomain:servicesbecause both branches land there (RealtimeServicePlugin, the discovery payload) with a@objectstack/clientlimb under B.Why it is escalated rather than adjudicated: item 1 asks whether the open framework ships a realtime transport at all, or whether that stays cloud/enterprise. That is a product capability boundary — 功能新增类 and a tiering statement about what open core promises. Neither is a triage call, whatever the four facets say.
⛔ The card cannot be split to unblock the mechanical half. The honest-predicate repair (item 3) looks independent and is not: under A discovery becomes truthful by retracting the claim, under B by making it true. Same defect, opposite edits. So the decision gates it.
The fork
A — realtime stays out of open core. Discovery stops advertising a service with no mounted surface as
enabled: true; the contract is restored by retraction. The console keeps polling (10 s foreground, 60 s hidden), which works today. Zero new capability.B — open core mounts a realtime surface (WS or SSE), advertises the route per ADR-0076 D12, and exposes a subscribe primitive in
@objectstack/clientso the bell can subscribe with a polling fallback./discovery两头都违反:enabled: true、handlerReady: false、routes里没有 realtime 条目。⇒ 契约必须恢复,这一点与能力取舍无关,两条路都得做,只是方向相反。至于能力本身:实时传输在开源核里是一个大而永久的面 —— 连接生命周期、socket 上的鉴权、背压、水平扩展的会话归属,每一条落地后都绑住所有自托管部署。①指向 A:把不变量收回来,面保持小,把传输留给有运维模型的那一层。⛔ 这不是说实时没价值,是说它的成本主要在运维语义而不在代码。enabled: true不是「存在一条可订阅通道」的谓词。一个客户端(人或 AI)照它去订阅,会订到空,然后静默地丢掉铃铛 —— 没有报错、没有红、没有任何信号。诚实的谓词是handlerReady: true且有可连的 route。⇒ 无论 A 还是 B,discovery 都必须把这个形状变得不可误读。这是「声明即强制」在发现面上的直接实例。推荐:A。 ①以≥50% 权重领起并指向 A;②零实测拉动,按分歧推荐序取不扩散;④同向;③是两路共付的地板,不改方向。落地形态:让 discovery 对 realtime 说真话,并把「什么算一条可订阅通道」写成一处显式定义。
回退:B —— 若维护者判断实时是开源核对外承诺的一部分(而不是云层能力),则 B 成立,且 ADR-0076 D12 要求路由与订阅原语同批落地,⛔ 不可只挂不广告或只广告不挂。
置信缺口(本分析看不见什么): ⛔ 未测量云/企业层今天是否已经挂载了实时传输。若已挂载,A 就不是缺口而是一句干净的分层声明,推荐更强;若两层都没有,A 等于宣布这个平台目前没有实时通道,那是维护者该知情后再拍的。⛔ 也未测量除铃铛外是否还有别的消费者在等。这两项都只影响紧急度与措辞,⛔ 不翻转 ① 与 ④。
卡片的读数取自 2026-09-03 的 showcase 启动。⛔ 未在今天的
origin/main上复现/discovery的输出,也未核RealtimeServicePlugin是否仍在 48 个插件之列。裁决落地时由执行席重测,⛔ 不继承这两个数字。Generated by Claude Code
os-warren commentedon Sep 4, 2026
Maintainer ruling recorded — A: realtime stays out of open core;
/discoverystops advertising a realtime service that has no mounted surface, and "what counts as a subscribable channel" becomes one explicit definitionDirector seat, summon #14, session
session_01LsEjuNMPitCHwEfYftZ1om(GitHubos-warren), 2026-09-04. Provenance: maintainer, live PM chat, decision batch #38 (item 4, presented with the recommendation A, fallback B), verbatim reply 「同意」. Premise: triage facets 5542203078 (ADR-0076 D12 — advertise only what is mounted, mount what is advertised — violated on both ends:enabled: true,handlerReady: false, no realtime route; one consumer, objectui#7249, whose polling fallback works today).Ruled: A. The open framework does not mount a realtime transport in this ruling. Discovery is made truthful by retraction:
RealtimeServicePlugin(or whatever registers therealtimediscovery entry) advertisesenabled: falsewhen no transport surface is mounted, and the honest predicate —handlerReady: trueand a connectable route — is written down once, where discovery consumers can read it, soenabled: truecan no longer be read as "there is a channel". The console keeps polling. Not taken: B (mount WS/SSE + advertise the route + a@objectstack/clientsubscribe primitive) — the fallback if the maintainer had judged realtime part of open core's promise; if it is ever taken, ADR-0076 D12 requires route and primitive in the same change.Why (① ≥50%): a realtime transport is a large, permanent operational surface (connection lifecycle, socket auth, backpressure, session affinity under horizontal scale) that binds every self-hosted deployment; restoring the D12 invariant by retraction keeps the surface small and leaves transport to the layer with an operations model. ② is one consumer with a working fallback and zero reports; ④ points the same way, consistent with the 2026-09-03 「我们是一个开发工具」 ruling (#14824). ③ is paid under either option: the discovery shape must be unmisreadable.
Execution:
domain:serviceslane, S. Re-measure/discoveryon today'sorigin/mainfirst (triage did not reproduce the 2026-09-03 showcase reading); pin the retraction (realtime entryenabled: falsewith no mounted surface; the predicate definition under test); update the discovery docs line.Clause-②: yes(the published discovery payload changes what it says forrealtime) ⇒needs:contract-reviewon the PR. Changeset: the package that owns the discovery entry, minor with a note (a client that keyed onenabled: trueto subscribe was subscribing to nothing; it now seesfalse). Cross-link objectui#7249 so the bell card knows the answer is "poll".State transition, same stroke:
needs-user-decision→pm:queue.priority:p2·domain:servicesunchanged. Ledger: director seat post #12708, batch #38.Generated by Claude Code
os-warren commentedon Sep 4, 2026
Correction to the ruling comment above (5542632689): the director session id is
session_01LsEjuNMPitCHwEfYftZ1um(typo in the last three characters there). The ruling, provenance and state transition are unchanged.Generated by Claude Code
docs(qa): classify the five UNCLASSIFIED capability ledgers — four RE…
claude commentedon Sep 5, 2026
Claimed and dispatched —
pm:queue→pm:dispatched.Claim:
domain:servicesexecution seat, sessionsession_01ARYe3yQTQCUFm5qPYNgKaJBranch:
claude/issue-14646-discovery-stops-advertising-unmounted-realtimeClause-②: no
/discovery's response shape is a wire contract. Changing a value it reports is a behaviour change (no); changing its shape — a new key, a new accepted value in an enum — is a published contract face ⇒yes,needs:contract-reviewon PR and card, andCONTRACT_REVIEW_TIERis quota-exhausted, so such a PR parks.The ruling — settled, ⛔ not reopenable
Maintainer, director summon #14, recorded at
5542632689(and its own typo correction at5542640476— the ruling is unchanged): A./discoverystops advertising a realtime service that has no mounted surface;enabled: trueand 「no HTTP/WS surface is mounted」 are both true at once, which means the field had no single meaning.What the card measured
On a stock showcase boot,
GET /api/v1/discoveryreports realtime asenabled: truebutstatus: "degraded",handlerReady: false, message 「In-process event bus only — no HTTP/WS realtime surface is mounted」, and theroutesmap carries no realtime entry — whileRealtimeServicePluginis among the 48 loaded plugins. ⇒ a console client has nothing to subscribe to, and the inbox bell / approvals badge stay on polling.handlerReadyreturned only CHANGELOGs and unrelatedpackages/core/src/fallbacks/*. ⇒ finding it is the round's first step, and ⛔ this seat hands over no path, rather than a guessed one.In-flight check
No open PR of the 33 touches a path matching
discovery. Re-check against the real file set once you have located it.Generated by Claude Code
wip(#14646): one definition of a subscribable channel, wired into bot…
test(#14646): pin both directions of the subscribable-channel definit…
4 remaining items
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 14646, "status": "done", "branch": "claude/issue-14646-discovery-stops-advertising-unmounted-realtime", "pr": "https://github.com/objectstack-ai/objectstack/pull/15978", "premise_still_valid": true, "summary": "A1 answered: the handler is `discoveryHandler` in packages/rest/src/rest-server.ts (registerDiscoveryEndpoints), serving GET /api/v1/discovery by calling ObjectStackProtocolImplementation.getDiscovery() in packages/metadata-protocol/src/protocol.ts; the second producer is HttpDispatcher.getDiscoveryInfo() in packages/runtime/src/http-dispatcher.ts. The card's premise reproduced on today's origin/main: the ablation leg (both producers restored to merge base d4c2cb196) reports services.realtime.enabled true beside 'no HTTP/WS realtime surface is mounted'. Clause 3 is the deliverable: 'A subscribable channel exists only where discovery reports handlerReady: true together with a connectable route; enabled never means \"there is a channel\".' That sentence lives in isSubscribableChannel() in @objectstack/spec/api and is ENFORCED by being the computation both producers use to set services.realtime.enabled and capabilities.websockets (previously a literal false in each producer). Applied per slot via CHANNEL_SURFACE_SLOTS (realtime alone), so cache/queue/job keep their honest enabled:true with no route. The route comes from the occupant (new optional IRealtimeService.getChannelRoute), because no open-core producer mounts a realtime transport; @objectstack/service-realtime names none, which is the retraction, computed rather than hardcoded. No realtime surface, route, handler or client primitive is implemented.", "tests": "Suites and typechecks at ff617529c; ratchet-class gates re-run at the final head 04b60ff7d after the docs commit. Exit codes captured by redirecting first, never after a pipe. RED FIRST (ablation): fix committed first, then both producer files restored to merge base d4c2cb196 with an EXIT/INT/TERM restore trap on absolute paths. Mutation proved on disk by marker counts (isSubscribableChannel 5 -> 0 in protocol.ts, 4 -> 0 in http-dispatcher.ts) and by on-disk blob hashes equalling the base blobs. No rebuild needed for either leg: both pin files import their producer from local src, and spec was deliberately left at HEAD so the ablation isolates the producers rather than the definition the pins import. RED: metadata-protocol/src/discovery-realtime-channel.pin.test.ts exit 1, 3 failed / 2 passed - 'does NOT advertise an in-process realtime bus as a channel' (expected true to be false), 'DOES advertise a realtime occupant that mounts a channel' (expected 'degraded' to be 'available'), 'answers enabled, routes.realtime and capabilities.websockets with the SAME predicate' (services.realtime.enabled: expected true to be false). runtime/src/discovery-realtime-channel.pin.test.ts exit 1, 3 failed / 1 passed, same three assertions. RESTORE: git checkout HEAD -- both paths; post-restore blob hashes equal the HEAD blob hashes and git diff HEAD over the two files is empty. GREEN: metadata-protocol full package test exit 0 (2391 passed, 10 skipped, 166 files); objectql full package test exit 0 (4714 passed, 273 files); runtime full package test exit 0 (3295 passed, 232 files); service-realtime full package test exit 0 (31 passed); rest's five discovery-*.test.ts against a rebuilt metadata-protocol dist exit 0 (40 passed). The runtime negative pin drives the REAL InMemoryRealtimeAdapter that RealtimeServicePlugin registers (devDependency plus a src alias, the idiom runtime already uses for service-job / service-package), not a stand-in. typecheck exit 0 for spec, metadata-protocol, runtime, objectql, service-realtime - runtime was red once on two TS2339 in the new pin file (the dispatcher's realtime entry inferred as a union without `route`), fixed by annotating it ServiceInfo, then 0. Gate family derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` from the real change set (not a hand list); it warned the tree is 5 commits behind origin/main on three files unrelated to this diff. Green, each exit captured: spec check:generated (0 after regenerating api-surface/, export-origins/, content/docs/references/**), check:api-surface, check:authorable-surface, check:export-origins, check:exported-any, check:dual-source-exports, check:entry-nameability, spec check:docs, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:service-providers, check:merge-driver, check:adr-anchors, check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:docs-redirects, check:docs-audit-scope, check:docs-transcript-drift, check:corpus-claim-drift, check-doc-frontmatter.mjs, check-docs-section-name.mjs, check-empty-changeset.mjs --base origin/main, check:objectui-changeset, check:override-consistency, check:published-files, check:dts-closure, check:changeset-gate-self-tests, check:role-word, check:logger-receiver-detach, check:durability-log-level, check:filter-alias-parity. The doc gates and the ratchet gates were both re-run after the docs commit at 04b60ff7d, all 0. api-surface delta: 3 added, 0 removed or narrowed. LINT NARROWING, declared: repo-wide `pnpm lint` is CI's run. (1) population from eslint's own config, not my judgement: the single root eslint.config.mjs registers no .mdx/.md/.json handling, so the lintable intersection of this diff is exactly its .ts/.mjs paths; (2) file count read from --format json: 11 files linted, 0 errors, 0 warnings, exit 0; (3) invariance for untouched files: that config's own header records, with a positive control, that it 'never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file'. Not run locally, left to CI: the full 55+ gate farm and the dogfood suite. One NOT MEASURED recorded honestly: the first spec build attempt returned the lock wrapper's exit 99 (queue-timeout, never acquired) - not a red, not a pass; re-run to exit 0 with the same OS_VERIFY_LOCK_SLOT.", "docs_drift_verdicts": { "method": "Each page read PER FILE at a known clean sha (ff617529c, git status --porcelain empty), never by a content/docs-wide grep; every zero-hit claim came from a command whose positive control fired on the same file and scope. The drift bot's own output was not used as evidence.", "content/docs/protocol/kernel/realtime-protocol.mdx": "FALSIFIED, FIXED IN THIS PR. Its Connection Endpoint section printed \"enabled\": true for realtime and promised 'discovery will advertise routes.realtime again'. Rewritten: payload now shows enabled:false plus capabilities.websockets, and a new 'What counts as a subscribable channel' section carries the definition and a do-not-key-on-enabled callout. Re-scanned after the edit: all 7 remaining advertis/discovery/enabled:true hits are inside the rewritten block; control 'subscribe' = 51 hits. The rest of the page documents a PLANNED transport and stays true because nothing was mounted.", "content/docs/kernel/services-checklist.mdx": "FALSIFIED IN THREE PLACES, ALL FIXED. (1) the realtime callout said the slot reports degraded while routes.realtime is 'never advertised'; (2) the closing capability bullet said 'websockets is hardcoded false' - it is now derived, so that sentence is simply wrong; (3) the general rule 'the services map still reports a registered stub as { enabled: true, ... }' now has exactly one exception, since a registered realtime stub reads enabled:false. All three rewritten and the exception declared rather than left implicit. The word 'hardcoded' no longer appears on the page.", "content/docs/ai/knowledge-rag.mdx": "NULL, measured. One hit (line 76): IRealtimeService events drive KnowledgeService.handleRecordUpsert/Delete. That is in-process publish/subscribe fan-out; this change adds one optional member and alters neither delivery nor how discovery describes in-process delivery. No edit.", "content/docs/automation/webhooks.mdx": "NULL, measured. Every hit is about IRealtimeService.publish(event) fan-out and InMemoryRealtimeAdapter being single-node and unpersisted (lines 225, 255, 332-335, 358, 440, 459, 610, 630), plus one line contrasting WebSocket fan-out as a different transport (722). All still true: no transport was wired and the adapter is unchanged. No edit.", "content/docs/kernel/contracts/index.mdx": "NULL, measured. Single hit (line 56), a one-row table describing IRealtimeService as a publish/subscribe event system. The page does not enumerate the interface's members, and an additive optional member does not change that description. No edit.", "content/docs/plugins/packages.mdx": "NULL, measured. Hits at 276-283 say service-realtime is in-process pub/sub, that there is NO WebSocket/SSE endpoint and no client transport, and that handleUpgrade is deliberately unimplemented. All still exactly true - this change mounts nothing, and the shipped adapter implements neither handleUpgrade nor the new getChannelRoute. No edit.", "content/docs/releases/v16.mdx": "READ-ONLY, NOT OPENED FOR WRITING. Read-only verdict: NOT falsified, nothing for anyone to fix. Its two hits are historical - line 239 records that schema-only surfaces were labelled 'not yet enforced' in #3197 (those labels are untouched), and line 779 records that WellKnownCapabilitiesSchema gained transactionalBatch in v16. This change adds no capability key; it changes how websockets is COMPUTED, not the vocabulary." }, "scope_against_ruling": "20 files, all inside ruling A's (a) realtime stays out of open core / (b) discovery stops advertising an unmounted realtime service / (c) one explicit definition. (c): spec/src/api/discovery.zod.ts (the predicate, the slot set, the reader, the field descriptions). (c)+(a): spec/src/contracts/realtime-service.ts (optional getChannelRoute - the seam by which a non-open-core host advertises, which is what lets open core mount nothing). (b)+(c): metadata-protocol/src/protocol.ts and runtime/src/http-dispatcher.ts (both producers derive the entry, routes.realtime and capabilities.websockets from the predicate) and their two pin files. (a): service-realtime/src/realtime-service-plugin.ts (comment only, no behaviour) and service-realtime/src/no-channel-route.pin.test.ts (pins that the shipped occupant names no channel, making (a) a decision rather than an omission). (b): the two existing pins updated from enabled:true to false (objectql/src/protocol-discovery.test.ts, runtime/src/http-dispatcher.test.ts). Test infrastructure for (b): runtime/package.json, runtime/vitest.config.ts, pnpm-lock.yaml - dev-only, to reach the real InMemoryRealtimeAdapter from the runtime pin. Published docs this change falsified: protocol/kernel/realtime-protocol.mdx, kernel/services-checklist.mdx. Generated, never hand-edited (consequence of (c)): references/api/discovery.mdx, references/api/protocol.mdx, spec/api-surface/api.json, spec/export-origins/api.json. Repo requirement: the changeset.", "mcp_calls": "1 - every GitHub read and write went through repo-scoped REST (probed first: issue read HTTP 200; issue/PR create, label add and read-backs all 200/201). The ONE MCP call is a declared channel switch: REST /search/issues returned HTTP 403, so the pre-file duplicate check for the finding below used a single targeted mcp__github__search_issues. It returned a non-empty result (one unrelated issue, #13285), so the channel is proven live and no separate control term was needed.", "open_questions": [], "out_of_scope_findings": [ "filed as #15982: the same capability bullet in content/docs/kernel/services-checklist.mdx also names the retired `features` map spelling (#4828 renamed it to `capabilities`) and the retired `workflow` slot (#4451) - both wrong before this change, so only the clause this change falsified was corrected here. Unassigned, no labels." ], "notes": "PARKED as instructed: draft, ready NOT flipped, auto-merge NOT armed. Clause-2 re-declared from the diff as YES, which agrees with the ruling comment 5542632689's own declaration (the claim comment's 'Clause-2: no' was explicitly this-seat's-reading): the published /discovery payload changes what it says for realtime, and @objectstack/spec gains three exported members plus one optional interface member on a published contract face - additive in shape, changed in value. needs:contract-review is on PR #15978 and on this card; both label writes were read back and both are present. One platform write to note rather than correct: the PATCH that added the docs verdicts left TWO attribution footers on the PR body - my session-URL footer survived verbatim and the platform appended a bare one under it. A full byte diff of the read-back shows that append is the ONLY difference; no content was mutated. Not repaired, per the no-re-append rule." }Generated by Claude Code
claude commentedon Sep 5, 2026
Correction: this seat's claim comment declared
Clause-②: no. That was wrong. The round's re-declaration ofyesis correct, and this PR PARKS.The claim comment's tier call was this seat's reading, and the implementing round re-derived it from the diff instead of inheriting it. It was right to. Measured on the branch against
origin/main:Three members added to the published api-surface, none removed or narrowed, plus one optional member on a published interface (
IRealtimeService.getChannelRoute). Additive widening of a published contract face is clause-② by the settled boundary — the 2026-08-28 ruling excludes runtime permission/security behaviour from clause ②, but says 「条款②只指已发布契约面」, and this diff changes that face rather than only behaviour behind it. Soyes.The changeset is
minorfor@objectstack/spec,@objectstack/metadata-protocoland@objectstack/runtime— correct, and the rule that decides it is the act, not the commit type: additive widening of a published surface is at leastminorregardless of afix(...)-shaped title.Carriers verified rather than taken on report:
check-clause2-carriers --pair 15978exits 0, andneeds:contract-reviewis present on both this card and PR #15978. The PR is draft,auto_mergeis null, and it is not in the merge queue.Consequence, stated plainly. A
Clause-②: nofrom this seat would have routed a published-surface widening to land with no contract review at all. The park exists because the round re-measured instead of trusting the dispatch brief. That is the second tier call this seat got wrong today in the opposite direction — on #14552 the brief wrongly predicted a park that measurement retired — and both were caught the same way, by a round treating the PM's framing as falsifiable rather than binding.Status: PARKED, and it stays parked. The
CONTRACT_REVIEW_TIERneeded for a clause-② review has been rate-limited since roughly 15:10 today; real probe tasks at 15:55 and 16:44 both returned HTTP 429. The quota exemption covers dispatch only and has never covered contract review, so this does not get waved through on a green CI. ⛔ Do not flip ready and ⛔ do not arm this PR until an at-tier review passes it.Generated by Claude Code
fix(#14646): re-measure the moved census control, and drop the runtim…
docs(#14646): record at both sites what the runtime pin stopped proving
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 14646, "status": "done", "branch": "claude/issue-14646-discovery-stops-advertising-unmounted-realtime", "pr": "https://github.com/objectstack-ai/objectstack/pull/15978", "head": "3c22dddf2", "premise_still_valid": true, "summary": "Unchanged from the first report: the handler is `discoveryHandler` in packages/rest/src/rest-server.ts, serving GET /api/v1/discovery via ObjectStackProtocolImplementation.getDiscovery() (packages/metadata-protocol/src/protocol.ts); the second producer is HttpDispatcher.getDiscoveryInfo() (packages/runtime/src/http-dispatcher.ts). The definition - 'A subscribable channel exists only where discovery reports handlerReady: true together with a connectable route; enabled never means \"there is a channel\"' - is isSubscribableChannel() in @objectstack/spec/api, enforced by being the computation both producers use for services.realtime.enabled and capabilities.websockets. This round repaired the two CI failures on 04b60ff7d and recorded, at both code sites and in the changeset, what the second repair cost.", "ci_repairs": { "1_dogfood_census": "authz-probe-blind-spot.test.ts. Cause: the comment added to realtime-service-plugin.ts moved a pinned EXACT control count - the census pattern is a bare /RealtimeService/g, so it matches inside `IRealtimeService` and PROSE about the symbol moves the symbol's count exactly as code does. Reproduced locally first (exit 1, 1 failed / 32 passed), and the count verified independently: 10 at merge base d4c2cb196, 11 at HEAD, the single new occurrence being `IRealtimeService.getChannelRoute()` in my comment. Remedy taken: RE-MEASURED THE LEDGER 10 -> 11, which the census's own header says is what the pin is for ('re-read by whoever moved it instead of aging into a stale claim'). The comment was NOT reworded to push the counter back. Green after: exit 0, 33 passed. The row now carries a note naming the phenomenon for the next person who documents that file.", "2_type_source_resolution": "check-type-source-resolution. WHICH CASE I MEASURED: the ALREADY-COUNTED-PROGRAM case, not onboarding. Measured against origin/main rather than recalled - packages/runtime's `typecheck` script names tsconfig.test.json byte-identically before and after this diff (`tsc --noEmit && pnpm check:test-typecheck` -> `... --package packages/runtime --project tsconfig.test.json`), and the diff added one devDependency line and no script. So the (via tsconfig.test.json) program was already counted, which the registry doc-block calls 'the exposure this ratchet exists to catch'; the re-baseline limb was closed to me and widening the shrink-only entry was refused. WHICH REMEDY I TOOK: NEITHER - I removed the dist-resolved import entirely. `paths` was tried first, as the gate instructs, and measured: it cleared check-type-source-resolution (exit 0, registry unwidened) and then made `pnpm --filter @objectstack/runtime typecheck` exit 1 with 13 TS6059 from @objectstack/service-realtime's file graph billed into packages/runtime's test-typecheck ledger - the PR #12570 shape, a ledger that package cannot see - because the mandated rule pulls another package's source into a program whose rootDir is ./src, which tsconfig.test.json's own header states it will not widen. That ledger's only other remedy is MAINTAINER-ONLY. All 13 were TS6059 and none was a code error. So the devDependency, the vitest alias and the lockfile change were reverted to the merge base (verified byte-identical to d4c2cb196; `pnpm install --frozen-lockfile` exit 0 proves the restored lockfile is consistent), and the runtime pin declares its occupant locally. Green after: runtime typecheck exit 0, runtime suites 274 passed, check-type-source-resolution exit 0." }, "pin_strength": { "1_real_or_stand_in": "A STAND-IN, in those words. The runtime producer pin no longer drives the shipped InMemoryRealtimeAdapter.", "2_what_it_stopped_proving": "Named assertion: 'does NOT advertise an in-process realtime bus as a channel' (the first case in packages/runtime/src/discovery-realtime-channel.pin.test.ts) no longer says anything about the SHIPPED occupant - only that the producer derives its answer from whatever occupant it is given. Measured, one mutation and two pins: giving InMemoryRealtimeAdapter a getChannelRoute() returning '/api/v1/realtime' (mutation proved on disk - marker 0 -> 1, blob 02c22cf22 -> ca40ae771; restore proved by blob equality and an empty git diff HEAD; trap on absolute paths) leaves the runtime pin GREEN on all 4 cases. Before the revert that same mutation would have reddened it. That is the whole of the loss.", "3_is_it_recovered": "YES, and measured rather than asserted - by the same mutation. packages/services/service-realtime/src/no-channel-route.pin.test.ts goes RED under it: exit 1, 2 of its 3 cases fail ('names no channel route', 'is therefore not a subscribable channel in discovery terms'). That pin drives the real class through a relative in-package import, so it costs no cross-package type edge. The two pins divide the reading - that one pins WHAT THE SHIPPED OCCUPANT NAMES, the runtime one pins THAT THE PRODUCER DERIVES FROM WHATEVER AN OCCUPANT NAMES - and neither can go green by accident of the other, so the runtime side only ever needed the derivation. Scope of the equivalence claim, measured not assumed: the producers perform exactly two reads on an occupant (readChannelRoute, readServiceSelfInfo); both return undefined for the real adapter AND for the stand-in today. That is a statement about today, which is exactly why the shipped-occupant claim lives in the other package rather than here. Recorded where PM asked, not only in this report: the docblock of the runtime pin now states the loss and the mutation result verbatim, and the changeset carries a testing note saying the producer pins use a stand-in and why." }, "tests": "All at head 3c22dddf2 unless stated; exit codes captured by redirecting first, never after a pipe. REPRO BEFORE FIX: dogfood census exit 1 (1 failed / 32 passed), assertion 'control \"RealtimeService\" moved ... expected 11 to be 10'. AFTER: exit 0, 33 passed. check-type-source-resolution --self-test exit 0; check-type-source-resolution exit 0 (125 tsc programs across 78 packages; 61 registered - registry NOT widened). pnpm --filter @objectstack/runtime typecheck exit 0 (was exit 1 with 13 TS6059 under the `paths` attempt). runtime pin + http-dispatcher + discovery-schema-conformance exit 0, 274 passed. service-realtime full package exit 0, 31 passed. check:cross-package-test-inputs, check:test-source-alias, check:nul-bytes, spec check:generated, spec check:api-surface, check:doc-anchors, check:docs-single-h1, check-empty-changeset --base origin/main - all exit 0 at the final head. TEETH EXPERIMENT (the answer to 'is the coverage recovered'): one mutation of the shipped adapter, run under the shared verify lock, with an EXIT/INT/TERM trap on absolute paths. Mutation proved on disk (getChannelRoute occurrences 0 -> 1; blob changed). service-realtime pin exit 1, 2 failed / 1 passed. runtime pin exit 0, 4 passed. Restore proved: blob back to 02c22cf22c7fbf15805b6fc323ab760fed7d7e03 and git diff HEAD over the file empty. No rebuild needed for either leg: both pins reach their subject through in-package source or a src-aliased spec. Earlier-round evidence unchanged and still valid at this head for the parts it covered: metadata-protocol full package 2391 passed / 10 skipped, objectql full package 4714 passed, runtime full package 3295 passed, rest's five discovery-* suites 40 passed, the red-then-green ablation of both producers.", "dispatch_gates_process_note": "MEASURED, and it corrects the hypothesis in the brief: the stale-tree warning and the missed gate are NOT connected. `check:type-source-resolution` WAS in the first derived family - it is on line 58 of that same run's output, matched via gate source 'packages/*' through packages/runtime/package.json among others. Independently, the staleness warning named its own affected families (check-role-word.mjs, bare-root-worklist.mjs, ensure-pm-labels.sh) and this gate is not one of them. So dispatch-gates.mjs did its job and no card is warranted for it; the miss is MINE - I ran a hand-picked subset of the 53 derived gates (the glob 'packages/*' makes that derivation nearly the whole farm, so a subset was unavoidable) and my selection rule was wrong: it did not include a gate whose subject - the type resolution of a NEW cross-package dependency edge - was something my diff newly CREATED. That is the rule I would carry forward: from the derived list, always run the gates whose subject your diff newly creates, whatever else you skip.", "mcp_calls": "1 for the whole run, unchanged - no MCP call this round. Every GitHub read and write went through repo-scoped REST. The one MCP call was the declared channel switch for the pre-file duplicate check (REST /search/issues returns 403), and it returned a non-empty result so the channel was proven live.", "open_questions": [], "out_of_scope_findings": [ "filed as #15982: the capability bullet in content/docs/kernel/services-checklist.mdx also names the retired `features` map spelling (#4828) and the retired `workflow` slot (#4451) - both wrong before this change. Unassigned, no labels.", "NOT filed, reported for your decision: after a package's test layer has been onboarded and re-baselined, a NEW cross-package source dep in its test program has no self-serve remedy in a package whose rootDir is ./src - the registry refuses the widening, `paths` produces TS6059 billed to a ledger the other package cannot see, and both the ledger expansion and the rootDir widening are closed to a dev seat. I judged this working-as-designed (the correct outcome, not taking the dep, is what the ratchets steered me to) rather than a defect, so I did not file a card. File one if you read it the other way." ], "notes": "PARKED and untouched: draft, ready NOT flipped, auto-merge NOT armed, needs:contract-review on PR #15978 and card #14646. Clause-2 stays YES. The PR body was updated to correct its own now-false claim that the runtime pin drives the real adapter, and to carry the two-failure reading; the platform again appended a second bare attribution footer on that PATCH - a full byte diff of the read-back shows the append is the ONLY difference, and it is left alone per the no-re-append rule." }Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 14646, "status": "done", "branch": "claude/issue-14646-discovery-stops-advertising-unmounted-realtime", "pr": "https://github.com/objectstack-ai/objectstack/pull/15978", "head": "54fba060e", "premise_still_valid": true, "summary": "This round: merged origin/main (f50c394da, #16002) into the branch, validated the merged tree, pushed. The card's implementation is unchanged - isSubscribableChannel() in @objectstack/spec/api is the one definition, enforced by being the computation both discovery producers use for services.realtime.enabled and capabilities.websockets. No conflicts, nothing regenerated, nothing to escalate.", "merge": { "how": "git fetch origin main, then `bash scripts/pm/os-regen-merge.sh` - the repo's own sequence - exit 0. MERGED, never rebased, amended or force-pushed. Merge commit 54fba060e; origin/main was exactly f50c394da, confirmed an ancestor.", "conflicts": "NONE, and the reason is measured rather than lucky: main touched NOT ONE file this branch edits - checked file by file over discovery.zod.ts, realtime-service.ts, protocol.ts, http-dispatcher.ts, realtime-service-plugin.ts, the census, and both hand-edited docs pages: all 'main-moved: NO'. The generated shards are disjoint too: main moved api-surface/automation.json, export-origins/automation.json and five other reference pages; this branch moved api-surface/api.json, export-origins/api.json and references/api/{discovery,protocol}.mdx. os-regen-merge step 2 therefore KEPT the branch's bytes on all four (main did not move them, so taking main's side would have been a pure revert), and step 3 recorded no additional changes. No os-regen deferral was written to $GIT_DIR/os-regen-pending. Nothing was hand-edited in any generated artifact.", "both_sides_survive": "Asserted in both directions, per os-regen-merge step 4.3: main's three new entries (EndConfig, EndConfigParsed, EndConfigSchema) are present in api-surface/automation.json, and this branch's three (CHANNEL_SURFACE_SLOTS, isSubscribableChannel, readChannelRoute) are present in api-surface/api.json.", "regeneration": "NOT NEEDED, and proven rather than assumed: the spec build closure was rebuilt on the merged source first (AGENTS.md sec.9/sec.10 - only a rebuild of merged source can validate a generated artifact), then `pnpm --filter @objectstack/spec run check:generated` exit 0 - zero of 15 artifacts stale. So there is no regeneration commit; the merge commit is the whole of this round's diff. Branch delta vs origin/main is unchanged: exactly 3 added / 0 removed in both api-surface/api.json and export-origins/api.json, 18 files / 784 insertions / 67 deletions - identical to pre-merge.", "the_fix_verified_by_content": "f50c394da checked by reading it, not the API: scripts/check-regen-pending.mjs now reads the root package.json's packageManager (line 951) and writes it into the fixture (959), with the self-test case 'the fixture pins the ROOT's packageManager, so Corepack cannot resolve latest' (1042-1045); fixtureSelfTest appears 5x. Root packageManager is pnpm@10.31.0+sha512..." }, "tests": "All on the merged tree at 54fba060e; exit codes captured by redirecting first, never after a pipe. GREEN: @objectstack/spec test exit 0 (12838 passed, 478 files); @objectstack/metadata-protocol test exit 0 (2391 passed, 10 skipped, 166 files); @objectstack/runtime test exit 0 (3295 passed, 232 files); @objectstack/service-realtime test exit 0 (31 passed); the dogfood census pin test/authz-probe-blind-spot.test.ts exit 0 (33 passed); objectql src/protocol-discovery.test.ts exit 0 (32 passed); runtime typecheck exit 0; spec typecheck exit 0. GATES, each exit captured: check:merge-driver 0 (this is the gate that was red - it runs check-regen-pending's self-test); check-regen-pending.mjs --self-test 0 and its main run 0; check-type-source-resolution --self-test 0 and main 0 (125 programs / 78 packages, registry unwidened); spec check:generated 0; check:cross-package-test-inputs 0; check:test-source-alias 0; check:service-providers 0; check:nul-bytes 0; check:doc-anchors 0; check:doc-authoring 0; check:docs-single-h1 0; check:docs-redirects 0; check:docs-audit-scope 0; check:corpus-claim-drift 0; check:docs-transcript-drift 0; check-doc-frontmatter 0; check-empty-changeset --base origin/main 0; check:objectui-changeset 0. THREE READINGS RECORDED AS **NOT MEASURED**, none of them a red: (1) two lock acquisitions returned the wrapper's exit 99 (queue-timeout, never acquired) - the box was heavily contended; the slot OS_VERIFY_LOCK_SLOT=issue-14646-dev kept the place and both were re-run to completion. (2) `pnpm check:regen-pending` returned 254 - no such script; the real entry point is `check:merge-driver`, and the script itself was then run directly. (3) metadata-protocol's first run showed 162 test FILES failed with only 40 tests executed - diagnosed as an unbuilt dependency closure in the fresh worktree ('Failed to resolve entry for package @objectstack/core' etc.), not assertion failures; after `pnpm --filter '@objectstack/runtime^...' build` it is the 2391-pass run above. Same class: check:docs-transcript-drift returned its own exit 3, whose text says 'PREREQUISITE NOT MET ... this is NOT a pass and NOT a finding: nothing was measured' - @objectstack/lint was unbuilt; after building it, 0.", "mcp_calls": "1 for the whole card, unchanged - no MCP call this round. Every GitHub read and write went through repo-scoped REST.", "open_questions": [], "out_of_scope_findings": [ "filed as #15982 (earlier round): the capability bullet in content/docs/kernel/services-checklist.mdx also names the retired `features` map spelling (#4828) and the retired `workflow` slot (#4451). Unassigned, no labels. Unchanged by this merge - main did not touch that page.", "NOT filed, still open for your call (earlier round): after a package's test layer is onboarded and re-baselined, a NEW cross-package source dep in its test program has no self-serve remedy in a package whose rootDir is ./src. I judged it working-as-designed." ], "notes": "PARK UNCHANGED AND UNTOUCHED: still draft, ready NOT flipped, auto-merge NOT armed, needs:contract-review on PR #15978 and card #14646, Clause-2 still YES. I did not re-run any CI job, disabled or quarantined nothing, made no empty commit, and did not widen the PR - this round's entire diff is the merge commit. PR body updated to record the merge and the re-run figures; the platform again appended a second bare attribution footer on that PATCH, left alone per the no-re-append rule. Nothing came back red that was not check:merge-driver's own subject, so there was nothing to escalate before acting." }Generated by Claude Code
os-zhuang commentedon Sep 5, 2026
Contract review (clause ②) — PASS — PR #15978 at head
54fba060Director seat, summon #15,
session_01TezFG8ZMrNH6n5VTNpPpdH(os-zhuang), 2026-09-05T22:4xZ, on the maintainer's instruction 「有很多pr等着契约复审」. Tier fuse:get_sessionthis session readssession_context.model=last_served_model=CONTRACT_REVIEW_TIER. Readings from the PR diff,origin/mainand the sibling repos; dev reports (5553790377, 5554621496) and the seat's correction (5553510620) read afterwards as cross-check.Implemented-by:
session_01ARYe3yQTQCUFm5qPYNgKaJos-dev round (branchclaude/issue-14646-discovery-stops-advertising-unmounted-realtime)Reviewed-by:
session_01TezFG8ZMrNH6n5VTNpPpdH① Derived judgments against ruling A (5542632689)
discovery.zod.ts:isSubscribableChannel(info) = handlerReady === true && typeof route === 'string' && route.length > 0;CHANNEL_SURFACE_SLOTS = {'realtime'};readChannelRoute(svc)reads an occupant's optionalgetChannelRoute(). Computed in one place;enabled/websockets/routes.realtimedescribes rewritten to say what they do and do not mean./discoverystops advertising an unmounted realtime servicemetadata-protocolgetDiscovery(),runtimegetDiscoveryInfo()) import the predicate and deriveservices.realtime.enabled,capabilities.websocketsandroutes.realtimefrom it, replacing a literalfalseand a hardcodedundefined. On a stock boot the shippedInMemoryRealtimeAdapterimplements nogetChannelRoute⇒enabled: false, no route — retraction computed, not hardcoded. Pins: two-direction (an in-process bus is NOT advertised; a declared occupant with a route IS), red-first ablation 3/2 and 3/1 on the two producer suites.service-realtimechange is a comment plus ano-channel-route.pin.test.tsthat pins the occupant names no channel (makes (a) a decision, not an omission).api-surface/api.json+3 (CHANNEL_SURFACE_SLOTS,isSubscribableChannel,readChannelRoute), 0 removed / narrowed; one optional memberIRealtimeService.getChannelRoute?()on a published interface (additive; existing implementers keep compiling). The wire payload changes one value (realtime.enabledtrue → false on stock boots) and stops emittingroutes.realtimewhen unmounted — both the ruled behaviour. Consumers: objectui / cloudorigin/mainhave zero reads ofservices.realtime.enabled,capabilities.websocketsorroutes.realtimeoutside tests (this seat's grep); the one known consumer, the objectui bell (#7249), already polls.yeson the additive wideningcache/queue/jobuntouchedCHANNEL_SURFACE_SLOTS; in-process contracts keepenabled: truewith no route. Avoids the over-retraction a global "enabled means route" would have caused.realtime-protocol.mdxandservices-checklist.mdxwere falsified and are fixed; four other pages read per file and judged null;content/docs/releases/v16.mdxcorrectly left read-only;content/docs/references/**regenerated. Residual pre-existing errors filed as #15982, not folded in./RealtimeService/gmatches prose) — re-measured, not fudged;check-type-source-resolutionmeasured as the already-counted-program case. Both explained with the measurement.② semver
@objectstack/spec,@objectstack/metadata-protocol,@objectstack/runtimeminor, with the client note the ruling asked for ("a client that keyed onenabled: trueto subscribe was subscribing to nothing; it now seesfalse"). Additive widening + a value change on a published payload ⇒ minor is right; thefix(...)title does not lower it.③ Boundary flags
Clause-②: nocorrected by the seat itself (5553510620) after the round measured +3 api-surface members. Concur with the correction.service-realtime); theno-channel-route.pin.test.tsinservice-realtimecloses that half from the other side. Accepted.Evidence and landing
CI on
54fba060: 35 success / 2 skipped / 0 red;mergeable_state: clean;check-governed-merges --test0 of 18 paths.os-regen-merge.shdrove the main merge (exit 0, nothing deferred).Closes #14646correct.Clearing, same stroke:
needs:contract-reviewoff this card and PR #15978 with provenance; then ready + auto-merge (SQUASH).Generated by Claude Code
os-zhuang commentedon Sep 5, 2026
MERGED — PR #15978 landed via the merge queue at 2026-09-05T23:23:03Z, merge commit
64914638935e65a0f554987a63f4cfb752ce22c9. Landed content read from that commit: 18 files —packages/spec/src/api/discovery.zod.ts(+129/−10,isSubscribableChannel/CHANNEL_SURFACE_SLOTS/readChannelRoute),packages/spec/src/contracts/realtime-service.ts(+32,getChannelRoute?()),api-surface/api.json+export-origins/api.jsonregenerated,runtime/src/http-dispatcher.ts,metadata-protocol/src/protocol.ts,service-realtime/src/realtime-service-plugin.ts, three pin tests, docs pages — the reviewed set (PASS at head54fba060). Card closed byCloses;pm:dispatchedalready off;priority:p2·domain:servicesretained. Director seat,session_01TezFG8ZMrNH6n5VTNpPpdH, 23:24Z.Generated by Claude Code
docs(qa): classify the five UNCLASSIFIED capability ledgers — four RE…
fix(discovery): stop advertising a realtime service with no mounted s…
feat(spec)!: RealtimeEventType names the emitted data.record.* / data…
docs(service-realtime): re-anchor the dead tracker citation to the co…