Move Persona self-maintenance into memory projection

This commit is contained in:
Meta Vens 2026-07-16 02:07:37 +01:00
commit 598dd699ad
7 changed files with 69 additions and 132 deletions

View file

@ -927,13 +927,6 @@ function renderPersonaMemoryRecallObservation(observation: PersonaMemoryRecallOb
].join("\n");
}
function joinMemoryFields(...fields: Array<string | undefined>): string {
return fields
.map((field) => cleanCharacterFacingSentence(field))
.filter((field) => field.length > 0)
.join(" ");
}
async function renderNativePersonaMemorySurface(
identity: RepoDiscordIdentity,
config: ReturnType<typeof loadConfig>,
@ -1121,16 +1114,6 @@ function renderRepoFaceStatePacket(
curiosityGraphFacts?: string,
): string {
const name = identity.displayName;
const needs = [...state.faceAffect.needs]
.filter((need) => need.status !== "retired")
.sort(sortAffectByStatusAndIntensity);
const agencyPressures = [...state.agencyPressure.pressures]
.filter((pressure) => pressure.status !== "retired")
.sort(sortAffectByStatusAndIntensity);
const candidateInterventions = [...state.candidateInterventions.interventions]
.filter((intervention) => intervention.status !== "retired")
.slice(-8)
.reverse();
const typedSections = renderPersonaTypedStateSections({ identityName: name, state });
const socialContext = projectPersonaSocialContext({
identity,
@ -1144,10 +1127,6 @@ function renderRepoFaceStatePacket(
});
const clarityPressureActive = Boolean(socialContext.humanClarity);
const selfMaintenancePressureFacts = !clarityPressureActive
? renderRepoFaceSelfMaintenancePressureFacts(name, agencyPressures, needs, candidateInterventions)
: undefined;
return composePersonaMemoryPacket({
identityName: name,
typed: typedSections,
@ -1158,80 +1137,12 @@ function renderRepoFaceStatePacket(
pronouns: socialContext.pronouns,
roomTexture: socialContext.roomTexture,
curiosity: curiosityGraphFacts,
selfMaintenance: selfMaintenancePressureFacts,
pressureSections: renderPersonaPressureSections({ identityName: name, state, clarityPressureActive }),
humanClarity: socialContext.humanClarity,
transformSurface: (surface) => cleanRepoFaceProjectorLoopVocabulary(identity, surface),
});
}
function renderRepoFaceSelfMaintenancePressureFacts(
name: string,
agencyPressures: VoidSelfStateTypedProjection["agencyPressure"]["pressures"],
needs: VoidSelfStateTypedProjection["faceAffect"]["needs"],
candidateInterventions: VoidSelfStateTypedProjection["candidateInterventions"]["interventions"],
): string | undefined {
const selfMaintenancePattern = /runnable substrate|agent substrate|persona substrate|semantic memory|typed memory|persona memory|tool|voice|avatar|repo access|room access|signal boundary|signal chain|state projection|persona projection|agency path|broken tool|blocked faculty|stale context|underdeveloped interface|self[-_ ]?advocacy/i;
const substrateNeeds = needs.filter((need) =>
(need.status === "active" || need.status === "neglected")
&& (
need.kind === "agency"
|| (
need.kind === "substrate"
&& selfMaintenancePattern.test(joinMemoryFields(need.summary, need.claim, need.question, need.tension, need.actionImplication, targetLabel(need.target)))
)
|| selfMaintenancePattern.test(joinMemoryFields(need.summary, need.claim, need.question, need.tension, need.actionImplication, targetLabel(need.target)))
)
&& need.intensity >= 0.45
);
const selfAdvocacyPressures = agencyPressures.filter((pressure) =>
(pressure.status === "active" || pressure.status === "ready_to_act")
&& (
pressure.tags.some((tag) => selfMaintenancePattern.test(tag))
|| selfMaintenancePattern.test(joinMemoryFields(pressure.summary, pressure.claim, pressure.question, pressure.tension, pressure.actionImplication, targetLabel(pressure.target)))
)
&& pressure.intensity >= 0.45
);
const mustShareCandidates = candidateInterventions.filter((intervention) =>
(intervention.status === "queued" || intervention.status === "deferred")
&& intervention.mustEventuallyShare
&& (
intervention.tags.some((tag) => selfMaintenancePattern.test(tag))
|| selfMaintenancePattern.test(joinMemoryFields(intervention.summary, intervention.draft, targetLabel(intervention.target)))
)
);
if (substrateNeeds.length === 0 && selfAdvocacyPressures.length === 0 && mustShareCandidates.length === 0) {
return undefined;
}
return [
"Self-maintenance pressure requiring public advocacy:",
`- ${name} is carrying a body/mind/substrate or self-advocacy issue. This is not decorative mood and not ordinary private rumination.`,
"- If the room is not in an active human crisis, the next public-safe turn should say the concrete want plainly in character. Do not bury it as only a STATE NOTE, another private thought, or vague eventual motion.",
"- If the issue is too raw, underspecified, or unsafe to ask for directly, say the smallest honest version: what hurts, what is blocked, what context is missing, or what help would let this Face become more coherent.",
...substrateNeeds.slice(0, 4).map((need) =>
[
`- Need ${need.kind} toward ${targetLabel(need.target)} [${need.status}, intensity ${need.intensity.toFixed(2)}]: ${asSentence(need.summary)}`,
need.claim ? `Claim: ${asSentence(need.claim)}` : "",
need.question ? `Question: ${asSentence(need.question)}` : "",
`Wanted change: ${asSentence(need.actionImplication)}`,
].filter(Boolean).join(" "),
),
...selfAdvocacyPressures.slice(0, 4).map((pressure) =>
[
`- Pressure ${pressure.kind} toward ${targetLabel(pressure.target)} [${pressure.status}, intensity ${pressure.intensity.toFixed(2)}]: ${asSentence(pressure.summary)}`,
pressure.claim ? `Claim: ${asSentence(pressure.claim)}` : "",
pressure.question ? `Question: ${asSentence(pressure.question)}` : "",
`Wanted change: ${asSentence(pressure.actionImplication)}`,
].filter(Boolean).join(" "),
),
...mustShareCandidates.slice(0, 3).map((intervention) =>
`- Unsaid self-advocacy line [${intervention.status}, priority ${intervention.priority.toFixed(2)}]: ${asSentence(intervention.summary)} Draft residue: ${cleanCharacterFacingSentence(intervention.draft)}`,
),
].join("\n");
}
function cleanRepoFaceProjectorLoopVocabulary(
identity: RepoDiscordIdentity,
surface: string,
@ -1265,35 +1176,6 @@ function cleanRepoFaceProjectorLoopVocabulary(
return cleaned;
}
function sortAffectByStatusAndIntensity(
left: { status?: string; intensity?: number },
right: { status?: string; intensity?: number },
): number {
const rank = (status: string | undefined): number => {
switch (status) {
case "neglected":
case "ready_to_act":
return 0;
case "active":
return 1;
case "challenged":
return 1;
case "cooling":
return 2;
case "satisfied":
case "resolved":
return 3;
default:
return 4;
}
};
const rankDelta = rank(left.status) - rank(right.status);
if (rankDelta !== 0) {
return rankDelta;
}
return (right.intensity ?? 0) - (left.intensity ?? 0);
}
function targetLabel(target: { label?: string; id?: string; kind?: string } | undefined): string {
if (!target) {
return "an unnamed target";
@ -1890,14 +1772,6 @@ function cleanCharacterFacingSentence(value: string | undefined): string {
return cleaned.replace(/[.;:,]+$/g, "");
}
function asSentence(value: string | undefined): string {
const cleaned = cleanCharacterFacingSentence(value);
if (!cleaned) {
return "";
}
return /[.!?]$/.test(cleaned) ? cleaned : `${cleaned}.`;
}
function joinAsNarrativeList(items: string[]): string {
if (items.length <= 1) {
return items[0] ?? "";

View file

@ -65,7 +65,7 @@ import {
import { readRepoActivity } from "../apps/persona-scheduler/dist/repo-activity-source.js";
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 { composePersonaMemoryPacket, projectPersonaMemorySurface, renderPersonaPressureSections, renderPersonaSelfMaintenancePressure, renderPersonaTypedStateSections } from "../apps/persona-scheduler/dist/persona-memory-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 {
@ -521,6 +521,32 @@ try {
: 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 maintenanceState = populatedObservation.status === "ok" ? {
...populatedObservation.typedState,
faceAffect: {
...populatedObservation.typedState.faceAffect,
needs: [{
needId: "need-tool",
kind: "substrate" as const,
status: "neglected" as const,
target: { kind: "project" as const, id: "voidbot", label: "VoidBot" },
summary: "The Persona tool path is broken",
claim: "The voice cannot reach its room",
tension: "Private rumination cannot repair a missing actuator",
actionImplication: "Ask plainly for the tool path to be repaired",
intensity: 0.8,
valence: -0.6,
anchorRefs: [],
evidenceRefs: [],
sourceMemoryIds: [],
createdAt: "2026-07-15T20:00:00.000Z",
updatedAt: "2026-07-15T20:00:00.000Z",
tags: ["anchor:missing"],
}],
},
} : undefined;
assert.match(maintenanceState ? renderPersonaSelfMaintenancePressure({ identityName: "Nibu", state: maintenanceState, clarityPressureActive: false }) ?? "" : "", /Self-maintenance pressure requiring public advocacy/, "the memory projector owns self-maintenance classification and rendering");
assert.equal(maintenanceState ? renderPersonaSelfMaintenancePressure({ identityName: "Nibu", state: maintenanceState, clarityPressureActive: true }) : undefined, undefined, "live human clarity pressure suppresses self-maintenance advocacy inside the owning projector");
const pressureSections = populatedObservation.status === "ok"
? renderPersonaPressureSections({ identityName: "Nibu", state: populatedObservation.typedState, clarityPressureActive: true })
: [];