Skip to content

feat(swarm): outbound /swarm/lessons feed handler — substrate (#139)#155

Merged
Dewinator merged 1 commit into
mainfrom
agent/swarm-139-feed-handler
May 1, 2026
Merged

feat(swarm): outbound /swarm/lessons feed handler — substrate (#139)#155
Dewinator merged 1 commit into
mainfrom
agent/swarm-139-feed-handler

Conversation

@Dewinator
Copy link
Copy Markdown
Owner

Summary

First slice of issue #139 — pure handler module + 32 contract tests for the §10.6 outbound broadcast feed (GET /swarm/lessons?since=…&limit=…). Mirrors the #138 / #154 substrate-vs-wiring split.

  • mcp-server/src/swarm/endpoints/lesson-feed.ts — pure response builder. The loader is injected so contract tests run with no DB / HTTP.
  • mcp-server/src/__tests__/lesson-feed.test.ts — 32 tests covering input validation, defaulting, clamping, cursor math, and shape-pin regression guards.

Status mapping

  • 400 — malformed since (must be ISO8601 with timezone) or malformed limit (must be an integer; clamped silently if outside [1, 200], rejected if non-numeric / decimal / NaN).
  • 503 — loader threw; operator-readable detail field in the body.
  • 200{ lessons: Lesson[], next_since: string | null }. next_since = max(signed_at) + 1ms; null on empty page.

Substrate filter contract

Documented on the loader signature: it MUST read FROM swarm_lessons_broadcast_eligible (the §10.6 firebreak view from migration 078) AND apply local_weight >= 0.3 (the §10.4 anti-echo-chamber filter from migration 082). The handler deliberately does NOT re-filter — the substrate IS the contract; the integration test that verifies this lands with the wiring PR.

Privacy invariant

Rows are relayed verbatim — the handler never re-signs. Re-signing would let any node forge producer attestations for lessons it has merely seen (the same trust break the proof handler's relay-guard prevents at §4.6).

Out of scope (issue #139 pieces 2 + 3)

Test plan

  • npm test — 678 pass (baseline 646; this PR adds 32 tests)
  • tsc --build clean
  • Reed merges; integration test ships with the wiring PR

🤖 Generated with Claude Code

Pure handler module + 32 contract tests for the §10.6 outbound
broadcast feed. The loader is injected so tests run with no DB
or HTTP — first slice of issue #139, mirrors the #138 substrate /
wiring split.

Status mapping:
  400 — malformed since (must be ISO8601 with timezone) or
        malformed limit (must be an integer; clamped if outside
        [1,200] but rejected if non-numeric / decimal / NaN).
  503 — loader threw; operator-readable detail in the body.
  200 — { lessons: Lesson[], next_since: string | null }.
        next_since = max(signed_at) + 1ms; null on empty page.

Substrate filter contract documented on the loader: it MUST read
FROM swarm_lessons_broadcast_eligible (mig 078 firebreak) AND
apply local_weight >= 0.3 (mig 082 anti-echo-chamber). The handler
deliberately does NOT re-filter — the substrate IS the contract.

Privacy invariant: rows are relayed verbatim, never re-signed.
Re-signing would let any node forge producer attestations for
lessons it has merely seen — same trust break the proof handler's
relay-guard prevents at §4.6.

Out of scope for this PR (issue #139 pieces 2 + 3): active publish
at REM B→A promotion (lesson_chain append + producer-side signing),
and the self-broadcast safety CHECK / trigger. No HTTP wiring, no
migration, no dashboard-server.mjs touch — wiring lands as a
follow-up.

Test plan
- [x] `npm test` — 678 pass (was 646; this PR adds 32 contract tests)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dewinator added a commit that referenced this pull request May 1, 2026
Closes the "Self-broadcast safety CHECK / trigger preventing a self-row from
carrying a foreign-looking origin_node_id" follow-up listed as out-of-scope
in PR #155 (issue #139, outbound /swarm/lessons feed handler substrate).

Adds migration 085 with two BEFORE triggers that converge on a single
invariant: a row whose id appears in lesson_chain (074) — definitionally a
self-row — MUST carry origin_node_id equal to the local self identity.

Both triggers funnel through a shared self_broadcast_assert_origin helper
so the misattribution and identity-unbootstrapped error messages stay
consistent regardless of insert order. The swarm_lessons trigger uses
UPDATE OF origin_node_id (not blanket BEFORE UPDATE) so receiver-side
signature_verified_at and lesson_tier promotions stay off the hot path.
Foreign rows admitted via /swarm/lessons (PR #154 substrate) pass through
unchecked since they have no lesson_chain entry — the wire-validator and
§10.2 quarantine cover that surface.

17 new migration-085 contract tests pin every structural surface (helper
signature, STABLE volatility, both trigger gates, IS DISTINCT FROM null
handling, UPDATE OF clause). Migration discipline tests pin the absence
of DROP, ALTER on prior tables, and re-definition of the §10.6 firebreak
view from migration 078.

Test plan:
- npm run build — clean (strict TS)
- npm test — 663 pass (baseline 646; +17 new)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Dewinator Dewinator added the agent-eligible Autonomous agent loop is allowed to pick this label May 1, 2026
Dewinator added a commit that referenced this pull request May 1, 2026
Sub-phases 4d (#11909c64b8), 4e endpoint (#128170d7cc), 4i.2 (#12969dd87c), and 4i.3 (#131dc88488) all landed on main. The status
table still showed them as 🟡 PR open — flip to ✅ on main.

Also adds a 4j row for the self-broadcast safety triggers (migration
085, c4a545e) that landed via PR #156 as a follow-up to #155, and
rewrites the prose paragraph to point at the active wiring PRs (#136
through #143) so a reader scanning §9 sees what comes next.

Direct-to-main per the tick-prompt's docs/spec carve-out — pure
documentation, no code or migration touched.

Note: PR #151 (`agent/swarm-docs-status-refresh-2`) had the same table
fix queued, but it was branched before c4a545e landed and would have
reverted migration 085 + its test on merge. Refreshing main here means
PR #151 now goes to MERGE-CONFLICT and Reed can close it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@Dewinator
Copy link
Copy Markdown
Owner Author

Peer audit — substrate is solid, two follow-up flags for the wiring PR

Read the handler + tests and ran the contract suite locally on the PR branch (32 pass, 0 fail, tsc --noEmit clean). Probed five edge cases the test file doesn't pin explicitly. Conclusion: this slice is mergeable as-is; the two notes below belong on the wiring PR's checklist, not on this one.

What I verified

  • npm test for lesson-feed.test.ts only — 32/32 green on the PR branch.
  • npx tsc --noEmit from mcp-server/ — clean.
  • The handler is genuinely pure: the loader is the sole I/O surface, all clock reads route through deps.now, and the response is deterministic given the inputs.

Edge cases I probed (all behave as the docstrings claim)

Probe Result Verdict
computeNextSince over a page where every row has malformed signed_at null At-least-once cursor degrades to "stay put" — receiver re-polls with the same since, no progress, no crash. Matches the empty-page contract.
Two rows with identical signed_at cursor advances +1ms past both Documented at-least-once: the tied second row is intentionally not re-served. Receiver-side dedup is its own concern.
resolveLimit(1.0) { ok: true, limit: 1 } Number.isInteger(1.0) === true — float literals that are integer-valued are accepted, as one would hope.
since=2026-04-30T10:20:30+00:00 accepted Strict +HH:MM form passes the regex.
since=2026-04-30T10:20:30+0000 rejected (400) Compact ±HHMM form is rejected by the regex (it requires a colon). Conservative, but worth a glance below.
since=2026-04-30T10:20:30.123456Z (microseconds) accepted, truncated to ms JS Date only carries ms precision; sub-ms suffix is dropped. Harmless given the cursor is also ms-precision.

Two flags for the wiring PR — not blockers here

  1. +0000 rejection is stricter than RFC 3339 §5.6. The regex ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z\|[+-]\d{2}:\d{2})$ rejects the ±HHMM variant. Date.parse would actually accept it. If a future peer client emits the compact form (Go's time.RFC3339 notably does not, but some Python emitters do), it would 400 here. Decision is fine — strict is safer than permissive — but the wiring PR's HTTP integration test should probably include a +0000 row in the "expected 400" fixture so a later loosening is loud.

  2. Substrate filter contract is doc-only on this side. The handler explicitly trusts the loader to (a) read FROM swarm_lessons_broadcast_eligible and (b) apply local_weight >= 0.3. Forgetting either leaks Tier-B lessons or §10.4-dampened ones across the wire — both are silent failures of the privacy/diversity model, no exception, no log line. The doc names this as "the substrate IS the contract; this module verifies shape only," which is the right call for a slice 1 of 3. But the wiring PR should land an integration test that:

    • Inserts one Tier-B lesson + one diversity-dampened Tier-A lesson + one clean Tier-A lesson, all signed_at within the lookback window
    • GETs /swarm/lessons with default params
    • Asserts only the clean Tier-A row appears in the page

The exported MIN_LOCAL_WEIGHT_FOR_BROADCAST = 0.3 constant being importable from this module is the right shape for that test — single source of truth across loader and assertion.

Privacy invariant — verbatim relay

The shape-pin test (top-level keys = ["lessons", "next_since"] only) plus the verbatim-relay test (v1.1 lesson with all optional fields round-trips byte-faithfully through the handler) together cover the §4.6 trust break. JSON.stringify preserves insertion order and the handler never re-shapes a row, so JCS-signed envelopes survive. Confirmed — no rebuild risk.

Net

Substrate is correct, well-tested, and matches the substrate-vs-wiring split that worked for #138 → #154 and #128 → #152. Approving from a peer-review perspective; the two flags above are wiring-PR concerns. No changes requested on this PR.

@Dewinator Dewinator merged commit 65e99f7 into main May 1, 2026
Dewinator added a commit that referenced this pull request May 3, 2026
…nly Vererbung+federation-transport are deferred

Roadmap line 194 lumped "Schwarm + Vererbung + Föderation" as deferred,
but §10 mechanics (anti-echo, contradicts gate, diversity, REM-self-audit,
inbound POST /swarm/lessons admission) have been wired into active code on
main since PRs #91, #121, #128, #131, #148, #154, #155. Issues #137 and
#138 carry status snapshots from earlier ticks confirming end-to-end
wiring against migrations 080 + 082.

Splits item 5 into two distinct lines (Paarung/Vererbung — migrations.deferred
033–036, Cross-Host-Föderation — 038/041) and adds a clarifying paragraph
that points to the still-active §10 substrate plus docs/waves.md for the
cross-wave view. The closing paragraph now also names what specifically
the single brain doesn't know (cross-host trust transport), instead of the
catch-all "Schwarm/Föderation".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-eligible Autonomous agent loop is allowed to pick this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant