openvibers/openvibe.network Commits

Network: the go-live consumer always delivers (the cutover flag held every go-live since 10-10) (#142)

OP OpenVibers committed 11 Oct 2026 · ea7d8f861a · 6 files, +8 −16

**A production bug, found while re-checking compatibility register C-85.** Followers received no go-live notifications from 2026-10-10 08:47 UTC.

**Cause:**
- Live runs `GOLIVE_NOTIFY=events`, which leaves go-lives to Network's `live.stream.started` consumer: "left to Network's live.stream.started consumer" in every go-live since then.
- Network's consumer acknowledged each one as `ignored:follows_cutover_pending`, because `NETWORK_GO_LIVE_FOLLOWS_READY` was never set. That was six go-lives on 10-10 alone, each logged by both sides.
- Each side was waiting for the other.

**Fixed in production first:**
- Network's follows are complete: 64 active channel follows across 13 channels, the same 64 as Live's table; Network has been the follows authority since 2026-09-26.
- So `NETWORK_GO_LIVE_FOLLOWS_READY=1` was set and Network restarted (2026-10-11 ~01:20 UTC). The next go-live is announced by Network: inbox, push, email and Discord.

**This PR (no-compat, convert-verify-delete):**
- The consumer always delivers; the flag and the `ignored:follows_cutover_pending` outcome go.
- The README describes the consumer as the only go-live path; `.env.example` drops the flag.
- Tests no longer pass the flag. The "pending" case is gone, and the delivery cases (followers notified once, mute and email preferences, the per-streamer window, a stale start announcing nothing) are unchanged and green.

Live removes its direct call and `GOLIVE_NOTIFY` next (C-85 closes).

`ov test`: events-consumer, follows-go-live and notification-events green.

Changes

ddiff --git a/.env.example b/.env.example
iindex 269b654b..6e66d9ca 100644
---- a/.env.example
++++ b/.env.example
@@@ -79,8 +79,6 @@ JWT_PUBLIC_KEY=data/keys/public.pem
# Connected services (internal, not proxied through Cloudflare)
OV_LIVE_INTERNAL_URL=http://127.0.0.1:3000
--# Enable only after Live writes follows to Network and the final Live snapshot is reconciled.
--#NETWORK_GO_LIVE_FOLLOWS_READY=1
OV_TOOLS_INTERNAL_URL=http://127.0.0.1:4001
OV_GAMES_INTERNAL_URL=http://127.0.0.1:8000
OV_MEDIA_INTERNAL_URL=http://127.0.0.1:4100
ddiff --git a/README.md b/README.md
iindex 070710b9..72944e4a 100644
---- a/README.md
++++ b/README.md
@@@ -287,11 +287,10 @@ notifications. Live is never asked. A channel whose subject has no Network accou
a follower whose subject has no account is counted as `unresolved`; the streamer never hears about
themself; the `stream` category's mute and email choice apply. A start more
than 30 minutes old (a replay, or Events catching up) announces nothing. The per-streamer window (one
--announcement an hour, eight a day; `stream_live_cooldown_min`, `stream_live_daily_cap`) is shared with
--Live's direct `POST /internal/events/stream-live` (`server/notifications/stream-live.js`), so while Live
--still makes that call, whichever arrives first announces and the other is skipped.
--Until `NETWORK_GO_LIVE_FOLLOWS_READY=1` is set on Network, Events go-lives are acknowledged as
--`ignored:follows_cutover_pending` and Live's direct POST remains the delivery path.
++announcement an hour, eight a day; `stream_live_cooldown_min`, `stream_live_daily_cap`) is kept by
++`server/notifications/stream-live.js`. This consumer is the only go-live path (compatibility register C-85): Live
++publishes `live.stream.started` and makes no direct call. Follows have been Network's since 2026-09-26, and the
++cutover flag that held the consumer back is gone.
**Project usage.** `tools.usage.recorded` and `events.usage.recorded` (hourly rollups of a developer
project's use, `common.usage-recorded@1`) are kept per project and day (`server/developer/usage.js`,
ddiff --git a/server/notifications/events-consumer.js b/server/notifications/events-consumer.js
iindex 6fbc9af6..1295e411 100644
---- a/server/notifications/events-consumer.js
++++ b/server/notifications/events-consumer.js
@@@ -160,7 +160,7 @@ function liveStarted(event, { now, maxAgeMs }) {
* @param {() => ({ sendLiveAlert(streamer: object, stream: object): Promise<object> }|null)} [o.discord]
* @param {{ record(event: object): string }} [o.projectUsage] ../developer/usage.js createProjectUsage()
*/
--async function createEventsConsumer({ db, notifications, secrets, discord = () => null, moderationAudit = null, projectUsage = null, liveStartedMaxAgeMs = LIVE_STARTED_MAX_AGE_MS, goLiveFollowersReady = process.env.NETWORK_GO_LIVE_FOLLOWS_READY === '1', now = () => Date.now(), log = console }) {
++async function createEventsConsumer({ db, notifications, secrets, discord = () => null, moderationAudit = null, projectUsage = null, liveStartedMaxAgeMs = LIVE_STARTED_MAX_AGE_MS, now = () => Date.now(), log = console }) {
const keys = Array.isArray(secrets) ? secrets.filter((s) => typeof s === 'string' && s.length >= 32) : secretsFrom(secrets);
const inbox = createPgInbox(db, { table: INBOX_TABLE, now });
const userBySubject = db.prepare('SELECT id FROM users WHERE subject_id = ?');
@@@ -169,8 +169,6 @@ async function createEventsConsumer({ db, notifications, secrets, discord = () =
/** live.stream.started inside the inbox transaction: the announcement window, then one notification per person. */
async function liveStreamStarted(event) {
-- // Live's direct POST remains the delivery path until its Network writes and the final reconciliation are complete.
-- if (!goLiveFollowersReady) return 'ignored:follows_cutover_pending';
const v = liveStarted(event, { now: now(), maxAgeMs: liveStartedMaxAgeMs });
if (typeof v === 'string') return v;
const streamer = await streamerBySubject.get(v.subject);
ddiff --git a/test/events-consumer.test.js b/test/events-consumer.test.js
iindex 8f6d84bd..828ecba4 100644
---- a/test/events-consumer.test.js
++++ b/test/events-consumer.test.js
@@@ -206,7 +206,7 @@ const rows = async (userId) => await db.prepare('SELECT * FROM notifications WHE
const discordCalls = [];
let clock = Date.now();
consumer = await createEventsConsumer({
-- db, notifications, secrets: SECRET, now: () => clock, goLiveFollowersReady: true,
++ db, notifications, secrets: SECRET, now: () => clock,
discord: () => ({ sendLiveAlert: async (streamer, stream) => { discordCalls.push([streamer, stream]); return { sent: true }; } }),
});
const liveEvent = (over = {}, payload = {}) => ({
@@@ -221,9 +221,6 @@ const rows = async (userId) => await db.prepare('SELECT * FROM notifications WHE
});
const goLives = async (uid) => await db.prepare("SELECT * FROM notifications WHERE user_id = ? AND type = 'STREAM_LIVE' ORDER BY seq").all(uid);
-- const pending = await createEventsConsumer({ db, notifications, secrets: SECRET, goLiveFollowersReady: false });
-- assert.strictEqual((await pending.apply(liveEvent())).outcome, 'ignored:follows_cutover_pending');
-- assert.strictEqual((await goLives(21)).length, 0, 'Network does not announce before Live follow writes are cut over');
// One delivery: Dave, Erin and Gina (all go-lives) are notified once each; Frank muted the category, Carol
// is the streamer, the stranger has no account, Hank unfollowed.
ddiff --git a/test/follows-go-live.test.js b/test/follows-go-live.test.js
iindex d7db832e..f8e1bfa9 100644
---- a/test/follows-go-live.test.js
++++ b/test/follows-go-live.test.js
@@@ -36,7 +36,7 @@ const follows = require('../server/identity/follows');
const principals = require('../server/identity/principals');
const SECRET = 's'.repeat(40);
const notifications = new NotificationService(db);
--const consumer = await createEventsConsumer({ db, notifications, secrets: SECRET, goLiveFollowersReady: true, log: { log() {}, warn() {}, error() {} } });
++const consumer = await createEventsConsumer({ db, notifications, secrets: SECRET, log: { log() {}, warn() {}, error() {} } });
const app = express();
Object.assign(app.locals, { db, config, privateKey: keys.privateKey, publicKey: keys.publicKey });
app.use(express.urlencoded({ extended: true }));
ddiff --git a/test/notification-events.test.js b/test/notification-events.test.js
iindex 01fadd27..53d95d91 100644
---- a/test/notification-events.test.js
++++ b/test/notification-events.test.js
@@@ -178,7 +178,7 @@ const checkEnvelope = (env, subject) => {
const d = getDb();
const n = new NotificationService(d);
const c = await createEventsConsumer({
-- db: d, notifications: n, secrets: SECRET, goLiveFollowersReady: true,
++ db: d, notifications: n, secrets: SECRET,
discord: () => ({ sendLiveAlert: async (s) => { discord.push(s.username); return { sent: true }; } }),
log: { log: (m) => logged.push(m), warn() {}, error() {} },
});