Surface live peer hooks for Face social reads

This commit is contained in:
Meta Vens 2026-05-22 16:59:19 +01:00
commit fb8b9bd48d
2 changed files with 63 additions and 1 deletions

View file

@ -2201,17 +2201,23 @@ async function renderRepoFaceSocialOpportunitySurface(input: {
const typedState = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath });
const relations = collectRepoFaceSocialRelations(typedState);
const unmappedPeers = collectUnmappedSocialPeers(input.identity, input.registryIdentities, relations);
const messages = collectSocialOpportunityMessages(input);
const unmappedPeerHooks = collectUnmappedPeerSocialHooks({
identity: input.identity,
unmappedPeers,
messages,
});
if (relations.length === 0) {
return [
"No existing person-bond or person-status read matched this turn.",
"Use the transcript as evidence only when someone gives a real hook for trust, irritation, protection, rivalry, respect, suspicion, deference, or a careful question.",
...formatUnmappedPeerHookLines(unmappedPeerHooks),
unmappedPeers.length > 0
? `Unmapped registered peers: ${formatUnmappedPeers(unmappedPeers)}. Do not greet the whole roster at once.`
: "No peer roster was available for comparison this turn.",
].join("\n");
}
const messages = collectSocialOpportunityMessages(input);
const hookedRelations = relations
.map((relation) => ({
relation,
@ -2224,6 +2230,7 @@ async function renderRepoFaceSocialOpportunitySurface(input: {
return [
"Existing person-bonds/status reads have no matching speaker in the attached recent messages.",
"Treat the transcript itself as possible fresh social evidence, not as an obligation to invent a relationship.",
...formatUnmappedPeerHookLines(unmappedPeerHooks),
unmappedPeers.length > 0
? `Still-unmapped peers: ${formatUnmappedPeers(unmappedPeers)}. If one of them appears in the transcript, there is evidence available for a possible first read.`
: "No unmapped registered peers were found for this turn.",
@ -2239,6 +2246,7 @@ async function renderRepoFaceSocialOpportunitySurface(input: {
}
if (unmappedPeers.length > 0) {
lines.push(...formatUnmappedPeerHookLines(unmappedPeerHooks));
lines.push(`Unmapped registered peers: ${formatUnmappedPeers(unmappedPeers)}.`);
}
@ -2324,6 +2332,59 @@ function formatUnmappedPeers(peers: RepoDiscordIdentity[]): string {
return peers.map((peer) => `${peer.displayName}/${peer.repoName}`).join(", ");
}
function collectUnmappedPeerSocialHooks(input: {
identity: RepoDiscordIdentity;
unmappedPeers: RepoDiscordIdentity[];
messages: Array<{ label: string; message: SourceMessage }>;
}): Array<{ peer: RepoDiscordIdentity; hooks: Array<{ label: string; message: SourceMessage }> }> {
const selfTokens = new Set(socialTargetTokens(input.identity.displayName, input.identity.id, input.identity.repoName));
const peersByToken = new Map<string, RepoDiscordIdentity>();
for (const peer of input.unmappedPeers) {
for (const token of socialTargetTokens(peer.displayName, peer.id, peer.repoName)) {
if (!selfTokens.has(token)) {
peersByToken.set(token, peer);
}
}
}
const byPeer = new Map<string, { peer: RepoDiscordIdentity; hooks: Array<{ label: string; message: SourceMessage }> }>();
for (const entry of input.messages) {
if (!entry.message.isBot || !entry.message.content.trim()) {
continue;
}
const peer = peersByToken.get(normalizeSocialLabel(entry.message.authorName));
if (!peer) {
continue;
}
const bucket = byPeer.get(peer.id) ?? { peer, hooks: [] };
bucket.hooks.push(entry);
byPeer.set(peer.id, bucket);
}
return [...byPeer.values()]
.map((entry) => ({
peer: entry.peer,
hooks: entry.hooks.slice(-2),
}))
.sort((left, right) => left.peer.displayName.localeCompare(right.peer.displayName))
.slice(0, 5);
}
function formatUnmappedPeerHookLines(
peerHooks: Array<{ peer: RepoDiscordIdentity; hooks: Array<{ label: string; message: SourceMessage }> }>,
): string[] {
if (peerHooks.length === 0) {
return [];
}
return [
"Possible first-read hooks from unmapped peers who actually spoke nearby:",
...peerHooks.map((entry) =>
`- ${entry.peer.displayName}: ${entry.hooks.map(formatSocialOpportunityMessage).join(" | ")}`,
),
"Use one of these only if it sparks a real first read, question, tease, alliance, irritation, or rivalry. Do not manufacture intimacy from a single quote.",
];
}
function collectSocialOpportunityMessages(input: {
channelPlan: RepoFaceChannelPlan;
recentMessages: SourceMessage[];

View file

@ -269,3 +269,4 @@
{"ts":"2026-05-22T14:09:25Z","type":"implementation","status":"ok","note":"Expanded repo Face state projection input and projector contract so volatile state is not crushed before the child sees it. The state packet now exposes all non-retired affect needs, bonds, status reads, mood dimensions, agency pressure, activation profile, runtime speaking/rest pressure, durable memories, short-term residue, incubations, candidate speech pressure, and recent speech residue in character-facing terms. Nibu projected memory grew from ~231 words to ~1094 words, and a mock dry run passed with a more specific witness-as-caste Aetheria turn."}
{"ts":"2026-05-22T14:59:26Z","type":"implementation","status":"ok","note":"Repo Face state projection now imports existing interaction-memory pronoun guidance for visible humans and the owner profile, preventing projectors from guessing gender in relationship prose; Nibu projection smoke guards Metacrat she/her."}
{"ts":"2026-05-22T15:48:15Z","type":"implementation","status":"ok","note":"Repo Face article publishing now crosses the parent Interpreter as an ARTICLE DSL block instead of the legacy VOIDBOT_REPO_IDENTITY_ARTICLE JSON sentinel. Deterministic worker code renders and validates YAML frontmatter, strips model-supplied body frontmatter, routes Aetheria articles to Aetheria/Articles and non-Aetheria blog posts to gamecult-site GameCult/Blog, and smoke coverage verifies the contract."}
{"ts":"2026-05-22T15:59:05Z","type":"implementation","status":"ok","note":"Repo Face social affordances now include recent message hooks from unmapped registered peers who actually spoke nearby, instead of only listing peer names. A Libby-only dry run after the change routed cleanly and improved from zero social signal to five, with the Face forming live reads about Aqua/Epiphany rather than only repeating CultLib work doctrine."}