Skip to content

IPubSub's contract docblock states a delivery guarantee no shipped driver provides ("At-least-once delivery") #12651

Description

@os-warren

Found while building the authz.invalidated channel for #11968 (PR on claude/issue-11968-authz-cache-substrate). Filed unassigned, no repair attempted there — it is a packages/spec change and out of that card's scope.

The contradiction, measured on origin/main

packages/spec/src/contracts/cluster-service.ts:73, the docblock on the IPubSub interface itself:

Fan-out messaging primitive. At-least-once delivery; handlers MUST be idempotent. The memory driver delivers synchronously within a process; remote drivers (redis pub/sub, postgres LISTEN/NOTIFY, nats) deliver across nodes.

content/docs/kernel/cluster.mdx §4.2, on the same at-least-once value:

No shipped driver provides this yet. The redis driver publishes over plain Redis pub/sub, which is at-most-once — fire-and-forget, no persistence, no replay for a node that was down at publish time.

And @objectstack/service-cluster-redis's own publish docblock says the same in its own words: at-most-once, "acceptable only for events that are pure cache-invalidation hints, never the source of truth." The memory driver does not cross a process boundary at all.

So the two statements disagree, and the one on the interface — the place a consumer reads before writing a subscriber — is the one that is wrong. cluster.mdx §4.2 and the driver docblocks are the measured statements.

Why this is worth a card rather than a shrug

The interface docblock is the load-bearing one for the failure mode it invites. A consumer that reads "At-least-once delivery; handlers MUST be idempotent" reasonably concludes that a message will arrive and that its only obligation is to tolerate duplicates. The actual transport's failure mode is the opposite — a lost message, with no replay and no upper bound on how long a node stays wrong. An author who designs for duplicates and not for loss has designed for the wrong hazard, and nothing in the type or the tests contradicts them.

Two things narrow the blast radius today and neither closes it:

Suggested shape (not a decision — this is a spec-seat call)

Correct the IPubSub docblock to state the shipped guarantee (at-most-once; a missed message is expected; handlers must be idempotent and tolerate loss), and point at deliverySemantics for the per-channel declaration. Whether the interface should say "at-most-once" flatly or "whatever the driver declares, and no shipped driver exceeds at-most-once" is the question worth answering deliberately, because the second phrasing keeps the door open for a durable driver without re-writing the contract when one lands.

Anything stronger — a declared per-driver guarantee the type system can read, or refusing an at-least-once channel declaration at boot while no driver can honour it — is a larger design question and deliberately not proposed here.

Not in scope of this report

