feat: add SenderName, WasMentioned to inbound context; preload group members#141
feat: add SenderName, WasMentioned to inbound context; preload group members#141
Conversation
…members - Add SenderName (from uidToNameMap) and SenderUsername (from_uid) to finalizeInboundContext so AI knows who's talking - Add WasMentioned flag for proper elevated/exec permission gating in group chats (OpenClaw framework uses this to control access) - Hoist isMentioned computation outside requireMention block - Opportunistic uid→name cache fill from reply.from_name and MultipleForward.users payloads (previously wasted data) - Preload group members on bot startup (alongside GROUP.md prefetch) for immediate SenderName resolution including DM from group users - Fix cleanupStaleCaches bug: remove incorrect uidToNameMap.delete(groupId) (uidToNameMap is a flat uid→name map, not keyed by groupId) Fixes sender identity gap where AI only saw raw UIDs. Group chat: immediate resolution via uidToNameMap. DM: resolved if user appeared in any group with the bot.
- Add extractBaseUid() to strip space prefix (s14_abc → abc)
- Add resolveSenderName() with 3-tier lookup:
1. Direct match: uidToNameMap.get(from_uid)
2. Base uid fallback: strip space prefix → lookup
3. Cross-space scan: find any s{X}_abc matching base uid
- Update buildSenderPrefix to use resolveSenderName (affects
history context display too)
- 12 new tests for extractBaseUid, resolveSenderName, and
cross-space buildSenderPrefix
DM coverage: user known in Space A group → resolved when DM
arrives from Space B (different prefix, same base uid).
…ervice_bot)
- Use regex /^s(\d+)_(.+)$/ to validate space prefix format s{digits}_{baseUid}
- Remove orphaned JSDoc comment
- Add test: service_bot, support_team should not be stripped
- Add fetchUserInfo() in api-fetch.ts — calls GET /v1/bot/user/info?uid=xxx - Graceful degradation: returns null on 404 (endpoint not yet implemented), 500 errors, or network failures — no crash, just falls back to raw uid - Wire into inbound.ts: for DM messages where resolveSenderName() misses, call fetchUserInfo() and cache result in uidToNameMap - 5 new tests covering success, 404, 500, network error, and empty response This closes the last SenderName blind spot: DM users who never appeared in any group will be resolved once the backend implements the endpoint. Until then, the adapter silently falls back to raw uid (existing behavior).
When fetchUserInfo returns null (user not found or endpoint unavailable), cache empty string as sentinel so subsequent DMs from the same unknown UID skip the API call. resolveSenderName already treats empty string as no-match.
Jerry-Xin
left a comment
There was a problem hiding this comment.
Nice work adding SenderName, WasMentioned, and the member-preloading infrastructure. The overall approach is solid — opportunistic cache filling from multiple sources (MultipleForward payloads, reply metadata, prefetch) is a smart pattern that makes the name resolution robust without adding extra API calls in the hot path.
A few items to address below, one is a definite bug (malformed JSDoc), and the rest are suggestions / observations.
| /** | ||
| * Build a sender label in the format "displayName(uid)" for history context. | ||
| * Falls back to just uid if no name is found. | ||
| /** |
There was a problem hiding this comment.
Bug: There's a duplicate /** here. Line 321 has a stray /** left over from the old buildSenderPrefix JSDoc block that was removed. The result is malformed JSDoc:
/**
/**
* Extract the base uid ...
The first /** on line 321 should be removed.
| if (baseHit) return baseHit; | ||
|
|
||
| // Scan for any space-prefixed variant with the same base uid | ||
| for (const [uid, name] of uidToNameMap) { |
There was a problem hiding this comment.
Nit / future concern: This for...of scan is O(n) over the entire uidToNameMap. Right now the map is probably small enough that it doesn't matter, but if the bot joins many large groups and the prefetch fills thousands of entries, this could become noticeable — it runs on every inbound message where the direct lookup misses.
Worth either:
- Adding a brief comment documenting the O(n) trade-off so future readers know it's intentional, or
- Building a small reverse index (
baseUid → name) alongsideuidToNameMapwhen entries are inserted, so this lookup stays O(1).
No rush — just something to keep in mind if the map grows.
| uidToNameMap.set(message.from_uid, userInfo.name); | ||
| } else { | ||
| // Negative cache — prevent repeated API calls for unknown UIDs | ||
| uidToNameMap.set(message.from_uid, ""); |
There was a problem hiding this comment.
Minor edge case worth noting: the negative cache uses "" (empty string) as a sentinel, and resolveSenderName treats any falsy value from uidToNameMap.get() as "not found" (line 345: if (direct) return direct). This works correctly end-to-end because the caller checks cached === undefined (line 1327) before the fetchUserInfo path.
However, there's a subtle interaction: if resolveSenderName is called for a uid with a "" sentinel, the direct lookup falls through (falsy), and the cross-space scan could match the same entry and return "". The caller sees falsy senderName, enters the DM block, and cached === undefined correctly prevents re-fetching — so it's fine.
But it's a bit fragile. Consider using a dedicated sentinel (e.g. a separate Set<string> of failed UIDs) to make the negative-cache intent explicit and decouple it from resolveSenderName's truthiness checks. Not blocking, just a resilience suggestion.
| } catch { | ||
| // Ignore per-group failures (group may not have GROUP.md) | ||
| } | ||
| // Prefetch group members → fill uidToNameMap for SenderName resolution |
There was a problem hiding this comment.
Nice — prefetching group members on startup combined with the opportunistic fills from MultipleForward payloads and reply metadata in inbound.ts means the name cache will be well-populated by the time messages arrive. Solid pattern.
| if (isGroup && requireMention) { | ||
| // Debug: log received mention info | ||
| log?.debug?.(`dmwork: [RECV] mention payload: uidsCount=${mentionUids.length}, all=${mentionAll}, originalCount=${originalMentionUids.length}`); | ||
| log?.debug?.(`dmwork: [RECV] mention payload: isMentioned=${isMentioned}, originalCount=${originalMentionUids.length}`); |
There was a problem hiding this comment.
Minor: the old debug log included uidsCount and all which was useful for diagnosing mention-gating edge cases (e.g. "why was this message gated even though all=true?"). The new log with just isMentioned loses that granularity.
Since mentionUids and mentionAll are now scoped inside the if (isGroup) block above, they're not accessible here. Consider either hoisting them or re-referencing from the payload:
log?.debug?.(`dmwork: [RECV] mention payload: isMentioned=${isMentioned}, all=${message.payload?.mention?.all}, originalCount=${originalMentionUids.length}`);
Problem
DMWork adapter only sets SenderId in finalizeInboundContext. AI sees raw UIDs, not display names.
Changes (+163/-12, 4 files)
Commit 1: Core identity fields + preload
Commit 2: Cross-space DM resolution
296 tests pass. Fixes #142