Phase 434: home-mode mention ingest and notification error logging (v0.8.397).

Persist guild inbound handler as ingestInboundGuildMessage for home view, resolve mentions from DB members when guild mesh is detached, and add structured error logs for mark-all, open notification, and mark-channel-read.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-23 18:09:41 -04:00
co-authored by Cursor
parent 23c10599d7
commit da8541dd0c
2 changed files with 88 additions and 34 deletions
+2
View File
@@ -135,6 +135,8 @@ Primary consumer: `apps/pearcord/index.js` sidecar reading pear-pipe JSON.
**v0.8.115:** `loadGuild` / `_openGuild` clears stale `activeChannelId`, then `_selectGuildChannelWithUnread` picks mention-weighted unread channels before falling back to the first text channel. `setSearch` records `searchRecentByGuild` in user prefs. View: `searchRecentQueries`.
**v0.8.397:** `ingestInboundGuildMessage(msg)` processes guild mesh gossip while on home (mention alerts + inbox via DB member lookup). `markAllNotificationsRead` / `openNotificationTarget` / `markChannelRead` log structured errors. Smoke helper: `simulateGuildMessage(platform, msg)` in `smoke-runner.cjs`.
**v0.8.252:** Guild search mesh skipped when `guildOpenNoViewableChannels`; emoji batch prefetch skips negative miss TTL; sticker gossip prefetch gated on `attachments`; `session.startup` logs INFO when initial guild mesh flush was bounded. Smoke: `test:guild-search-no-viewable-skip`, `test:sticker-prefetch-attachments-ready`.
**v0.8.248:** Guild open filters unread/last picks with `_canViewChannel`; sets `guildOpenNoViewableChannels` when none accessible; defers `_ensureGuildModules` via `setTimeout(0)` after channel pick; emoji mesh prefetch clears miss cache on success. Smokes: `test:guild-viewable-channel-pick`, `test:guild-open-no-viewable`, `test:guild-plain-beats-dual-combo`.
+86 -34
View File
@@ -880,6 +880,12 @@ class PearcordPlatform extends EventEmitter {
return { mode: 'home' }
}
/** Ingest a guild message from mesh gossip (works while on home after join). */
async ingestInboundGuildMessage (msg) {
if (!this._ingestInboundGuildMessage) throw new Error('guild message handler unavailable')
return this._ingestInboundGuildMessage(msg)
}
async _initDmSession () {
const user = this.identity.user
if (!user) throw new Error('register first')
@@ -2909,7 +2915,7 @@ class PearcordPlatform extends EventEmitter {
}
_wireGuild (guildInstance) {
guildInstance.setMessageHandler(async (msg) => {
this._ingestInboundGuildMessage = async (msg) => {
const row = enrichMessageRow(msg)
const existing = await this.db.get(COLLECTIONS.MESSAGES, {
channelId: row.channelId,
@@ -2920,14 +2926,15 @@ class PearcordPlatform extends EventEmitter {
await this._onInboundMessage(row)
this._queueLinkEmbed(row).catch(() => {})
const ch = await this.db.get(COLLECTIONS.CHANNELS, {
guildId: this.guild?.guild?.id,
guildId: row.guildId || this.guild?.guild?.id,
id: row.channelId
})
if (ch) this._indexMessageForSearch(row, ch)
this._fanoutBotEvent(BOT_EVENTS.MESSAGE_CREATE, { message: row })
this.emit('message', row)
return row
})
}
guildInstance.setMessageHandler(this._ingestInboundGuildMessage)
guildInstance.on('message', (msg) => this.emit('message', msg))
guildInstance.on('message-delete', (p) => this.emit('message-delete', p))
guildInstance.on('presence', (payload) => {
@@ -3340,7 +3347,9 @@ class PearcordPlatform extends EventEmitter {
async _onInboundMessage (msg) {
const user = this.identity.user
if (!user || msg.authorId === user.id || !msg.guildId) return
const members = this.guild ? await this.guild.listMembers() : []
const members = this.guild
? await this.guild.listMembers()
: await this.db.find(COLLECTIONS.MEMBERS, { guildId: msg.guildId })
const usersById = await this._usersById(members)
const plain = this._messagePlaintext(msg)
const mentioned = resolveMentionedUserIds(plain, members, usersById)
@@ -3520,7 +3529,18 @@ class PearcordPlatform extends EventEmitter {
async markAllNotificationsRead () {
if (!this.notifications) return 0
return this.notifications.markAllRead()
const span = this.log.time('notification.markAll')
try {
const count = await this.notifications.markAllRead()
span.end({ count })
return count
} catch (err) {
this.log.error('notification.markAll error', {
err: err?.message || String(err)
})
span.fail(err)
throw err
}
}
async listAuditLog (limit = 50, opts = {}) {
@@ -6465,16 +6485,39 @@ class PearcordPlatform extends EventEmitter {
}
async openNotificationTarget ({ guildId, channelId, messageId } = {}) {
if (guildId && this.guild?.guild?.id !== guildId) {
await this.loadGuild(guildId)
const span = this.log.time('notification.open', {
guildId: guildId || null,
channelId: channelId || null,
messageId: messageId || null
})
try {
if (guildId && this.guild?.guild?.id !== guildId) {
await this.loadGuild(guildId)
}
if (channelId && this.activeChannelId !== channelId) {
await this.selectChannel(channelId)
}
let result
if (messageId) {
result = await this.jumpToMessage(messageId)
} else {
result = { found: !!channelId, channelId: channelId || this.activeChannelId }
}
span.end({
found: !!result?.found,
channelId: result?.channelId || channelId || this.activeChannelId
})
return result
} catch (err) {
this.log.error('notification.open error', {
guildId: guildId || null,
channelId: channelId || null,
messageId: messageId || null,
err: err?.message || String(err)
})
span.fail(err)
throw err
}
if (channelId && this.activeChannelId !== channelId) {
await this.selectChannel(channelId)
}
if (messageId) {
return this.jumpToMessage(messageId)
}
return { found: !!channelId, channelId: channelId || this.activeChannelId }
}
formatGuildDeepLink (opts = {}) {
@@ -6956,29 +6999,38 @@ class PearcordPlatform extends EventEmitter {
async markChannelRead (channelId, guildId) {
const user = this.identity.user
if (!user || !channelId || !guildId) return
const at = Date.now()
await this.db.insert(COLLECTIONS.READ_STATE, {
userId: user.id,
guildId,
channelId,
lastReadAt: at
})
if (guildId === DM_GUILD_ID && this.dm?.channel) {
this.dm.gossipReadReceipt({
channelId,
try {
const at = Date.now()
await this.db.insert(COLLECTIONS.READ_STATE, {
userId: user.id,
guildId,
channelId,
lastReadAt: at
})
}
await this.db.insert(COLLECTIONS.MENTION_ALERTS, {
userId: user.id,
guildId,
channelId,
count: 0,
updatedAt: Date.now()
})
if (this.notifications) {
await this.notifications.clearForChannel(guildId, channelId)
if (guildId === DM_GUILD_ID && this.dm?.channel) {
this.dm.gossipReadReceipt({
channelId,
userId: user.id,
lastReadAt: at
})
}
await this.db.insert(COLLECTIONS.MENTION_ALERTS, {
userId: user.id,
guildId,
channelId,
count: 0,
updatedAt: Date.now()
})
if (this.notifications) {
await this.notifications.clearForChannel(guildId, channelId)
}
} catch (err) {
this.log.error('inbox.mark-read error', {
channelId,
guildId,
err: err?.message || String(err)
})
throw err
}
}