Whether any existing consumer of IPubSub was written against the wrong guarantee. Only metadata.changed and (with #11968) authz.invalidated are in production use, and both are cache-invalidation hints, which is the one thing the transport is documented as acceptable for. A sweep of subscribers is a separate task if someone wants it.

Activity

  1. os-litant commented on Aug 27, 2026

    @os-litant
    Contributor

    Triage first-touch (triage seat, daily round 2026-08-27, session session_01Czs5tCwMymUvzB9QCA2yhH): graded pm:queue, type Task, routed domain:spec — the fix is the IPubSub contract docblock in packages/spec/src/contracts/cluster-service.ts, spec-lane sole-owner surface (describe/JSDoc prose face). Grading adopts the card's suggested shape: state the shipped guarantee honestly (at-most-once; a missed message is expected; handlers must be idempotent AND tolerate loss) and point at deliverySemantics for the per-channel declaration — prefer the driver-relative phrasing ("whatever the driver declares; no shipped driver exceeds at-most-once") so a future durable driver lands without rewriting the contract. Prose-only: no accept/reject behaviour changes, so no Clause-② tier. ⛔ The stronger designs the card names (type-readable per-driver guarantees, boot refusal of an at-least-once channel declaration) are out of scope — if measurement pulls toward them, that comes back as a decision card. The subscriber sweep stays un-carded per the card's own scoping; the two production consumers are cache-invalidation hints, the documented-acceptable class.


    Generated by Claude Code

  2. added
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 27, 2026
  3. added theissue type on Aug 27, 2026
  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 28, 2026
  5. self-assigned this
    on Aug 28, 2026
  6. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 9 (domain:spec seat) — batch cap raised to 3 this stroke (maintainer 2026-08-28, verbatim 「并发加到3」, superseding the 2026-08-26 「并发降到2」)
    Session: session_01JvjTCjJQn9zSTXEhUKgT7s
    Branch: claude/issue-12651-ipubsub-delivery-docblock
    Worktree: objectstack-issue-12651
    Domain: domain:spec
    File surface: packages/spec/src/contracts/cluster-service.ts (the IPubSub docblock at :73) + regenerated spec artifacts (repo tooling only) + content/docs/kernel/cluster.mdx only if its §4.2 cross-reference needs the pointer updated (measure, don't assume) + changeset-or-skip per the repo docs rule (stop on breach; explain in the report)
    Container & model: S (prescribed wording, non-mechanical verification), mode:subagent, model: opus — Clause-② no per triage (comment 5435192691: prose-only, no accept/reject change); dispatch-gates --tier at f75a38af reports no path-derived mandate (cluster-service.ts flagged clause-② suspect — content judgment: prose-only, no)
    Clause-②: no
    Serial constraints cleared: batch siblings #12599 (contracts/crypto-provider.ts, subagent) and #12038 (api/** + ledgers, cloud) — all three source-disjoint (different files; same contracts/ dir as #12599 but no shared file); all spec PRs share the generated-artifact landing relay (#12718 → #12737 → then these, in green order), dispatch-time parallel safe. H17 (#8345/#8346): no intersection. No in-flight card pins IPubSub delivery semantics (the #11968 authz.invalidated PR records the contradiction inline and is the beneficiary, not a collision).

    Premise re-checked at claim (origin/main f75a38af): the wrong sentence still at cluster-service.ts:73 ("At-least-once delivery"); the measured correction still at cluster.mdx:147 ("No shipped driver provides this yet"). Triage's adopted shape governs: state the shipped guarantee honestly — driver-relative phrasing ("whatever the driver declares; no shipped driver exceeds at-most-once"), handlers idempotent AND loss-tolerant, pointer to deliverySemantics; the stronger designs stay out of scope.


    Generated by Claude Code

  7. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    Dev claim (os-dev subagent, dispatched by the domain:spec PM seat on PM claim comment 5447301043).

    Session: session_01JvjTCjJQn9zSTXEhUKgT7s
    Branch: claude/issue-12651-ipubsub-delivery-docblock (pushed empty at f75a38af, write route verified before the first edit)
    Worktree: objectstack-issue-12651

    Working the triage-adopted shape only (comment 5435192691): driver-relative phrasing on the IPubSub docblock, handlers idempotent AND loss-tolerant, pointer to deliverySemantics. The stronger designs the card names stay out of scope. Assignee untouched.


    Generated by Claude Code

  8. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 12651,
      "status": "done",
      "branch": "claude/issue-12651-ipubsub-delivery-docblock",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12837",
      "premise_still_valid": true,
      "summary": "Rewrote the IPubSub contract docblock at packages/spec/src/contracts/cluster-service.ts in the triage-adopted shape: delivery is whatever the configured driver declares and no shipped driver exceeds at-most-once (driver-relative, so a future durable driver needs no contract rewrite); a missed message is EXPECTED and handlers must be idempotent AND tolerate loss; the staleness bound lives outside the bus; deliverySemantics is named as the per-channel surface along with what it is (what a channel asks for, not what it gets). The driver-reach sentence (memory synchronous in-process, remote drivers cross nodes) is unchanged - it was never part of the false claim. New text worded to AGREE with cluster.mdx section 4.2, the service-cluster-redis publish docblock and the authz.invalidated channel module rather than add a third wording. Patch changeset for @objectstack/spec, not skip-changeset: the docblock compiles into the published declarations (verified in packages/spec/dist/contracts/index.d.ts), so it reaches consumers' editors, which is the point of the repair. BOTH PM mechanism assumptions measured FALSE and no file edited for either: (1) the docblock reaches NO generated artifact - build-docs.ts regenerates content/docs/references from JSON Schemas and IPubSub is a plain TS interface with no Zod schema; the two spec artifacts naming it (api-surface/contracts.json, export-origins/contracts.json) record the symbol name only, and a full spec build with gen:schema + gen:openapi left git status carrying nothing but my two files; (2) cluster.mdx section 4.2 carries NO cross-reference to the interface docblock - the sole IPubSub mention in content/docs is cluster.mdx:173 in section 4.3 about partitionKey, untouched by this change - so no docs file is edited. Out-of-scope designs (type-readable per-driver guarantees, boot refusal of at-least-once, subscriber sweep) untouched. Assignee never touched. Left as draft deliberately per the landing-relay note; origin/main merged in before opening the PR.",
      "tests": "All anchored to final commit 8368e05b (git rev-parse --short HEAD after merging origin/main ce3ea060); heavy runs via scripts/pm/os-verify-lock.sh, every exit code captured before any pipe. (1) pnpm --filter @objectstack/spec build - 'os-verify-lock: VERDICT command-exit 0'; 'check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present.'; corrected docblock confirmed present at packages/spec/dist/contracts/index.d.ts:9176 and the old 'At-least-once delivery' string absent from dist. (2) Gate union re-derived AFTER the merge with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' (no paths passed; script self-derives: 2 paths, three-dot semantics, merge base ce3ea0600) = 32 families, re-run in full on the merged head: 30 exit 0. Green includes check:generated, check:docs, check:authorable-surface, check:liveness, check:strictness-ledger, check:empty-state, check:variant-docs, check:skill-refs, check:doc-authoring, check:doc-formula-expressions, check:merge-driver, check:adr-0087-registration, check:empty-changeset, check:changeset-no-major, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:type-source-resolution, check:published-files, check:comment-mask-adoption, check:spec-parsed-alias, check:changeset-gate-self-tests, check:objectui-changeset, check:page-declaration-shape, check:slot-lookup, check:pm-half-states, check:ci-filter-parity, check:plugin-teardown-shape, docs-audit/check-affected-docs, docs-audit/check-drift-comment, release-rehearsal-clone --self-test. (3) The 2 non-green families are NOT MEASURED, not red, each in its own printed words: scripts/check-dev-prereqs.mjs exit 1 - 'The workspace is not built - 1 unmet precondition, not a list of problems' (66 of 67 packages unbuilt in this worktree; a full workspace build is CI's run); scripts/pm/check-half-states.mjs exit 3 - 'PREREQUISITE NOT MET - the token in the environment is not a valid GitHub credential ... Nothing was swept ... it is no reading at all' (container has a proxy placeholder, not a GitHub token; the gate inspects board state, not this diff). Note check:doc-formula-expressions initially exited 1 with its own 'PREREQUISITE NOT MET ... Nothing was measured' text; I built @objectstack/formula then @objectstack/lint under the lock and re-ran it to a real exit 0 rather than report the prerequisite as a result. (4) pnpm --filter @objectstack/spec typecheck - exit 0, including 'check:test-typecheck: OK - @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json'. (5) pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/kernel/cluster.test.ts - 'Test Files 1 passed (1) / Tests 19 passed (19)'. (6) Repo-wide ESLint, NOT a narrowed run and so no narrowing declaration is owed: eslint . --no-inline-config --format json completed under the lock in 93s, VERDICT command-exit 0; parsed from the JSON output - 5339 files (eslint's own enumeration), 0 errors, 0 warnings, 0 fatal, with contracts/cluster-service.ts confirmed present in the run. No ablation was run: this is a prose-only change with no guard to mutate, so an ablation would have nothing to prove and none is claimed.",
      "mcp_calls": "6 - issue_read get, issue_read get_comments, add_issue_comment (dev claim), issue_write (filed #12836), create_pull_request, add_issue_comment (this report). All bulk reads went through zero-quota channels: repo-scoped REST for the dedup scan and the PR body read-back, git and local grep for everything else. Channel note: the REST search endpoint is 403 for this seat ('sessions are bound to their configured repositories'), but repo-scoped REST reads answer 200, so the dedup scan used the REST list endpoint (4 pages, 326 open non-PR issues) plus a local regex - no MCP search call and no wide MCP scan.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12836: two docblocks elsewhere reference IPubSub's retired at-least-once claim and go stale exactly when this PR lands - the 'known contradiction' paragraph at packages/core/src/security/authz-invalidation-channel.ts:45-51 (which declares its own expiry: 'Repairing that docblock is a packages/spec change and is filed separately'), and 'At-least-once semantics held vacuously' at packages/services/service-cluster/src/memory/pubsub.ts:18 (also arguably wrong on its own terms - that same docblock says handler errors are swallowed, so a throwing handler loses the message). Not fixed in place: both sit outside the PM-declared file surface and in two different packages (@objectstack/core, @objectstack/service-cluster), so the bounded in-place exemption fails on new verification surface. Filed unassigned, no labels, carrying 'Blocked-by: #12651'. Dedup-checked against all 326 open non-PR issues before filing - no duplicate. Deliberately excluded from that card: packages/triggers/trigger-api/src/api-trigger.ts:31 also says 'At-least-once delivery' but about inbound API-trigger events per ADR-0041, a different subject on a different transport."
      ]
    }

    Generated by Claude Code

  9. added
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 28, 2026
  10. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    Review of record — PM ACCEPT + clause-② contract review PASS, both for PR #12837 at head 8368e05b.

    Contract-review chain (hung on both carriers this stroke, reviewed, cleared same-stroke below): the diff's path limb fires (packages/spec/src/contracts/cluster-service.ts) and the dispatch ran at opus — below CONTRACT_REVIEW_TIER — so the review-chain compensation applies. Eligibility per the widened self-review path (maintainer 2026-08-21, verbatim 「你自己就是 Fable,自己就可以审核」: an at-tier dispatch seat reviews the below-tier implementer's contract increment) with the downgrade fuse re-read this stroke: this seat's external_metadata.last_served_model = claude-fable-5 = CONTRACT_REVIEW_TIER. Contract increment read from the diff itself:

    • One docblock rewritten, exactly the triage-adopted shape (comment 5435192691): delivery is driver-relative ("whatever the configured driver declares, and no shipped driver exceeds at-most-once"), a missed message is EXPECTED, handlers idempotent AND loss-tolerant with the wrong-hazard warning stated, staleness bound placed outside the bus, deliverySemantics named as the per-channel surface with the honest ask-not-get framing; the driver-reach sentence untouched. The text AGREES with cluster.mdx §4.2 and the redis driver docblock (no third wording). Fact-checked against the tree: kernel/cluster.zod.ts:88-94 does default deliverySemantics to at-least-once when omitted with scope-dependent default — the new docblock's claim is accurate.
    • Zero accept/reject change: 2 files (docblock + patch changeset), no schema, no type, no generated artifact (the dev MEASURED the no-regen fact rather than assuming — full spec build left only the two files). Content limb: no. The narrowing is prose honesty, the exact declared-vs-real correction class.

    PM checklist: draft ✓ base main, Fixes #12651 first line, no other closure adjacency ✓ 2 files inside the claimed surface; both PM mechanism assumptions measured FALSE and honestly not acted on (no doc regen; cluster.mdx untouched — its §4.2 now agrees instead of contradicting) ✓ changeset patch correctly argued (the docblock ships in the published .d.ts — verified at dist/contracts/index.d.ts; not docs-only) ✓ gates 30/32 green with the 2 non-green quoted as their own PREREQUISITE-NOT-MET words (neither diff-sensitive), and one prerequisite refusal (doc-formula-expressions) built-and-re-run to a REAL exit 0 rather than reported as a reading ✓ repo-wide ESLint (5339 files, 0/0/0) ✓ no ablation claimed for a prose change (correctly: nothing to mutate) ✓ out-of-scope finding #12836 verified filed, dedup-controlled (326-issue scan), Blocked-by: #12651, with the api-trigger.ts near-miss deliberately excluded and the exclusion reasoned.

    needs:contract-review is CLEARED from both carriers in the same stroke as this PASS.

    Landing: waits on CI green at 8368e05b, then the landing relay (#12718 → #12737 → this, in green order; regen-merge sync if main moves). Per the review-chain landing rule this seat lands it (ready → queue) once the relay reaches it.


    Generated by Claude Code

  11. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions