Extract Persona turn assembler
This commit is contained in:
parent
4fe801a074
commit
059f9b39df
6 changed files with 133 additions and 63 deletions
78
apps/persona-scheduler/src/persona-turn-assembler.ts
Normal file
78
apps/persona-scheduler/src/persona-turn-assembler.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import type { RepoDiscordIdentity, RepoFacePendingMention } from "@voidbot/core";
|
||||
import type { PromptImageAttachment, SourceMessage } from "@voidbot/shared";
|
||||
|
||||
import type { BifrostGovernanceDigest } from "./bifrost-governance-source.js";
|
||||
import type { InitiativeParticipant } from "./initiative-engine.js";
|
||||
import { projectPersonaConversation, type PersonaConversationProjection } from "./persona-conversation-projector.js";
|
||||
import type { PersonaHumanPronounGuidance } from "./persona-social-context-projector.js";
|
||||
import { buildPersonaJurisdictionDiveDirective, buildPersonaTurnPrompt } from "./persona-turn-prompt-projector.js";
|
||||
import type { ChannelSnapshot } from "./turn-context-source.js";
|
||||
import type { PersonaChannelPlan } from "./turn-routing.js";
|
||||
|
||||
export interface PersonaTurnAssemblyInput {
|
||||
identity: RepoDiscordIdentity;
|
||||
channelId: string;
|
||||
channelPlan: PersonaChannelPlan;
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
participant: InitiativeParticipant;
|
||||
pendingMentions: RepoFacePendingMention[];
|
||||
memorySurface?: string;
|
||||
semanticMemoryRecallSurface?: string;
|
||||
repoActivitySurface?: string;
|
||||
conversationMemorySurface?: string;
|
||||
humanPronounGuidance?: PersonaHumanPronounGuidance[];
|
||||
bifrostDigest?: BifrostGovernanceDigest;
|
||||
githubActionsEnabled: boolean;
|
||||
globalAgentDoctrine: string;
|
||||
}
|
||||
|
||||
export interface PersonaTurnAssembly {
|
||||
prompt: string;
|
||||
conversation: PersonaConversationProjection;
|
||||
imageAttachments: PromptImageAttachment[];
|
||||
}
|
||||
|
||||
export function assemblePersonaTurn(input: PersonaTurnAssemblyInput): PersonaTurnAssembly {
|
||||
const conversation = projectPersonaConversation({
|
||||
identity: input.identity,
|
||||
recentMessages: input.recentMessages,
|
||||
channelSnapshots: input.channelSnapshots,
|
||||
pendingMentions: input.pendingMentions,
|
||||
channelPlan: input.channelPlan,
|
||||
});
|
||||
const prompt = buildPersonaTurnPrompt({
|
||||
identity: input.identity,
|
||||
channelId: input.channelId,
|
||||
channelPlan: input.channelPlan,
|
||||
channelSnapshots: input.channelSnapshots,
|
||||
recentMessages: input.recentMessages,
|
||||
memorySurface: input.memorySurface,
|
||||
semanticMemoryRecallSurface: input.semanticMemoryRecallSurface,
|
||||
repoActivitySurface: input.repoActivitySurface,
|
||||
conversationMemorySurface: input.conversationMemorySurface ?? conversation.transcript,
|
||||
humanPronounGuidance: input.humanPronounGuidance,
|
||||
bifrostDigest: input.bifrostDigest,
|
||||
participant: input.participant,
|
||||
pendingMentions: input.pendingMentions,
|
||||
jurisdictionDive: buildPersonaJurisdictionDiveDirective(input.identity, input.participant),
|
||||
githubActionsEnabled: input.githubActionsEnabled,
|
||||
globalAgentDoctrine: input.globalAgentDoctrine,
|
||||
});
|
||||
return {
|
||||
prompt,
|
||||
conversation,
|
||||
imageAttachments: collectPromptImages([input.recentMessages, ...input.channelSnapshots.map((snapshot) => snapshot.messages)].flat()),
|
||||
};
|
||||
}
|
||||
|
||||
function collectPromptImages(messages: SourceMessage[]): PromptImageAttachment[] {
|
||||
const seen = new Set<string>();
|
||||
const images: PromptImageAttachment[] = [];
|
||||
for (const message of messages) for (const attachment of message.attachments ?? []) {
|
||||
if (attachment.kind !== "image" || !attachment.localPath || seen.has(attachment.localPath)) continue;
|
||||
seen.add(attachment.localPath);
|
||||
images.push({ messageId: message.id, authorName: message.authorName, filename: attachment.filename, contentType: attachment.contentType, localPath: attachment.localPath });
|
||||
}
|
||||
return images.slice(0, 8);
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- Repo Face state projection is no longer allowed to be deterministic schema slurry. `scripts/run-repo-face-heartbeats.ts` builds a compact state packet, then calls the model prompt at `prompts/repo-face-state-projector.prompt.md` to turn it into character-facing prose before the Face sees it. `REPO_FACE_STATE_PROJECTOR_ENABLED=false` exists only as an emergency isolation switch; both the model output and fallback packet reject schema/prompt-construction leaks such as `grants:`, `jurisdictions:`, `Face of`, repo paths, and "do not prompt" phrasing.
|
||||
- First role-addressed chat also initializes the target repo's local `.voidbot` home. `ensureRepoFaceInitialized` resolves `repoPath` from the registry, `SOURCE_REPO_ROOT`, or `E:/Projects/<repoName>`, creates `voice/`, `state/`, `birth/`, and `logs/`, writes `voice/identity.json`, then starts `scripts/run-repo-face-birth.mjs` detached. That worker runs EpiphanyAgent's `epiphany-repo-personality scout` and `epiphany-repo-birth-runner` so newborn personality/memory/trajectory packets come from repo contents and git history. Status and logs live in the target repo under `.voidbot/birth/voidbot-face-birth.json` and `.voidbot/logs/voidbot-face-birth.log`.
|
||||
- Manual newborn/strengthening passes should use `prompts/repo-face-personality-seed.prompt.md`. It makes the intended rite explicit: inspect repo docs/source/state/history, migrate useful legacy `memory.json` material into typed `.cc` memories, seed values/needs/agency/social state only when they will change behavior, and keep `.voidbot/voice` plus `.voidbot/state` as the repo-local home.
|
||||
- Agent turns use one CTB/Final-Fantasy-X-style initiative queue. `apps/persona-scheduler` owns the resident one-minute observation pulse on Yggdrasil; the pulse does not own speech cadence. `initiative-engine.ts` owns participant reconciliation plus clock, active-turn freeze/recovery, queue-result commits, eligibility, ready ordering, and the global unprompted admission gate. It admits at most one unprompted Persona per `REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES` window even when idle-rest cooling is disabled or inactive; direct mentions remain obligations and bypass that cadence. Heat changes individual recovery pressure but cannot drain a ready backlog one turn per pulse. `state-store.ts` alone owns load/migration/atomic persistence of scheduler state. Canonical scheduler state is now one typed `voidbot.persona_scheduler_state` record in `.voidbot/status/repo-face-heartbeats.cc`. A configured legacy `.json` path resolves to that `.cc`, the former JSON document is migrated once with a witness, and subsequent JSON is a derived dashboard/debug projection that cannot override CultCache. Discord mention ingress is no longer a second whole-state writer: the bot creates immutable per-message `voidbot.persona_attention_command` CultCache inbox documents at the same canonical path, and the scheduler ingests/commits/acknowledges them with replay-safe ordering. `active-turn-source.ts` alone reads Persona job liveness from Postgres/the remaining Void lock and may fail a stale queue claim; it returns facts to the engine and cannot edit initiative. `discord-activity-source.ts` owns read-only human-activity polling and derives both neutral idle observations and the next unprompted admission time from scheduler history; disabling idle-rest behavior cannot disable the global speech cadence. `scripts/run-repo-face-heartbeats.ts` may build neutral specs from registered identities and invoke queue actuators, but it no longer derives or persists speed, groups, heat, reaction bias, interrupt threshold, status, load, counters, recovery, initiative position, active-job liveness, or idle-activity observations. It remains the temporary orchestration shell for Persona physiology/memory context and prompt assembly.
|
||||
- Agent turns use one CTB/Final-Fantasy-X-style initiative queue. `apps/persona-scheduler` owns the resident one-minute observation pulse on Yggdrasil; the pulse does not own speech cadence. `initiative-engine.ts` owns participant reconciliation plus clock, active-turn freeze/recovery, queue-result commits, eligibility, ready ordering, and the global unprompted admission gate. It admits at most one unprompted Persona per `REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES` window even when idle-rest cooling is disabled or inactive; direct mentions remain obligations and bypass that cadence. Heat changes individual recovery pressure but cannot drain a ready backlog one turn per pulse. `state-store.ts` alone owns load/migration/atomic persistence of scheduler state. Canonical scheduler state is now one typed `voidbot.persona_scheduler_state` record in `.voidbot/status/repo-face-heartbeats.cc`. A configured legacy `.json` path resolves to that `.cc`, the former JSON document is migrated once with a witness, and subsequent JSON is a derived dashboard/debug projection that cannot override CultCache. Discord mention ingress is no longer a second whole-state writer: the bot creates immutable per-message `voidbot.persona_attention_command` CultCache inbox documents at the same canonical path, and the scheduler ingests/commits/acknowledges them with replay-safe ordering. `active-turn-source.ts` alone reads Persona job liveness from Postgres/the remaining Void lock and may fail a stale queue claim; it returns facts to the engine and cannot edit initiative. `discord-activity-source.ts` owns read-only human-activity polling and derives both neutral idle observations and the next unprompted admission time from scheduler history; disabling idle-rest behavior cannot disable the global speech cadence. `scripts/run-repo-face-heartbeats.ts` may build neutral specs, acquire source observations, and invoke queue actuators, but it no longer owns scheduler state or final Persona turn composition.
|
||||
- Bifrost governance acquisition is isolated in `bifrost-governance-source.ts`. That adapter alone owns the remaining `governance-threads.mjs digest` CLI contract and converts provider command/parse failures into typed error topics; prompt assembly receives governance facts and does not know the subprocess.
|
||||
- Scheduler state mutation is centralized. `initiative-engine.ts` owns reconciliation, controls, skip/stale witnesses, pressure, mention consumption, queue transitions, and tick finalization through named commits. The orchestration shell has no direct scheduler state/history writes; queue and context adapters only return facts.
|
||||
- Persona job submission is isolated in `turn-actuator.ts`. That output port alone owns worker actor/guild context construction, `ContextBundle` lowering, the `repo-face-rumination` command, approval defaults, request IDs, and `createJob`; it returns a queue receipt and has no state-selection or prompt-composition authority.
|
||||
|
|
@ -212,6 +212,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- Yggdrasil matches that state as of deployed revision `13bdb7a`: all five native Persona registry paths are `.cc`, migrated files are owned by `voidbot:voidbot`, the full initiative and portable-migration smokes pass in the deployed scheduler container, and prompt assembly succeeds for Muninn, Sleipnir, Hermodr, Tengu, and メタめ with the correct authority language. Idunn's upstream comparison/deployment path is healthy and evaluates VoidBot on its bounded cadence. The swarm remains paused after these checks; do not interpret migration success as permission to resume autonomous traffic.
|
||||
- Deterministic state-packet assembly now lives in `persona-state-packet-projector.ts`, which takes typed state, registry/room evidence, pronoun guidance, curiosity facts, and an explicit observation time. The shell no longer owns section ordering or social-context composition. The downstream regex vocabulary cleaner was deleted: it was a symptom compensator rewriting ordinary words such as `witness` and `canary` after projection. A smoke proves literal source language survives the projector unchanged.
|
||||
- Optional model-backed state projection now crosses `persona-text-projection-actuator.ts`. That actuator alone owns Codex args/process lifetime, ordered fallback models, timeout, output decoding, retry classification, and model-output JSONL telemetry. Focused smoke proves it chooses the last visible agent message, retries capacity/429 failures, and refuses to classify prompt-contract failures as model availability problems. The heartbeat shell lost another 182 lines and now only supplies the completed projection prompt plus runtime config.
|
||||
- Final Persona turn composition now crosses `persona-turn-assembler.ts` for both queued turns and `--assemble-prompt` inspection. It owns conversation projection, jurisdiction-dive cadence, final prompt composition, and the deduplicated eight-image evidence budget. The shell acquires facts and the turn actuator queues the assembled result; neither may maintain a parallel opinion about the final prompt or evidence bundle.
|
||||
- Direct Face `UPDATE REQUEST` and `BIFROST TOPIC` packets are ignored while `REPO_FACE_BIFROST_ENABLED=false`. Do not debug missing Bifrost jobs as a live failure unless that flag has been deliberately enabled again.
|
||||
- Live model-facing prompt prose has started moving out of executable code. The shared loader is `packages/shared/src/prompt-template.ts`; extracted prompt surfaces live under `prompts/`, including repo Face turn/reviewer prompts, provider wrapper prompts, situational social read prompts, source-grounding reminders, consensus-feed prompts, and repo Face model-smoke scenarios. Code should project data and select prompt names; prompt files should own behavioral prose.
|
||||
- Identity collapse has started. Active bot, worker, MCP repo-identity tools, and Face CTB scheduler paths now load through the canonical Face registry and treat repo Discord identities as compatibility projections. Next bounded move is to finish removing remaining direct conceptual dependence on repo identities, then split the Face turn loop into scheduler, context assembler, prompt renderer, candidate parser, policy gate, and transport adapter.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import {
|
|||
} from "@voidbot/core";
|
||||
import { createTextEmbedder, createVectorStores, RetrievalService } from "@voidbot/rag";
|
||||
import {
|
||||
type PromptImageAttachment,
|
||||
type SourceMessage,
|
||||
} from "@voidbot/shared";
|
||||
import {
|
||||
|
|
@ -59,12 +58,12 @@ import {
|
|||
import { projectPersonaMemorySurface } from "../apps/persona-scheduler/dist/persona-memory-projector.js";
|
||||
import { readPersonaCuriosityEvidence } from "../apps/persona-scheduler/dist/persona-curiosity-context-source.js";
|
||||
import { projectPersonaCuriosityContext } from "../apps/persona-scheduler/dist/persona-curiosity-projector.js";
|
||||
import { projectPersonaConversation } from "../apps/persona-scheduler/dist/persona-conversation-projector.js";
|
||||
import { buildPersonaJurisdictionDiveDirective, buildPersonaTurnPrompt, renderPersonaIdentityDoctrine } from "../apps/persona-scheduler/dist/persona-turn-prompt-projector.js";
|
||||
import { renderPersonaIdentityDoctrine } from "../apps/persona-scheduler/dist/persona-turn-prompt-projector.js";
|
||||
import { projectGamecultPersonaState, projectNativePersonaBody } from "../apps/persona-scheduler/dist/persona-standard-state-projector.js";
|
||||
import type { PersonaHumanPronounGuidance as RepoFaceHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
|
||||
import { projectPersonaStatePacket } from "../apps/persona-scheduler/dist/persona-state-packet-projector.js";
|
||||
import { projectPersonaText } from "../apps/persona-scheduler/dist/persona-text-projection-actuator.js";
|
||||
import { assemblePersonaTurn } from "../apps/persona-scheduler/dist/persona-turn-assembler.js";
|
||||
import { readPersonaHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-source.js";
|
||||
import {
|
||||
readDiscordActivitySnapshot,
|
||||
|
|
@ -433,46 +432,32 @@ async function queueRepoFaceTurn(input: {
|
|||
? projectNativePersonaBody(identity)
|
||||
: renderRepoActivityObservation(readRepoActivity({ identity, storageRoot: input.config.storageRoot }));
|
||||
const globalAgentDoctrine = await loadGlobalAgentDoctrine();
|
||||
const conversationProjection = projectPersonaConversation({
|
||||
identity,
|
||||
recentMessages,
|
||||
channelSnapshots,
|
||||
pendingMentions: input.pendingMentions,
|
||||
channelPlan,
|
||||
});
|
||||
const conversationMemorySurface = conversationProjection.transcript;
|
||||
const prompt = buildPersonaTurnPrompt({
|
||||
const assembly = assemblePersonaTurn({
|
||||
identity,
|
||||
channelId,
|
||||
channelPlan,
|
||||
channelSnapshots,
|
||||
recentMessages,
|
||||
memorySurface,
|
||||
repoActivitySurface,
|
||||
conversationMemorySurface,
|
||||
humanPronounGuidance,
|
||||
bifrostDigest,
|
||||
channelSnapshots,
|
||||
participant: input.participant,
|
||||
pendingMentions: input.pendingMentions,
|
||||
jurisdictionDive: buildPersonaJurisdictionDiveDirective(identity, input.participant),
|
||||
memorySurface,
|
||||
repoActivitySurface,
|
||||
humanPronounGuidance,
|
||||
bifrostDigest,
|
||||
githubActionsEnabled: input.config.repoFaceGithubActionsEnabled,
|
||||
globalAgentDoctrine,
|
||||
});
|
||||
const imageAttachments = collectPromptImageAttachments([
|
||||
recentMessages,
|
||||
...channelSnapshots.map((snapshot) => snapshot.messages),
|
||||
].flat());
|
||||
const result = await submitPersonaTurn({
|
||||
jobQueue: input.storage.jobQueue,
|
||||
provider: input.config.repoFaceHeartbeats.provider,
|
||||
identityId: identity.id,
|
||||
queuedAt: input.queuedAt,
|
||||
channelId,
|
||||
prompt,
|
||||
prompt: assembly.prompt,
|
||||
recentMessages,
|
||||
conversationFocus: conversationProjection.focus,
|
||||
conversationThreads: conversationProjection.threads,
|
||||
imageAttachments,
|
||||
conversationFocus: assembly.conversation.focus,
|
||||
conversationThreads: assembly.conversation.threads,
|
||||
imageAttachments: assembly.imageAttachments,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -698,18 +683,12 @@ async function assembleRepoFaceTurnPrompt(input: {
|
|||
const globalAgentDoctrine = await loadGlobalAgentDoctrine();
|
||||
const conversationMemorySurface = input.conversationSurfacePath
|
||||
? await readOptionalMemorySurface(input.conversationSurfacePath)
|
||||
: projectPersonaConversation({
|
||||
identity,
|
||||
recentMessages,
|
||||
channelSnapshots,
|
||||
pendingMentions: [],
|
||||
channelPlan,
|
||||
}).transcript;
|
||||
: undefined;
|
||||
const participant = buildInspectionParticipant(
|
||||
identity,
|
||||
input.config.repoFaceHeartbeats.baseRecoveryMinutes,
|
||||
);
|
||||
const prompt = buildPersonaTurnPrompt({
|
||||
const assembly = assemblePersonaTurn({
|
||||
identity,
|
||||
channelId,
|
||||
channelPlan,
|
||||
|
|
@ -723,7 +702,6 @@ async function assembleRepoFaceTurnPrompt(input: {
|
|||
bifrostDigest,
|
||||
participant,
|
||||
pendingMentions: [],
|
||||
jurisdictionDive: buildPersonaJurisdictionDiveDirective(identity, participant),
|
||||
githubActionsEnabled: input.config.repoFaceGithubActionsEnabled,
|
||||
globalAgentDoctrine,
|
||||
});
|
||||
|
|
@ -731,13 +709,13 @@ async function assembleRepoFaceTurnPrompt(input: {
|
|||
if (input.outPath) {
|
||||
const outPath = resolve(input.outPath);
|
||||
await mkdir(dirname(outPath), { recursive: true });
|
||||
await writeFile(outPath, prompt, "utf8");
|
||||
await writeFile(outPath, assembly.prompt, "utf8");
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
identityId: identity.id,
|
||||
promptLength: prompt.length,
|
||||
promptLength: assembly.prompt.length,
|
||||
outPath: input.outPath ? resolve(input.outPath) : undefined,
|
||||
memorySurfacePath: input.memorySurfacePath ? resolve(input.memorySurfacePath) : undefined,
|
||||
conversationSurfacePath: input.conversationSurfacePath ? resolve(input.conversationSurfacePath) : undefined,
|
||||
|
|
@ -965,31 +943,6 @@ function renderRepoActivityObservation(observation: RepoActivityObservation): st
|
|||
return observation.digest || `- No recent ${observation.sourceRepoName} activity was reported.`;
|
||||
}
|
||||
|
||||
function collectPromptImageAttachments(messages: SourceMessage[]): PromptImageAttachment[] {
|
||||
const seen = new Set<string>();
|
||||
const images: PromptImageAttachment[] = [];
|
||||
for (const message of messages) {
|
||||
for (const attachment of message.attachments ?? []) {
|
||||
if (attachment.kind !== "image" || !attachment.localPath) {
|
||||
continue;
|
||||
}
|
||||
const key = attachment.localPath;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
images.push({
|
||||
messageId: message.id,
|
||||
authorName: message.authorName,
|
||||
filename: attachment.filename,
|
||||
contentType: attachment.contentType,
|
||||
localPath: attachment.localPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
return images.slice(0, 8);
|
||||
}
|
||||
|
||||
async function readOptionalMemorySurface(path: string | undefined): Promise<string | undefined> {
|
||||
if (!path) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import { readPersonaCuriosityEvidence } from "../apps/persona-scheduler/dist/per
|
|||
import { projectPersonaCuriosityContext } from "../apps/persona-scheduler/dist/persona-curiosity-projector.js";
|
||||
import { projectPersonaConversation, renderPersonaRoomTopicSaturation } from "../apps/persona-scheduler/dist/persona-conversation-projector.js";
|
||||
import { buildPersonaJurisdictionDiveDirective, buildPersonaTurnPrompt } from "../apps/persona-scheduler/dist/persona-turn-prompt-projector.js";
|
||||
import { assemblePersonaTurn } from "../apps/persona-scheduler/dist/persona-turn-assembler.js";
|
||||
import { projectGamecultPersonaState } from "../apps/persona-scheduler/dist/persona-standard-state-projector.js";
|
||||
import { projectPersonaStatePacket } from "../apps/persona-scheduler/dist/persona-state-packet-projector.js";
|
||||
import { extractLastPersonaProjectionMessage, isRetryablePersonaProjectionFailure } from "../apps/persona-scheduler/dist/persona-text-projection-actuator.js";
|
||||
|
|
@ -681,6 +682,41 @@ const projectedTurnPrompt = buildPersonaTurnPrompt({
|
|||
globalAgentDoctrine: "Global doctrine.",
|
||||
});
|
||||
assert.match(projectedTurnPrompt, /A direct call is tugging at you[\s\S]*Nibu, answer this/, "the pure turn-prompt projector owns situation and pending-call policy from supplied facts");
|
||||
const assembledTurn = assemblePersonaTurn({
|
||||
identity: routedIdentity,
|
||||
channelId: "lore",
|
||||
channelPlan,
|
||||
channelSnapshots: [{ channelId: "aquarium", messages: [{
|
||||
id: "duplicate-image",
|
||||
authorId: "human-2",
|
||||
authorName: "Neighbor",
|
||||
content: "same image nearby",
|
||||
timestamp: "2026-07-15T21:09:00.000Z",
|
||||
isBot: false,
|
||||
channelId: "aquarium",
|
||||
attachments: [{ kind: "image", filename: "same.png", contentType: "image/png", localPath: "C:/cache/same.png" }],
|
||||
}] }],
|
||||
recentMessages: Array.from({ length: 9 }, (_, index) => ({
|
||||
id: `image-${index}`,
|
||||
authorId: "human",
|
||||
authorName: "Operator",
|
||||
content: `image evidence ${index}`,
|
||||
timestamp: `2026-07-15T21:${String(index).padStart(2, "0")}:00.000Z`,
|
||||
isBot: false,
|
||||
channelId: "lore",
|
||||
attachments: [{ kind: "image" as const, filename: `${index}.png`, contentType: "image/png", localPath: index === 0 ? "C:/cache/same.png" : `C:/cache/${index}.png` }],
|
||||
})),
|
||||
participant: promptParticipant,
|
||||
pendingMentions: [],
|
||||
conversationMemorySurface: "Inspection supplied conversation authority.",
|
||||
githubActionsEnabled: false,
|
||||
globalAgentDoctrine: "Global doctrine.",
|
||||
});
|
||||
assert.match(assembledTurn.prompt, /Inspection supplied conversation authority\./, "queued and inspection paths share one assembler with an explicit conversation override");
|
||||
assert.doesNotMatch(assembledTurn.prompt, /Visible cross-channel chronology/, "the assembler does not let its derived transcript override an explicit inspection surface");
|
||||
assert.equal(assembledTurn.imageAttachments.length, 8, "the assembler owns the bounded image evidence budget");
|
||||
assert.equal(assembledTurn.imageAttachments.filter((attachment) => attachment.localPath === "C:/cache/same.png").length, 1, "the assembler deduplicates image evidence across room and nearby snapshots");
|
||||
assert.equal(assembledTurn.conversation.focus?.channelId, "aquarium", "the assembler returns the same newest-evidence conversation projection supplied to the turn actuator");
|
||||
assert.equal(personaChannelSpeedMultiplier(routedIdentity), 3, "scheduler speed projection uses the routing organ's bounded channel policy");
|
||||
assert.equal(newestPendingMentionChannel([
|
||||
{ identityId: "nibu", channelId: "older", queuedAt: "2026-07-15T20:00:00.000Z" },
|
||||
|
|
|
|||
|
|
@ -430,3 +430,4 @@
|
|||
{"ts":"2026-07-16T03:40:00Z","type":"verification","status":"ok","note":"Idunn deployed 13bdb7a on Yggdrasil. Persisted registry has five native Persona .cc paths and zero JSON paths; Muninn/Sleipnir/Hermodr projection-import stores plus Tengu canonical store are voidbot-owned. Deployed initiative and migration smokes pass, and live prompt assembly for all five native Personas reports projection authority for the three quarantined imports, canonical gamecult.persona_state.v0 for Tengu, and the existing typed Void path for Metame. Swarm stayed paused with zero queued turns."}
|
||||
{"ts":"2026-07-16T03:50:00Z","type":"implementation","status":"ok","note":"Deterministic Persona state-packet assembly moved from the heartbeat shell into persona-state-packet-projector.ts with explicit typed inputs and observation time. The old regex vocabulary cleaner plus dead narrative helpers were deleted; focused smoke proves literal source language containing 'witness artifact' is not rewritten downstream."}
|
||||
{"ts":"2026-07-16T04:00:00Z","type":"implementation","status":"ok","note":"Codex-backed Persona text projection moved into persona-text-projection-actuator.ts, which owns model ordering, process arguments/lifetime, timeouts, output decoding, retry classification, and telemetry. Focused smoke proves last-agent-message extraction plus retry only for capacity/tool availability, and the heartbeat shell fell from 1250 to 1068 lines."}
|
||||
{"ts":"2026-07-16T04:20:00Z","type":"implementation","status":"ok","note":"Queued and inspection Persona turns now share persona-turn-assembler.ts as the single owner of conversation projection, jurisdiction cadence, final prompt construction, and bounded deduplicated image evidence. The duplicate shell attachment collector was deleted; typecheck plus initiative and migration smokes pass."}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ active_subgoals:
|
|||
- The resident `apps/persona-scheduler` process inside `voidbot.service` owns repo Face and native Persona turn pulses on Yggdrasil. Its `initiative-engine.ts` owns participant reconciliation, operator heat/speed projection, semantic-pressure commits, wall-clock advancement, active-turn freeze/completion recovery, queue-result commits, stale rescheduling, mention priority, rest/idle eligibility, and ready-order selection as a pure state machine. `state-store.ts` is the sole persistence/migration port: canonical state is one typed `voidbot.persona_scheduler_state` record in `repo-face-heartbeats.cc`; the old JSON shape is accepted only as a one-time migration source and emitted afterward only as a debug/dashboard projection. Missing state is explicit, malformed CultCache fails closed, and the backing store promotes atomic MessagePack writes. Discord attention crosses through immutable per-mention `voidbot.persona_attention_command` CultCache inbox documents resolved from that same canonical path; the bot creates commands, while the scheduler alone ingests, deduplicates, commits, and acknowledges them after durable state. The bot can no longer read-modify-write the whole heartbeat state. `active-turn-source.ts` is the narrow Postgres/legacy-lock adapter: it reports active job witnesses and marks truly stale queue claims failed, but it cannot mutate initiative. `persona-state-source.ts` owns read-only typed Persona-state acquisition and wall-clock physiology projection. It reports typed mind and current rest facts without creating a missing state surface or writing sleep/speaking-pressure updates; `initiative-engine.ts` alone decides whether those rest facts admit a turn. `persona-memory-context-source.ts` consumes that observation, refreshes only the derived per-Persona vector corpus, runs semantic recall, and returns structured hits or an explicit unavailable observation; it cannot reload or mutate canonical Persona state, render prompt prose, select, or queue. `persona-memory-projector.ts` owns the deterministic-versus-model projection choice, projector prompt contract, minimum-output invariant, and schema/path leak rejection through an injected text-model port. It cannot acquire state, retrieve vectors, or queue turns. `discord-activity-source.ts` owns read-only Discord human-activity observation and idle-window derivation from explicit watched channels plus scheduler history; it cannot mutate initiative, consume attention, or select work. `semantic-pressure-source.ts` embeds eligible Persona affinity cards against recent permitted human messages and returns bounded pressure projections; only `initiative-engine.ts` may turn those facts into dynamic heat, readiness, interrupt receipts, or history. `turn-context-source.ts` owns raw current-room and nearby-channel Discord acquisition plus its bounded derived attachment cache; it returns chronological evidence and explicit snapshot failures without reading Persona state, composing prompts, or queuing work. The heartbeat orchestration script may project registered identities into neutral participant specs and affinity candidates, build the still-unextracted deterministic state packet, render source observations into prompt surfaces, and report external actuator results, but it cannot decide, persist, migrate, inspect queue liveness, poll Discord, operate vector stores, own model-projection policy, commit semantic pressure, mutate Persona physiology, or overwrite initiative fields. The Windows `GameCult Local Orchestrator` no longer owns Persona scheduling, and its repo-Face task installer/wrapper have been deleted. Void moderation, mood, watchdog, and Bifrost dispatch remain separate organs pending their own daemon migration; they must not be smuggled back into the Persona scheduler.
|
||||
- `persona-state-packet-projector.ts` owns pure assembly of typed memory sections, social context, topic attraction, curiosity facts, pressure sections, and human-clarity pressure into the deterministic Persona state packet. The orchestration shell supplies observations and one explicit observation time. It cannot rewrite projected language: the old regex vocabulary cleaner and its dead narrative helpers were deleted rather than moved.
|
||||
- `persona-text-projection-actuator.ts` owns Codex process execution for optional model-backed Persona memory projection: ordered model fallback, read-only arguments, timeout, stdout/stderr capture, final-message decoding, retry classification, and model-output telemetry. It receives a complete prompt and returns text; it cannot acquire Persona/Discord state, select initiative, or queue work. Capacity/tool-availability failures may try the next configured model; prompt/contract failures do not.
|
||||
- `persona-turn-assembler.ts` is the single final-composition owner for queued and inspection Persona turns. It combines supplied observations through conversation projection and turn-prompt policy, derives jurisdiction cadence, and returns one prompt plus one bounded deduplicated image bundle. It cannot acquire source state, select initiative, or queue work; the shell and actuator cannot maintain parallel prompt opinions.
|
||||
- `bifrost-governance-source.ts` owns the remaining Bifrost digest CLI adapter and converts command or parse failures into typed error topics. Persona prompt composition receives governance facts without knowing Bifrost process mechanics; `scripts/run-repo-face-heartbeats.ts` may not invoke the Bifrost CLI directly.
|
||||
- `initiative-engine.ts` is the only writer of scheduler state fields and history. Reconciliation, controls, pause/disabled skips, stale-turn recovery witnesses, semantic pressure, attention consumption, queue transitions, and tick finalization all cross named engine commit primitives. Queue/context actuators return results and cannot patch participants or history directly.
|
||||
- `turn-actuator.ts` owns lowering an assembled Persona prompt and evidence bundle into the canonical worker `ContextBundle` and approved `repo-face-rumination` job. It owns the worker actor, command, request ID, approval defaults, and `createJob` call, then returns only a queue receipt. The orchestration shell chooses no queue mechanics and the actuator cannot read Persona or initiative state.
|
||||
|
|
@ -284,4 +285,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: Keep the Yggdrasil swarm paused. Extract prompt-assembly coordination from `scripts/run-repo-face-heartbeats.ts`: current-room/snapshot/governance acquisition should remain source calls, while memory/repo-activity/semantic-recall/conversation/global-doctrine projection and final prompt composition should cross one typed assembler boundary. Do not move source acquisition or queue authority into that organ, and do not unpause without an explicit controlled-live-fire decision.
|
||||
next_action: Keep the Yggdrasil swarm paused. Extract the remaining Windows-specific Void moderation launch and handshake mechanics from `scripts/run-repo-face-heartbeats.ts` into a narrow actuator. The shell may decide when the built-in Void participant needs a turn, but process launch, PowerShell encoding, handshake polling, timeout, and completion witness belong to one actuator. Do not unpause without an explicit controlled-live-fire decision.
|
||||
|
|
|
|||
Loading…
Reference in a new issue