Complete Persona social context projection
This commit is contained in:
parent
93a62be0b8
commit
19cdd39bc2
7 changed files with 148 additions and 295 deletions
|
|
@ -63,7 +63,7 @@ import {
|
|||
type PersonaMemoryRecallObservation,
|
||||
} from "../apps/persona-scheduler/dist/persona-memory-context-source.js";
|
||||
import { composePersonaMemoryPacket, projectPersonaMemorySurface, renderPersonaPressureSections, renderPersonaTypedStateSections } from "../apps/persona-scheduler/dist/persona-memory-projector.js";
|
||||
import { collectPersonaSocialRelations, observePersonaRoomTexture, renderPersonaHumanClarityPressure, renderPersonaHumanPronounFacts, renderPersonaRelationshipFreshness, renderPersonaRoomTexture, renderPersonaRoomWeather, renderPersonaSocialGraph, type PersonaHumanPronounGuidance as RepoFaceHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
|
||||
import { observePersonaRoomTexture, projectPersonaSocialContext, renderPersonaHumanPronounFacts, renderPersonaRoomWeather, type PersonaHumanPronounGuidance as RepoFaceHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
|
||||
import {
|
||||
readDiscordActivitySnapshot,
|
||||
type IdleCoolingSnapshot,
|
||||
|
|
@ -1132,32 +1132,17 @@ function renderRepoFaceStatePacket(
|
|||
.slice(-8)
|
||||
.reverse();
|
||||
const typedSections = renderPersonaTypedStateSections({ identityName: name, state });
|
||||
const humanClarityFacts = roomContext
|
||||
? renderPersonaHumanClarityPressure({ identity, ...roomContext })
|
||||
: undefined;
|
||||
const clarityPressureActive = Boolean(humanClarityFacts);
|
||||
|
||||
const relationshipFreshnessFacts = renderPersonaRelationshipFreshness({
|
||||
identityName: name,
|
||||
state,
|
||||
const socialContext = projectPersonaSocialContext({
|
||||
identity,
|
||||
registryIdentities,
|
||||
state,
|
||||
recentMessages: roomContext?.recentMessages ?? [],
|
||||
channelSnapshots: roomContext?.channelSnapshots ?? [],
|
||||
pronounGuidance: humanPronounGuidance,
|
||||
observedAt: new Date(),
|
||||
topicAttractorFacts: roomContext ? renderRepoFaceTopicAttractorFacts(identity, roomContext.recentMessages) : undefined,
|
||||
});
|
||||
const socialGraphFacts = renderPersonaSocialGraph({ identity, registryIdentities, state });
|
||||
const peerOpeningFacts = roomContext
|
||||
? renderRepoFacePeerOpeningFacts(identity, registryIdentities, roomContext)
|
||||
: undefined;
|
||||
const socialPressureFacts = roomContext
|
||||
? renderRepoFaceRelationshipPressureFacts(identity, registryIdentities, state, roomContext)
|
||||
: undefined;
|
||||
const pronounFacts = renderPersonaHumanPronounFacts(humanPronounGuidance);
|
||||
const roomTextureFacts = roomContext
|
||||
? renderPersonaRoomTexture({
|
||||
identity,
|
||||
...roomContext,
|
||||
topicAttractorFacts: renderRepoFaceTopicAttractorFacts(identity, roomContext.recentMessages),
|
||||
})
|
||||
: undefined;
|
||||
const clarityPressureActive = Boolean(socialContext.humanClarity);
|
||||
|
||||
const selfMaintenancePressureFacts = !clarityPressureActive
|
||||
? renderRepoFaceSelfMaintenancePressureFacts(name, agencyPressures, needs, candidateInterventions)
|
||||
|
|
@ -1166,16 +1151,16 @@ function renderRepoFaceStatePacket(
|
|||
return composePersonaMemoryPacket({
|
||||
identityName: name,
|
||||
typed: typedSections,
|
||||
relationshipFreshness: relationshipFreshnessFacts,
|
||||
socialGraph: socialGraphFacts,
|
||||
peerOpening: peerOpeningFacts,
|
||||
socialPressure: socialPressureFacts,
|
||||
pronouns: pronounFacts,
|
||||
roomTexture: roomTextureFacts,
|
||||
relationshipFreshness: socialContext.relationshipFreshness,
|
||||
socialGraph: socialContext.socialGraph,
|
||||
peerOpening: socialContext.peerOpening,
|
||||
socialPressure: socialContext.socialPressure,
|
||||
pronouns: socialContext.pronouns,
|
||||
roomTexture: socialContext.roomTexture,
|
||||
curiosity: curiosityGraphFacts,
|
||||
selfMaintenance: selfMaintenancePressureFacts,
|
||||
pressureSections: renderPersonaPressureSections({ identityName: name, state, clarityPressureActive }),
|
||||
humanClarity: humanClarityFacts,
|
||||
humanClarity: socialContext.humanClarity,
|
||||
transformSurface: (surface) => cleanRepoFaceProjectorLoopVocabulary(identity, surface),
|
||||
});
|
||||
}
|
||||
|
|
@ -1800,261 +1785,6 @@ function formatSignal(value: number): string {
|
|||
return `low ${value.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function renderRepoFacePeerOpeningFacts(
|
||||
identity: RepoDiscordIdentity,
|
||||
registryIdentities: RepoDiscordIdentity[],
|
||||
roomContext: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
},
|
||||
): string | undefined {
|
||||
const selfTokens = new Set(socialTargetTokens(identity.displayName, identity.id, identity.repoName));
|
||||
const peersByToken = new Map<string, RepoDiscordIdentity>();
|
||||
for (const peer of registryIdentities) {
|
||||
for (const token of socialTargetTokens(peer.displayName, peer.id, peer.repoName)) {
|
||||
if (!selfTokens.has(token)) {
|
||||
peersByToken.set(token, peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entries: Array<{ label: string; message: SourceMessage }> = [
|
||||
...roomContext.recentMessages.map((message) => ({ label: "current room", message })),
|
||||
...roomContext.channelSnapshots.flatMap((snapshot) =>
|
||||
snapshot.messages.map((message) => ({ label: "nearby room", message })),
|
||||
),
|
||||
];
|
||||
const byPeer = new Map<string, { peer: RepoDiscordIdentity; entries: Array<{ label: string; message: SourceMessage }> }>();
|
||||
|
||||
for (const entry of entries) {
|
||||
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, entries: [] };
|
||||
bucket.entries.push(entry);
|
||||
byPeer.set(peer.id, bucket);
|
||||
}
|
||||
|
||||
const peerFacts = [...byPeer.values()]
|
||||
.sort((left, right) => right.entries.length - left.entries.length)
|
||||
.slice(0, 6)
|
||||
.map(({ peer, entries }) => {
|
||||
const latest = entries.at(-1);
|
||||
const channelLabels = [...new Set(entries.map((entry) => entry.label))].join(", ");
|
||||
const excerpt = latest ? collapseWhitespace(latest.message.content, 180) : "";
|
||||
return `- ${peer.displayName}: ${entries.length} recent nearby message${entries.length === 1 ? "" : "s"} in ${channelLabels}. Latest visible line: "${excerpt}"`;
|
||||
});
|
||||
|
||||
if (peerFacts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
"Recent peer openings for possible social reads:",
|
||||
...peerFacts,
|
||||
"These are raw openings for the projector to translate into possible trust, irritation, rivalry, alliance, or no social move at all. Do not treat them as consensus.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderRepoFaceRelationshipPressureFacts(
|
||||
identity: RepoDiscordIdentity,
|
||||
registryIdentities: RepoDiscordIdentity[],
|
||||
state: VoidSelfStateTypedProjection,
|
||||
roomContext: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
},
|
||||
): string | undefined {
|
||||
const selfTokens = socialPressureTokensForIdentity(identity);
|
||||
const jurisdictionTokens = socialPressureJurisdictionTokens(identity);
|
||||
const peerProfiles = registryIdentities
|
||||
.filter((peer) => normalizeSocialLabel(peer.id) !== normalizeSocialLabel(identity.id))
|
||||
.map((peer) => ({
|
||||
identity: peer,
|
||||
tokens: socialPressureTokensForIdentity(peer),
|
||||
}));
|
||||
const relationTargets = collectPersonaSocialRelations(state)
|
||||
.map((relation) => ({
|
||||
label: relation.targetLabel,
|
||||
tokens: socialPressureTokens(relation.targetLabel),
|
||||
}))
|
||||
.filter((relation) => relation.tokens.length > 0);
|
||||
const entries: Array<{ label: string; message: SourceMessage }> = [
|
||||
...roomContext.recentMessages.map((message) => ({ label: "current room", message })),
|
||||
...roomContext.channelSnapshots.flatMap((snapshot) =>
|
||||
snapshot.messages.map((message) => ({ label: "nearby room", message })),
|
||||
),
|
||||
];
|
||||
const byId = new Map<string, {
|
||||
label: string;
|
||||
message: SourceMessage;
|
||||
score: number;
|
||||
signals: string[];
|
||||
}>();
|
||||
|
||||
for (const entry of entries) {
|
||||
const content = collapseWhitespace(entry.message.content, 10_000);
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
const normalizedContent = normalizeSocialLabel(content);
|
||||
const authorToken = normalizeSocialLabel(entry.message.authorName ?? entry.message.authorId);
|
||||
const signals: string[] = [];
|
||||
let score = 0;
|
||||
|
||||
const authorIsSelf = tokenAppearsInNormalizedText(authorToken, selfTokens);
|
||||
const contentNamesSelf = tokenAppearsInNormalizedText(normalizedContent, selfTokens);
|
||||
if (contentNamesSelf) {
|
||||
score += 3;
|
||||
signals.push(`names ${identity.displayName}`);
|
||||
} else if (authorIsSelf) {
|
||||
score += 1;
|
||||
signals.push(`${identity.displayName}'s own recent line`);
|
||||
}
|
||||
|
||||
const peerMatches = peerProfiles
|
||||
.filter((peer) =>
|
||||
tokenAppearsInNormalizedText(authorToken, peer.tokens) ||
|
||||
tokenAppearsInNormalizedText(normalizedContent, peer.tokens),
|
||||
)
|
||||
.slice(0, 3);
|
||||
if (peerMatches.length > 0) {
|
||||
score += peerMatches.length;
|
||||
signals.push(`touches peer ${peerMatches.map((peer) => peer.identity.displayName).join("/")}`);
|
||||
}
|
||||
|
||||
const relationMatches = relationTargets
|
||||
.filter((relation) =>
|
||||
tokenAppearsInNormalizedText(authorToken, relation.tokens) ||
|
||||
tokenAppearsInNormalizedText(normalizedContent, relation.tokens),
|
||||
)
|
||||
.slice(0, 3);
|
||||
if (relationMatches.length > 0) {
|
||||
score += relationMatches.length;
|
||||
signals.push(`touches existing social target ${relationMatches.map((relation) => relation.label).join("/")}`);
|
||||
}
|
||||
|
||||
if (tokenAppearsInNormalizedText(normalizedContent, jurisdictionTokens)) {
|
||||
score += 1;
|
||||
signals.push("touches this jurisdiction or its domain language");
|
||||
}
|
||||
|
||||
const socialPressureKinds = socialPressureLanguageKinds(content);
|
||||
if (socialPressureKinds.length > 0) {
|
||||
score += 2;
|
||||
signals.push(`uses social/status language (${socialPressureKinds.join(", ")})`);
|
||||
}
|
||||
|
||||
if (!entry.message.isBot && score > 0) {
|
||||
score += 1;
|
||||
signals.push("human voice");
|
||||
}
|
||||
|
||||
if (score < 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = byId.get(entry.message.id);
|
||||
if (!existing || score > existing.score) {
|
||||
byId.set(entry.message.id, {
|
||||
label: entry.label,
|
||||
message: entry.message,
|
||||
score,
|
||||
signals: [...new Set(signals)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const facts = [...byId.values()]
|
||||
.sort((left, right) => {
|
||||
const leftMs = Date.parse(left.message.timestamp);
|
||||
const rightMs = Date.parse(right.message.timestamp);
|
||||
if (Number.isFinite(leftMs) && Number.isFinite(rightMs) && leftMs !== rightMs) {
|
||||
return leftMs - rightMs;
|
||||
}
|
||||
return left.message.id.localeCompare(right.message.id);
|
||||
})
|
||||
.slice(-8);
|
||||
|
||||
if (facts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
"Recent relationship-pressure evidence:",
|
||||
...facts.map((fact) => {
|
||||
const speaker = fact.message.isBot ? `${fact.message.authorName} (agent/bot)` : fact.message.authorName;
|
||||
return `- [${fact.label}] ${speaker} said: "${collapseWhitespace(fact.message.content, 260)}" Signals: ${fact.signals.join("; ")}.`;
|
||||
}),
|
||||
"These are raw provocations, not settled memories. Project them as tentative felt pressure only where this character's values, territory, current mood, or existing relationships make them matter.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function socialPressureTokensForIdentity(identity: RepoDiscordIdentity): string[] {
|
||||
return socialPressureTokens(identity.displayName, identity.id, identity.repoName);
|
||||
}
|
||||
|
||||
function socialPressureJurisdictionTokens(identity: RepoDiscordIdentity): string[] {
|
||||
return socialPressureTokens(
|
||||
identity.repoName,
|
||||
identity.displayName,
|
||||
identity.description,
|
||||
...identity.channelPermissions.flatMap((permission) => [permission.label, permission.topic]),
|
||||
)
|
||||
.filter((token) => token.length >= 5);
|
||||
}
|
||||
|
||||
function socialPressureTokens(...values: Array<string | undefined>): string[] {
|
||||
const tokens = new Set<string>();
|
||||
for (const value of values) {
|
||||
const normalized = normalizeSocialLabel(value);
|
||||
if (normalized.length >= 3) {
|
||||
tokens.add(normalized);
|
||||
}
|
||||
for (const part of splitSocialPressureWords(value)) {
|
||||
const token = normalizeSocialLabel(part);
|
||||
if (token.length >= 4) {
|
||||
tokens.add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...tokens];
|
||||
}
|
||||
|
||||
function splitSocialPressureWords(value: string | undefined): string[] {
|
||||
return (value ?? "")
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.split(/[^A-Za-z0-9]+/)
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function tokenAppearsInNormalizedText(normalizedText: string, tokens: string[]): boolean {
|
||||
return tokens.some((token) => token.length > 0 && normalizedText.includes(token));
|
||||
}
|
||||
|
||||
function socialPressureLanguageKinds(content: string): string[] {
|
||||
const text = content.toLowerCase();
|
||||
const kinds: string[] = [];
|
||||
const groups: Array<[string, RegExp]> = [
|
||||
["status", /\b(status|standing|rank|hierarchy|authority|overbearing|defer(?:red|ring)?|challenge[ds]?|humiliat(?:e|ed|ing)|respect)\b/],
|
||||
["territory", /\b(turf|jurisdiction|steward(?:ship)?|custody|owner|ownership|belongs?|domain|lane|stepp(?:ed|ing)? on)\b/],
|
||||
["consultation", /\b(consult(?:ed|ation|ing)?|ask(?:ed|ing)?|permission|bypass(?:ed|ing)?|decorative|flavo[u]?r theater|rubber[- ]?stamp)\b/],
|
||||
["affiliation", /\b(friend(?:ship)?|rival(?:ry)?|alliance|resent(?:ment|s|ed|ing)?|trust|protect(?:ion|ive)?|envy|jealous|wrapped around)\b/],
|
||||
["attention", /\b(attention|ignored|neglected|noticed|summon(?:ed|s)?|called out|directly challenged|approval)\b/],
|
||||
];
|
||||
for (const [kind, pattern] of groups) {
|
||||
if (pattern.test(text)) {
|
||||
kinds.push(kind);
|
||||
}
|
||||
}
|
||||
return kinds;
|
||||
}
|
||||
|
||||
async function loadRepoFaceHumanPronounGuidance(
|
||||
config: ReturnType<typeof loadConfig>,
|
||||
roomContext?: {
|
||||
|
|
@ -2141,10 +1871,6 @@ function pronounEvidenceRank(profile: InteractionMemoryProfile, entry: Interacti
|
|||
return resolvedSetBonus + (sourceRank[entry.source] ?? 0) + stanceBonus + confidenceBonus + recencyBonus;
|
||||
}
|
||||
|
||||
function socialTargetTokens(...values: Array<string | undefined>): string[] {
|
||||
return [...new Set(values.map(normalizeSocialLabel).filter((value) => value.length > 0))];
|
||||
}
|
||||
|
||||
function normalizeSocialLabel(value: string | undefined): string {
|
||||
return (value ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ import { readRepoActivity } from "../apps/persona-scheduler/dist/repo-activity-s
|
|||
import { readPersonaStateObservation } from "../apps/persona-scheduler/dist/persona-state-source.js";
|
||||
import { readPersonaMemoryRecall } from "../apps/persona-scheduler/dist/persona-memory-context-source.js";
|
||||
import { composePersonaMemoryPacket, projectPersonaMemorySurface, renderPersonaPressureSections, renderPersonaTypedStateSections } from "../apps/persona-scheduler/dist/persona-memory-projector.js";
|
||||
import { observePersonaRoomTexture, renderPersonaHumanClarityPressure, renderPersonaHumanPronounFacts, renderPersonaRoomWeather, renderPersonaSocialGraph } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
|
||||
import { observePersonaRoomTexture, projectPersonaSocialContext, renderPersonaHumanClarityPressure, renderPersonaHumanPronounFacts, renderPersonaRoomWeather, renderPersonaSocialGraph } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
|
||||
|
||||
function participant(identityId: string, nextTurnAt: number): InitiativeParticipant {
|
||||
return {
|
||||
|
|
@ -505,6 +505,22 @@ try {
|
|||
? renderPersonaSocialGraph({ identity: routedIdentity, registryIdentities: [routedIdentity], state: populatedObservation.typedState })
|
||||
: undefined;
|
||||
assert.match(socialGraph ?? "", /Social graph topology:/, "social topology projection consumes typed state and the explicit registry without acquiring either");
|
||||
const socialContext = populatedObservation.status === "ok"
|
||||
? projectPersonaSocialContext({
|
||||
identity: routedIdentity,
|
||||
registryIdentities: [routedIdentity, { ...routedIdentity, id: "peer", repoName: "PeerRepo", displayName: "Peer" }],
|
||||
state: populatedObservation.typedState,
|
||||
recentMessages: [
|
||||
{ id: "peer-opening", authorId: "peer", authorName: "Peer", content: "Nibu, the room is yours.", timestamp: "2026-07-15T21:01:00.000Z", isBot: true, channelId: "aquarium" },
|
||||
{ id: "human-pressure", authorId: "human", authorName: "Operator", content: "Nibu, your authority over this jurisdiction is being challenged.", timestamp: "2026-07-15T21:02:00.000Z", isBot: false, channelId: "aquarium" },
|
||||
],
|
||||
channelSnapshots: [],
|
||||
pronounGuidance: [],
|
||||
observedAt: new Date("2026-07-15T21:03:00.000Z"),
|
||||
})
|
||||
: undefined;
|
||||
assert.match(socialContext?.peerOpening ?? "", /Peer: 1 recent nearby message/, "the aggregate social projection owns peer openings");
|
||||
assert.match(socialContext?.socialPressure ?? "", /human voice/, "the aggregate social projection owns relationship-pressure scoring");
|
||||
const pressureSections = populatedObservation.status === "ok"
|
||||
? renderPersonaPressureSections({ identityName: "Nibu", state: populatedObservation.typedState, clarityPressureActive: true })
|
||||
: [];
|
||||
|
|
|
|||
Loading…
Reference in a new issue