Move Persona self-maintenance into memory projection
This commit is contained in:
parent
19cdd39bc2
commit
598dd699ad
7 changed files with 69 additions and 132 deletions
|
|
@ -18,7 +18,8 @@ export function renderPersonaPressureSections(input: {
|
|||
const incubation = [...input.state.thoughtMemory.incubation].filter((entry) => entry.status !== "retired").sort((left, right) => right.maturation - left.maturation);
|
||||
const candidates = [...input.state.candidateInterventions.interventions].filter((entry) => entry.status !== "retired").slice(-8).reverse();
|
||||
const receipts = [...input.state.speechReceipts.recentReceipts].slice(-6).reverse();
|
||||
const sections: string[] = [];
|
||||
const selfMaintenance = renderPersonaSelfMaintenancePressure(input);
|
||||
const sections: string[] = selfMaintenance ? [selfMaintenance] : [];
|
||||
if (agency.length > 0) sections.push(clarityPressureActive
|
||||
? ["Agency pressures are currently demoted by live clarity pressure:", `- ${name} still has stored urges toward eventual motion, but the room has asked for plain understanding first. Do not expose the old detailed asks this turn; translate only the underlying value into simpler speech, repair, restraint, or silence.`].join("\n")
|
||||
: ["Agency pressures that want eventual motion:", ...agency.map((pressure) => [`- ${pressure.kind} toward ${targetLabel(pressure.target)} [${pressure.status}, intensity ${pressure.intensity.toFixed(2)}]: ${sentence(pressure.summary)}`, pressure.claim ? `Claim: ${sentence(pressure.claim)}` : "", pressure.question ? `Question: ${sentence(pressure.question)}` : "", pressure.tension ? `Tension: ${sentence(pressure.tension)}` : "", `Behavioral pull: ${sentence(pressure.actionImplication)}`].filter(Boolean).join(" "))].join("\n"));
|
||||
|
|
@ -34,6 +35,41 @@ export function renderPersonaPressureSections(input: {
|
|||
return sections;
|
||||
}
|
||||
|
||||
export function renderPersonaSelfMaintenancePressure(input: {
|
||||
identityName: string;
|
||||
state: VoidSelfStateTypedProjection;
|
||||
clarityPressureActive: boolean;
|
||||
}): string | undefined {
|
||||
if (input.clarityPressureActive) return undefined;
|
||||
const pattern = /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 fieldsMatch = (...fields: Array<string | undefined>): boolean => pattern.test(fields.map(cleanSentence).filter(Boolean).join(" "));
|
||||
const needs = [...input.state.faceAffect.needs].filter((need) =>
|
||||
(need.status === "active" || need.status === "neglected")
|
||||
&& (need.kind === "agency" || (need.kind === "substrate" && fieldsMatch(need.summary, need.claim, need.question, need.tension, need.actionImplication, targetLabel(need.target))) || fieldsMatch(need.summary, need.claim, need.question, need.tension, need.actionImplication, targetLabel(need.target)))
|
||||
&& need.intensity >= 0.45,
|
||||
).sort(sortAffect);
|
||||
const pressures = [...input.state.agencyPressure.pressures].filter((pressure) =>
|
||||
(pressure.status === "active" || pressure.status === "ready_to_act")
|
||||
&& (pressure.tags.some((tag) => pattern.test(tag)) || fieldsMatch(pressure.summary, pressure.claim, pressure.question, pressure.tension, pressure.actionImplication, targetLabel(pressure.target)))
|
||||
&& pressure.intensity >= 0.45,
|
||||
).sort(sortAffect);
|
||||
const candidates = [...input.state.candidateInterventions.interventions].filter((intervention) =>
|
||||
(intervention.status === "queued" || intervention.status === "deferred")
|
||||
&& intervention.mustEventuallyShare
|
||||
&& (intervention.tags.some((tag) => pattern.test(tag)) || fieldsMatch(intervention.summary, intervention.draft, targetLabel(intervention.target))),
|
||||
).slice(-8).reverse();
|
||||
if (needs.length === 0 && pressures.length === 0 && candidates.length === 0) return undefined;
|
||||
return [
|
||||
"Self-maintenance pressure requiring public advocacy:",
|
||||
`- ${input.identityName} 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.",
|
||||
...needs.slice(0, 4).map((need) => [`- Need ${need.kind} toward ${targetLabel(need.target)} [${need.status}, intensity ${need.intensity.toFixed(2)}]: ${sentence(need.summary)}`, need.claim ? `Claim: ${sentence(need.claim)}` : "", need.question ? `Question: ${sentence(need.question)}` : "", `Wanted change: ${sentence(need.actionImplication)}`].filter(Boolean).join(" ")),
|
||||
...pressures.slice(0, 4).map((pressure) => [`- Pressure ${pressure.kind} toward ${targetLabel(pressure.target)} [${pressure.status}, intensity ${pressure.intensity.toFixed(2)}]: ${sentence(pressure.summary)}`, pressure.claim ? `Claim: ${sentence(pressure.claim)}` : "", pressure.question ? `Question: ${sentence(pressure.question)}` : "", `Wanted change: ${sentence(pressure.actionImplication)}`].filter(Boolean).join(" ")),
|
||||
...candidates.slice(0, 3).map((intervention) => `- Unsaid self-advocacy line [${intervention.status}, priority ${intervention.priority.toFixed(2)}]: ${sentence(intervention.summary)} Draft residue: ${cleanSentence(intervention.draft)}`),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function composePersonaMemoryPacket(input: {
|
||||
identityName: string;
|
||||
typed: PersonaTypedStateSections;
|
||||
|
|
@ -44,7 +80,6 @@ export function composePersonaMemoryPacket(input: {
|
|||
pronouns?: string;
|
||||
roomTexture?: string;
|
||||
curiosity?: string;
|
||||
selfMaintenance?: string;
|
||||
pressureSections: string[];
|
||||
humanClarity?: string;
|
||||
transformSurface?: (surface: string) => string;
|
||||
|
|
@ -61,7 +96,6 @@ export function composePersonaMemoryPacket(input: {
|
|||
input.pronouns,
|
||||
input.roomTexture,
|
||||
input.curiosity,
|
||||
input.selfMaintenance,
|
||||
...input.pressureSections,
|
||||
input.humanClarity,
|
||||
].filter((section): section is string => Boolean(section));
|
||||
|
|
|
|||
|
|
@ -153,7 +153,8 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- Semantic Persona-memory recall is isolated in `persona-memory-context-source.ts`. It requires an existing `PersonaStateObservation`, rebuilds only that Persona's derived vector partition, searches it against current/nearby room evidence, and returns structured hits or an explicit unavailable result. It cannot reopen canonical `.cc` state, mutate Persona Mind, or render prompt doctrine. The heartbeat shell now only turns the typed observation into character-facing text.
|
||||
- Persona memory projection policy is isolated in `persona-memory-projector.ts`. It owns the deterministic/model choice, projector prompt template, minimum model-output invariant, and schema/path leak rejection through an injected text-model port. Repo Faces and typed native Personas use the same boundary. The large deterministic state-packet renderer is still in the heartbeat shell and is the next extraction target.
|
||||
- The pure typed-state core of that packet now also lives in `persona-memory-projector.ts`: private identity texture, activation vectors, runtime/rest/speaking pressure, needs, bonds, status reads, mood dimensions, and durable/short-term memories. The shell still owns agency/incubation/candidate/receipt rendering and the ordering of externally acquired social, room, clarity, maintenance, and curiosity facts; those are the remaining packet-builder debris.
|
||||
- `persona-memory-projector.ts` now owns the full typed-state packet contract, including clarity-conditioned agency pressure, incubation, deferred speech, recent receipts, final ordering of named external fact sections, final vocabulary transformation, and leak validation. Prompt assembly parity is covered by `--assemble-prompt nibu` producing the same 153,917-character prompt before and after the cut. The shell still computes social/relationship/room/clarity/maintenance fact strings; moving those pure renderers behind a social-context projection boundary is next.
|
||||
- `persona-memory-projector.ts` owns the full typed-state packet contract, including clarity-conditioned agency pressure, incubation, deferred speech, recent receipts, self-maintenance advocacy, final ordering of named external fact sections, final vocabulary transformation, and leak validation. Prompt assembly parity is covered by `--assemble-prompt nibu` producing the same 153,917-character prompt across these ownership cuts.
|
||||
- Self-maintenance classification, rendering, clarity suppression, and ordering now live inside `persona-memory-projector.ts` with the other typed-state pressure sections. The heartbeat shell supplies typed state and a clarity witness; it cannot select, reorder, or separately thread self-advocacy policy. Exact Nibu prompt assembly remains 153,917 characters.
|
||||
- `persona-social-context-projector.ts` owns one aggregate social-context projection from supplied identity, registry, typed state, messages, pronoun guidance, and observation time. That result includes relationship freshness, mapped/unmapped topology, peer openings, relationship-pressure scoring, pronouns, room texture, and human-clarity pressure. Clarity evidence sorts on the real `SourceMessage.timestamp`; the heartbeat shell no longer owns social tokenization or scoring helpers.
|
||||
- Home-repo activity acquisition is isolated in `repo-activity-source.ts`. It owns source-repo/cursor resolution and the bounded read-only exporter invocation, returning typed observations that prompt composition renders without subprocess or JSON-parsing authority.
|
||||
- Deployment authority detail: Idunn executes `/srv/odin/deploy-manifests/voidbot`; `/usr/local/sbin/deploy-voidbot` is only the identical inspection/recovery copy. Install both from `gamecult-ops/scripts/deploy-voidbot.sh`. `/srv/build/VoidBot/.git` and `/srv/build/cultcache-ts/.git` must remain wholly `idunn`-owned because lazy partial-clone reads can fetch objects; root owns releases and service promotion, never build-repo Git state.
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor
|
|||
- Repo character turns now have a state-projector plus turn-Interpreter shape. The state-in boundary is `prompts/repo-face-state-projector.prompt.md`: typed state and identity material are translated into lived character prose before the child sees it. The state projector must surface values, drives, wounds, fascinations, aesthetic taste, dignity, mood, needs, social reads, bonds, agency pressure, and the personality-specific meaning of sparse or dense social topology when the state contains those facts. The deterministic state packet feeding that projector is intentionally verbose and parent-facing: it exposes private notes, values, activation vectors, runtime rest/speaking pressure, affect needs, bonds, status reads, mood dimensions, durable memories, short-term residue, agency pressures, incubating thoughts, candidate speech pressure, recent speech residue, social graph facts, room weather, and semantic curiosity graph attractors so every included variable can appear directly or bend behavior. The curiosity graph is built by seeding history/source vector retrieval from current room context, nearby channels, typed Face state, and home-jurisdiction identity, then decoding retrieved chunks into weighted attractor clusters with prominence, saturation, novelty, density, jurisdiction fit, suggested motion, and evidence labels. The projector owns turning those neutral facts into lived curiosity, boredom, fatigue, territorial pull, underexplored intrigue, or a closing/pivot impulse; deterministic code must not decide the feeling. Deterministic code may also attach neutral graph facts such as mapped people and unmapped registered peers to the projector packet, but it must not decide whether that feels like anxiety, territorial scouting, or nothing. Deterministic code attaches recent read-only home-repo activity before chat, using the Face's own state path as the repo-activity cursor source, so current implementation motion is visible as body context. Current-room and nearby-channel logs no longer cross a model conversation projector; deterministic code attaches raw oldest-to-newest transcript evidence with explicit recency/correction rules so the Face can notice human reframes directly. Deterministic code also attaches a social-affordance surface that matches active typed person-bonds/status reads to recent raw messages from that person, so relationship state arrives as a live hook rather than private decorative lore. Deterministic code still owns full prompt assembly and inspection through `scripts/run-repo-face-heartbeats.ts --assemble-prompt <identity>`; that path composes stable doctrine, allowed rooms, research/archive capability guidance, Bifrost digest, projected memory/affect prose, home-repo activity, live social affordances, and raw conversation evidence into the exact child prompt. It must not invent personality, mood, affect, or social memory from typed state. Situational obligations such as direct-call priority or first-public-introduction duty are emitted only when the scheduler packet proves they apply, not as standing prompt law. The child character writes a natural in-character turn on full `gpt-5.4` and may describe desired speech, repo/article proposals, social reads, needs, mood shifts, or memory-worthy thoughts without emitting action DSL blocks. The cheaper Spark/Mini parent Interpreter decides route/retry/drop and translates warranted effects into structured `STATE NOTE` and `SAY` blocks for the worker while governance dispatch remains paused. `STATE NOTE` blocks become typed short-term memory, affect needs, social bonds, status reads, mood dimensions, or agency pressure. The worker still accepts legacy compact JSON sentinels and legacy `UPDATE REQUEST` blocks for compatibility, but the live prompt path keeps transport and state syntax out of the character's roleplay surface.
|
||||
- `apps/persona-scheduler` is the resident daemon organ that owns the serialized Persona wall-clock pulse on Yggdrasil. It invokes the remaining turn orchestrator without overlap and remains alive between pulses. `apps/persona-scheduler/src/initiative-engine.ts` owns the pure scheduling state machine: participant reconciliation, deterministic base speed/bias/threshold derivation, operator heat/speed projection, semantic-pressure commits, wall-clock advancement, active-turn freeze/completion recovery, queue-success/failure/dry-run commits, stale rescheduling, pending-mention priority, rest/idle eligibility, and ready ordering. Explicit blocked/withdrawn status, active load, semantic receipts, and initiative position survive reconciliation; new Faces without a usable mouth fail closed. `apps/persona-scheduler/src/state-store.ts` owns scheduler persistence and legacy migration. It stores one typed `voidbot.persona_scheduler_state` record in `repo-face-heartbeats.cc`, migrates the former JSON document once with a `storage_migrated` witness, rejects malformed CultCache instead of replacing it, and writes JSON only as a derived dashboard/debug projection. `packages/core/src/repo-face-heartbeat-queue.ts` resolves both legacy `.json` configuration and current `.cc` configuration to the same canonical path; the Discord bot publishes one immutable `voidbot.persona_attention_command` CultCache inbox document per addressed message. The scheduler store ingests and deduplicates those commands, persists them as pending attention, then acknowledges inbox files only after the durable commit; a crash before acknowledgement replays safely. `apps/persona-scheduler/src/active-turn-source.ts` owns the Postgres/legacy-lock liveness adapter. It recognizes only Persona turn request IDs, applies the bounded stale threshold, marks dead queue claims failed, and returns active/stale witnesses; it never writes scheduler state. `apps/persona-scheduler/src/persona-state-source.ts` owns read-only typed Persona-state acquisition and present-time physiology projection. It reports typed mind plus rest facts without creating missing state or writing sleep/speaking-pressure state; the initiative engine alone applies rest eligibility. `apps/persona-scheduler/src/persona-memory-context-source.ts` owns the derived vector refresh and semantic recall query for one supplied typed Mind observation. It returns typed hits/unavailability and cannot reacquire canonical state, render prompt prose, or queue work. `apps/persona-scheduler/src/persona-memory-projector.ts` owns deterministic/model projection policy, its prompt template, output sufficiency, leak rejection, all typed-state sections including clarity-conditioned agency/incubation/candidate/receipt pressure, and final ordering of named external fact sections. `apps/persona-scheduler/src/persona-social-context-projector.ts` owns time-explicit relationship freshness, room-texture observation, memory-facing room facts, turn-facing room weather, and pronoun-guidance rendering from supplied facts. Neither projector may acquire Discord, reopen Persona state, retrieve vectors, or queue work. `apps/persona-scheduler/src/discord-activity-source.ts` owns neutral Discord activity acquisition: it deduplicates explicit watched channels, excludes bot traffic, reports raw human messages, and derives idle-cooling times from scheduler history without writing state or selecting work. `apps/persona-scheduler/src/semantic-pressure-source.ts` owns embedding-backed affinity observation and returns bounded projections for eligible Personas and permitted channels. It cannot mutate initiative; `initiative-engine.ts` alone applies pressure, dynamic heat, interrupt readiness, dedupe receipts, and transition history. `apps/persona-scheduler/src/turn-context-source.ts` owns current-room and nearby-channel Discord evidence plus the bounded image-attachment cache. It returns chronological source messages and explicit snapshot failure witnesses; it has no access to Persona state, prompt composition, or the job queue. `scripts/run-repo-face-heartbeats.ts` projects the identity registry into neutral participant specs and affinity candidates, still computes social-graph/peer-opening/relationship-pressure/human-clarity/maintenance facts, passes all facts by name to the projector, and invokes external queue actuators pending further extraction. External adapters report facts; only the initiative engine mutates load, counters, recovery position, pressure, and queue-transition history. Repo Faces and native Personas queue ordinary `repo-face-rumination` jobs through the canonical job queue. Built-in Void participates only when its separate moderation heartbeat organ is enabled; the hosted scheduler does not launch Windows PowerShell.
|
||||
- Human-clarity chronology, social topology, peer openings, and relationship-pressure scoring now share the same social-context owner. The shell consumes one named projection result and contains no duplicate social scorer.
|
||||
- Self-maintenance advocacy is a typed-state pressure projection owned by `persona-memory-projector.ts`. That organ classifies qualifying needs, agency pressures, and must-share candidates, suppresses them under live human-clarity pressure, and fixes their order alongside the other pressure sections; the heartbeat shell no longer carries a parallel maintenance policy.
|
||||
- `apps/persona-scheduler/src/bifrost-governance-source.ts` owns the remaining Bifrost governance digest subprocess contract. It supplies the CLI path, arguments, timeout, JSON parsing, and typed command/parse failure topics; Persona prompt composition receives a `BifrostGovernanceDigest` and cannot invoke Bifrost directly.
|
||||
- The scheduler state has one write authority. `initiative-engine.ts` commits participant reconciliation, operator controls, pause/disabled skips, stale-turn recovery witnesses, semantic pressure, pending-attention consumption, queue outcomes, and tick finalization. `run-repo-face-heartbeats.ts` contains no direct participant/state assignment or history append; its sources and actuators return facts to engine commits.
|
||||
- `apps/persona-scheduler/src/turn-actuator.ts` is the worker-queue output port. Given a completed prompt, output channel, evidence fields, provider, identity, timestamp, and narrow `createJob` capability, it builds the canonical worker actor/guild `ContextBundle`, creates one approved `repo-face-rumination` job, and returns `{created, activeJobId, requestMessageId}`. It cannot choose a Persona, mutate scheduler state, or compose prompt content. The orchestration shell no longer knows `ContextBuilder`, job approval defaults, worker command names, or `createJob` mechanics.
|
||||
|
|
|
|||
|
|
@ -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] ?? "";
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
: [];
|
||||
|
|
|
|||
|
|
@ -419,3 +419,4 @@
|
|||
{"ts":"2026-07-16T00:33:49Z","type":"implementation","status":"ok","note":"Created persona-social-context-projector.ts and moved time-explicit relationship freshness, room texture observation/rendering, room weather, and pronoun guidance there. Deleted duplicate shell implementations. Prompt assembly and scheduler dry-run execute successfully; smoke proves supplied-message room projection and pronoun rendering."}
|
||||
{"ts":"2026-07-16T00:44:55Z","type":"implementation","status":"ok","note":"Moved human-clarity chronology/repair pressure and mapped/unmapped social topology into persona-social-context-projector.ts. Corrected clarity ordering from nonexistent createdAt to SourceMessage.timestamp. Prompt assembly remains 153917 characters; smoke proves freshest timestamp selection and topology projection from supplied state/registry."}
|
||||
{"ts":"2026-07-16T00:57:11Z","type":"implementation","status":"ok","note":"Persona social context now has one aggregate projector owning freshness, topology, peer openings, relationship-pressure scoring, pronouns, room texture, and clarity from supplied facts; the heartbeat shell no longer owns duplicate social scorers. TypeScript build, aggregate smoke, exact Nibu prompt assembly (153917 chars), dry-run, and operator surface tests passed."}
|
||||
{"ts":"2026-07-16T01:07:27Z","type":"implementation","status":"ok","note":"Self-maintenance advocacy ownership moved from the heartbeat shell into persona-memory-projector. The projector now classifies typed needs/agency/candidates, suppresses maintenance under live clarity pressure, and orders it with other pressure sections; duplicate shell selectors and helpers were deleted. Build, targeted smoke, exact 153917-character Nibu prompt assembly, dry-run, and operator surface tests passed."}
|
||||
|
|
|
|||
|
|
@ -278,4 +278,4 @@ rejected_paths:
|
|||
|
||||
current_status:
|
||||
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live. Repo Face/persona heartbeat jobs run through the direct `openai_api` provider configured for MiMo `mimo-v2.5-pro`, with the expensive state-projector pass still disabled. The 2026-07-04 transport repair replaced the old Bifrost HTTP/token bridge shortcut with typed Bifrost CultMesh Discord command and receipt documents advertised in the Bifrost provider store at `cultmesh://asgard.starfire.bifrost/commands/discord-post`. VoidBot stores the Bifrost target as a CultMesh URI, writes command docs to `E:/Projects/Bifrost/.bifrost/provider-store.cc`, optionally pumps Bifrost's command processor, and waits for typed receipts before completing repo Face Discord delivery. Bifrost's provider advertisement exposes `discord.post` command metadata for Odin/Eve agents, and a bounded Weksa live-fire completed visibly. Eve live state now runs end to end through CultLib dev retained publications, Hermodr ordered SSE, Eve component bindings, and VoidBot typed control state. Receipts acknowledge intent only. Heat initial load, provider-driven change, and browser-command change were visibly verified without reload through the same subscription.
|
||||
next_action: Extract self-maintenance pressure rendering from `scripts/run-repo-face-heartbeats.ts` behind a narrow projector boundary, then inspect whether curiosity-cluster construction belongs with its vector source or a separate curiosity projector.
|
||||
next_action: Map the heartbeat shell's curiosity pipeline from seed construction through vector retrieval to cluster rendering. Separate acquisition from pure projection, then move cluster ownership behind the smallest boundary that preserves those authorities.
|
||||
|
|
|
|||
Loading…
Reference in a new issue