From c0cf87520b69a661d9f39c8db9b889365845934f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 05:29:00 +0100 Subject: [PATCH] Extract person-shaped rumination organ --- .../src/relative-chronology-projector.ts | 21 +++ .../src/void-memory-maintenance-projector.ts | 19 +-- .../void-moderation-heartbeat-projector.ts | 22 +-- .../src/void-rumination-organ.ts | 53 ++++++++ .../src/void-rumination-projector.ts | 127 ++++++++++++++++++ .../src/void-rumination-text-actuator.ts | 10 ++ scripts/smoke-persona-initiative-engine.ts | 58 ++++++++ 7 files changed, 273 insertions(+), 37 deletions(-) create mode 100644 apps/persona-scheduler/src/relative-chronology-projector.ts create mode 100644 apps/persona-scheduler/src/void-rumination-organ.ts create mode 100644 apps/persona-scheduler/src/void-rumination-projector.ts create mode 100644 apps/persona-scheduler/src/void-rumination-text-actuator.ts diff --git a/apps/persona-scheduler/src/relative-chronology-projector.ts b/apps/persona-scheduler/src/relative-chronology-projector.ts new file mode 100644 index 0000000..46c6a38 --- /dev/null +++ b/apps/persona-scheduler/src/relative-chronology-projector.ts @@ -0,0 +1,21 @@ +export function projectRelativeChronology(value: T, observedAt: Date): T { + return projectValue(value, observedAt) as T; +} + +function projectValue(value: unknown, observedAt: Date, key = ""): unknown { + if (Array.isArray(value)) return value.map((entry) => projectValue(entry, observedAt)); + if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, projectValue(entry, observedAt, entryKey)])); + if (typeof value === "string" && /(?:at|timestamp)$/i.test(key) && Number.isFinite(Date.parse(value))) return relativeTime(value, observedAt); + return value; +} + +function relativeTime(value: string, observedAt: Date): string { + const difference = observedAt.getTime() - Date.parse(value); + const future = difference < 0; + const minutes = Math.max(0, Math.round(Math.abs(difference) / 60_000)); + const phrase = (amount: number, unit: string) => future ? `in ${amount} ${unit}${amount === 1 ? "" : "s"}` : `${amount} ${unit}${amount === 1 ? "" : "s"} ago`; + if (minutes < 60) return phrase(minutes, "minute"); + const hours = Math.round(minutes / 60); + if (hours < 24) return phrase(hours, "hour"); + return phrase(Math.round(hours / 24), "day"); +} diff --git a/apps/persona-scheduler/src/void-memory-maintenance-projector.ts b/apps/persona-scheduler/src/void-memory-maintenance-projector.ts index 00420ca..4d3e370 100644 --- a/apps/persona-scheduler/src/void-memory-maintenance-projector.ts +++ b/apps/persona-scheduler/src/void-memory-maintenance-projector.ts @@ -1,4 +1,5 @@ import { voidSelfStateOperationSchema, type VoidSelfStateOperation, type VoidSelfStateTypedProjection } from "@voidbot/core"; +import { projectRelativeChronology } from "./relative-chronology-projector.js"; const ALLOWED_OPERATIONS = new Set([ "merge_incubation_support", "queue_candidate_intervention", "retire_candidate_intervention", @@ -39,7 +40,7 @@ export function projectVoidMemoryMaintenanceContext(input: { const activeAgency = state.agencyPressure.pressures.filter((entry) => ["active", "cooling", "ready_to_act"].includes(entry.status)); const activeCandidates = state.candidateInterventions.interventions.filter((entry) => entry.status === "queued" || entry.status === "deferred"); const maintenancePressure = activeShortTerm.length + state.thoughtMemory.incubation.length + activeAgency.length + activeCandidates.length; - const relative = (value: T): T => projectRelativeChronology(value, input.observedAt) as T; + const relative = (value: T): T => projectRelativeChronology(value, input.observedAt); return { mode: sleeping ? "sleep_maintenance" : "awake_memory_maintenance", operationTimestamp: input.observedAt.toISOString(), @@ -85,19 +86,3 @@ export function parseVoidMemoryMaintenanceOperations(outputText: string): VoidSe return operation; }); } - -function projectRelativeChronology(value: unknown, observedAt: Date, key = ""): unknown { - if (Array.isArray(value)) return value.map((entry) => projectRelativeChronology(entry, observedAt)); - if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, projectRelativeChronology(entry, observedAt, entryKey)])); - if (typeof value === "string" && /(?:At|Timestamp)$/.test(key) && Number.isFinite(Date.parse(value))) return relativeTime(value, observedAt); - return value; -} - -function relativeTime(value: string, observedAt: Date): string { - const differenceMs = observedAt.getTime() - Date.parse(value); - if (!Number.isFinite(differenceMs)) return "unknown time"; - const future = differenceMs < 0; - const minutes = Math.max(0, Math.round(Math.abs(differenceMs) / 60_000)); - const amount = minutes < 60 ? `${minutes} minute${minutes === 1 ? "" : "s"}` : minutes < 1_440 ? `${Math.round(minutes / 60)} hour${Math.round(minutes / 60) === 1 ? "" : "s"}` : `${Math.round(minutes / 1_440)} day${Math.round(minutes / 1_440) === 1 ? "" : "s"}`; - return future ? `in ${amount}` : `${amount} ago`; -} diff --git a/apps/persona-scheduler/src/void-moderation-heartbeat-projector.ts b/apps/persona-scheduler/src/void-moderation-heartbeat-projector.ts index 94e8dfe..11eb7c4 100644 --- a/apps/persona-scheduler/src/void-moderation-heartbeat-projector.ts +++ b/apps/persona-scheduler/src/void-moderation-heartbeat-projector.ts @@ -1,5 +1,6 @@ import { voidSelfStateOperationSchema, type VoidSelfStateOperation, type VoidSelfStateTypedProjection } from "@voidbot/core"; import { z } from "zod"; +import { projectRelativeChronology } from "./relative-chronology-projector.js"; const ALLOWED_MODERATION_TAGS = new Set(["moderation:instaban", "moderation:strike", "moderation:case_only"]); @@ -30,7 +31,7 @@ export function projectVoidModerationHeartbeatContext(input: { enforcementMode: string; rules: string; }): VoidModerationHeartbeatContext { - const relative = (value: T): T => projectRelativeChronology(value, input.observedAt) as T; + const relative = (value: T): T => projectRelativeChronology(value, input.observedAt); return { mode: "moderation_heartbeat", operationTimestamp: input.observedAt.toISOString(), @@ -112,22 +113,3 @@ function validateOperation(entry: unknown, index: number, state: VoidSelfStateTy if (moderationTags.length !== 1) throw new Error("Moderation heartbeat open cases must include exactly one moderation classification tag."); return operation; } - -function projectRelativeChronology(value: unknown, observedAt: Date, key = ""): unknown { - if (Array.isArray(value)) return value.map((entry) => projectRelativeChronology(entry, observedAt)); - if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, projectRelativeChronology(entry, observedAt, entryKey)])); - if (typeof value === "string" && /(?:at|timestamp)$/i.test(key) && Number.isFinite(Date.parse(value))) return relativeTime(value, observedAt); - return value; -} - -function relativeTime(value: string, observedAt: Date): string { - const difference = observedAt.getTime() - Date.parse(value); - const future = difference < 0; - const minutes = Math.max(0, Math.round(Math.abs(difference) / 60_000)); - const phrase = (amount: number, unit: string) => future ? `in ${amount} ${unit}${amount === 1 ? "" : "s"}` : `${amount} ${unit}${amount === 1 ? "" : "s"} ago`; - if (minutes < 60) return phrase(minutes, "minute"); - const hours = Math.round(minutes / 60); - if (hours < 24) return phrase(hours, "hour"); - const days = Math.round(hours / 24); - return phrase(days, "day"); -} diff --git a/apps/persona-scheduler/src/void-rumination-organ.ts b/apps/persona-scheduler/src/void-rumination-organ.ts new file mode 100644 index 0000000..8c77ab8 --- /dev/null +++ b/apps/persona-scheduler/src/void-rumination-organ.ts @@ -0,0 +1,53 @@ +import { applyVoidSelfStateOperation, loadVoidSelfStateTypedDocuments } from "@voidbot/core"; +import { buildVoidRuminationPrompt, parseVoidRuminationOperations, projectVoidRuminationContext } from "./void-rumination-projector.js"; + +export type VoidRuminationResult = + | { status: "skipped"; reason: "already_attempted_this_pressure" | "napping_without_pressure"; runnerId?: string } + | { status: "ok"; runnerId: string; appliedOperationCount: number; queuedCandidateIds: string[] }; + +export async function runVoidRumination(input: { + statePath: string; + observedAt?: Date; + pendingMentions?: unknown[]; + recentHistory?: unknown; + repoActivity?: unknown; + recentRoomLinks?: unknown; + recentConversationTarget?: unknown; + publicSpeechTarget?: unknown; + doctrine: string; + rules: string; + voice: string; +}, dependencies: { + loadState?: typeof loadVoidSelfStateTypedDocuments; + applyOperation?: typeof applyVoidSelfStateOperation; + projectText: (prompt: string) => Promise; +}): Promise { + const loadState = dependencies.loadState ?? loadVoidSelfStateTypedDocuments; + const applyOperation = dependencies.applyOperation ?? applyVoidSelfStateOperation; + const state = await loadState({ canonicalPath: input.statePath }); + const observedAt = input.observedAt ?? new Date(); + const context = projectVoidRuminationContext({ ...input, state, observedAt }); + const hasExternalPressure = (input.pendingMentions?.length ?? 0) > 0 || hasObservedItems(input.recentHistory) || hasObservedItems(input.repoActivity) || hasObservedItems(input.recentRoomLinks); + const hasInternalPressure = context.speechPressureObligationIds.length > 0 || state.moderationCursor.openCases.some((entry) => ["pending", "watching"].includes(entry.status)) || state.candidateInterventions.interventions.some((entry) => entry.status === "queued"); + if (state.scheduledRuntime.sleepCycle.isNapping && !hasExternalPressure && !hasInternalPressure) return { status: "skipped", reason: "napping_without_pressure" }; + const runnerId = `void-rumination-attempt:${context.pressureFingerprint}`; + if (state.scheduledRuntime.lastRuns.some((run) => run.runner === runnerId)) return { status: "skipped", reason: "already_attempted_this_pressure", runnerId }; + await applyOperation({ canonicalPath: input.statePath }, { + operation: "record_scheduled_run", + run: { runner: runnerId, ranAt: observedAt.toISOString(), summary: "Started one bounded person-shaped rumination attempt for this pressure fingerprint." }, + }); + const operations = parseVoidRuminationOperations({ outputText: await dependencies.projectText(buildVoidRuminationPrompt(context)), state }); + for (const operation of operations) await applyOperation({ canonicalPath: input.statePath }, operation); + return { + status: "ok", runnerId, appliedOperationCount: operations.length, + queuedCandidateIds: operations.flatMap((operation) => operation.operation === "queue_candidate_intervention" ? [operation.intervention.interventionId] : []), + }; +} + +function hasObservedItems(value: unknown): boolean { + if (Array.isArray(value)) return value.length > 0; + if (!value || typeof value !== "object") return false; + const record = value as Record; + for (const key of ["messages", "items", "commits", "repos"]) if (Array.isArray(record[key]) && record[key].length > 0) return true; + return false; +} diff --git a/apps/persona-scheduler/src/void-rumination-projector.ts b/apps/persona-scheduler/src/void-rumination-projector.ts new file mode 100644 index 0000000..99d2e3d --- /dev/null +++ b/apps/persona-scheduler/src/void-rumination-projector.ts @@ -0,0 +1,127 @@ +import { createHash } from "node:crypto"; +import { voidSelfStateOperationSchema, type VoidSelfStateOperation, type VoidSelfStateTypedProjection } from "@voidbot/core"; +import { projectRelativeChronology } from "./relative-chronology-projector.js"; + +const ALLOWED_OPERATIONS = new Set([ + "upsert_open_case", "close_open_case", "record_short_term_memory", "merge_incubation_support", + "upsert_agency_pressure", "retire_agency_pressure", "upsert_affect_need", "retire_affect_need", + "upsert_social_bond", "retire_social_bond", "upsert_status_read", "retire_status_read", + "update_mood_dimensions", "queue_candidate_intervention", "retire_candidate_intervention", +]); + +export interface VoidRuminationContext { + mode: "person_shaped_rumination"; + operationTimestamp: string; + pressureFingerprint: string; + selfProfile: unknown; + affect: unknown; + memories: unknown[]; + shortTermMemories: unknown[]; + incubation: unknown[]; + agencyPressure: unknown[]; + speechPressureObligationIds: string[]; + candidateInterventions: unknown[]; + speechReceipts: unknown[]; + openCases: unknown[]; + scheduledRuntime: unknown; + pendingMentions: unknown[]; + recentHistory: unknown; + repoActivity: unknown; + recentRoomLinks: unknown; + recentConversationTarget: unknown; + publicSpeechTarget: unknown; + doctrine: string; + rules: string; + voice: string; +} + +export function projectVoidRuminationContext(input: { + state: VoidSelfStateTypedProjection; + observedAt: Date; + pendingMentions?: unknown[]; + recentHistory?: unknown; + repoActivity?: unknown; + recentRoomLinks?: unknown; + recentConversationTarget?: unknown; + publicSpeechTarget?: unknown; + doctrine: string; + rules: string; + voice: string; +}): VoidRuminationContext { + const obligationIds = speechPressureObligationIds(input.state); + const fingerprint = fingerprintVoidRuminationPressure({ + state: input.state, + pendingMentions: input.pendingMentions ?? [], recentHistory: input.recentHistory ?? null, + repoActivity: input.repoActivity ?? null, recentRoomLinks: input.recentRoomLinks ?? null, + }); + const relative = (value: T): T => projectRelativeChronology(value, input.observedAt); + return { + mode: "person_shaped_rumination", operationTimestamp: input.observedAt.toISOString(), pressureFingerprint: fingerprint, + selfProfile: relative(input.state.selfProfile), affect: relative(input.state.faceAffect), + memories: relative(input.state.thoughtMemory.memories), shortTermMemories: relative(input.state.thoughtMemory.shortTerm), + incubation: relative(input.state.thoughtMemory.incubation), agencyPressure: relative(input.state.agencyPressure.pressures), + speechPressureObligationIds: obligationIds, candidateInterventions: relative(input.state.candidateInterventions.interventions), + speechReceipts: relative(input.state.speechReceipts.recentReceipts), openCases: relative(input.state.moderationCursor.openCases), + scheduledRuntime: relative(input.state.scheduledRuntime), pendingMentions: relative(input.pendingMentions ?? []), + recentHistory: relative(input.recentHistory ?? null), repoActivity: relative(input.repoActivity ?? null), + recentRoomLinks: relative(input.recentRoomLinks ?? null), recentConversationTarget: input.recentConversationTarget ?? null, + publicSpeechTarget: input.publicSpeechTarget ?? null, doctrine: input.doctrine, rules: input.rules, voice: input.voice, + }; +} + +export function buildVoidRuminationPrompt(context: VoidRuminationContext): string { + return [ + "You are Void's private person-shaped rumination organ. Think as a participant with values and relationships, not as a maintenance report.", + "Return only a JSON array of complete typed Void self-state operation objects. No markdown, commentary, tools, file writes, or direct transport calls.", + `Allowed operations: ${[...ALLOWED_OPERATIONS].join(", ")}.`, + "New memories must be short-term and meaning-preserving. Queue speech only as candidate interventions; the parent delivery organ owns transport and receipts.", + "A reply candidate must retain the source channel and message in deliveryTarget. publicSpeechTarget is only for unanchored public artifacts or herald speech.", + "For every speechPressureObligationId, queue a live candidate tagged source_pressure:, retire the pressure, or upsert it with cooling/resolved status. Silent [] is invalid while obligations exist.", + "Use operationTimestamp for fresh exact timestamps. Relative chronology is evidence only.", + "If no anchored state change or sayable pressure exists, return [].", + "Rumination context:", JSON.stringify(context, null, 2), + ].join("\n\n"); +} + +export function parseVoidRuminationOperations(input: { outputText: string; state: VoidSelfStateTypedProjection }): VoidSelfStateOperation[] { + const trimmed = input.outputText.trim(); + const candidate = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim() ?? trimmed; + let raw: unknown; + try { raw = JSON.parse(candidate); } catch (error) { throw new Error(`Void rumination output is not valid JSON: ${error instanceof Error ? error.message : String(error)}`); } + if (!Array.isArray(raw)) throw new Error("Void rumination output must be a JSON array."); + const operations = raw.map((entry, index) => { + const operation = voidSelfStateOperationSchema.parse(entry); + if (!ALLOWED_OPERATIONS.has(operation.operation)) throw new Error(`Void rumination operation ${index + 1} is not allowed: ${operation.operation}.`); + return operation; + }); + assertSpeechPressureResolved(input.state, operations); + return operations; +} + +export function fingerprintVoidRuminationPressure(input: { state: VoidSelfStateTypedProjection; pendingMentions: unknown[]; recentHistory: unknown; repoActivity: unknown; recentRoomLinks: unknown }): string { + const state = input.state; + const pressure = { + affect: state.faceAffect, shortTerm: state.thoughtMemory.shortTerm, incubation: state.thoughtMemory.incubation, + agency: state.agencyPressure.pressures, candidates: state.candidateInterventions.interventions, + openCases: state.moderationCursor.openCases, receipts: state.speechReceipts.recentReceipts.slice(-16), + pendingMentions: input.pendingMentions, recentHistory: input.recentHistory, repoActivity: input.repoActivity, recentRoomLinks: input.recentRoomLinks, + }; + return createHash("sha256").update(JSON.stringify(pressure)).digest("hex").slice(0, 24); +} + +function speechPressureObligationIds(state: VoidSelfStateTypedProjection): string[] { + return state.agencyPressure.pressures + .filter((pressure) => ["active", "ready_to_act"].includes(pressure.status) && pressure.intensity >= 0.55) + .filter((pressure) => !state.candidateInterventions.interventions.some((candidate) => ["queued", "spoken"].includes(candidate.status) && candidate.tags.includes(`source_pressure:${pressure.pressureId}`))) + .map((pressure) => pressure.pressureId); +} + +function assertSpeechPressureResolved(state: VoidSelfStateTypedProjection, operations: VoidSelfStateOperation[]): void { + for (const pressureId of speechPressureObligationIds(state)) { + const resolved = operations.some((operation) => + (operation.operation === "queue_candidate_intervention" && operation.intervention.status === "queued" && operation.intervention.tags.includes(`source_pressure:${pressureId}`)) + || (operation.operation === "retire_agency_pressure" && operation.pressureId === pressureId) + || (operation.operation === "upsert_agency_pressure" && operation.pressure.pressureId === pressureId && ["cooling", "resolved", "retired"].includes(operation.pressure.status))); + if (!resolved) throw new Error(`Active advocacy pressure '${pressureId}' requires a candidate intervention or explicit cooling/retirement.`); + } +} diff --git a/apps/persona-scheduler/src/void-rumination-text-actuator.ts b/apps/persona-scheduler/src/void-rumination-text-actuator.ts new file mode 100644 index 0000000..7118084 --- /dev/null +++ b/apps/persona-scheduler/src/void-rumination-text-actuator.ts @@ -0,0 +1,10 @@ +import type { loadConfig } from "@voidbot/config"; +import { requestBoundedTextCompletion } from "./bounded-text-completion-actuator.js"; + +export async function projectVoidRuminationOperations(input: { prompt: string; config: ReturnType; model?: string }, dependencies: { fetch?: typeof fetch; readText?: (path: string) => Promise } = {}): Promise { + return requestBoundedTextCompletion({ + prompt: input.prompt, config: input.config, purpose: "Void person-shaped rumination", + model: input.model ?? process.env.VOID_RUMINATION_MODEL, + maxCompletionTokens: Number(process.env.VOID_RUMINATION_MAX_COMPLETION_TOKENS) || 3072, + }, dependencies); +} diff --git a/scripts/smoke-persona-initiative-engine.ts b/scripts/smoke-persona-initiative-engine.ts index 9fd0bc6..e568c79 100644 --- a/scripts/smoke-persona-initiative-engine.ts +++ b/scripts/smoke-persona-initiative-engine.ts @@ -89,6 +89,9 @@ import { readVoidModerationEvidence } from "../apps/persona-scheduler/dist/void- import { runVoidModerationHeartbeat } from "../apps/persona-scheduler/dist/void-moderation-heartbeat-organ.js"; import { projectVoidModerationOperations } from "../apps/persona-scheduler/dist/void-moderation-text-actuator.js"; import { runVoidModerationEnforcement } from "../apps/persona-scheduler/dist/void-moderation-enforcement-organ.js"; +import { buildVoidRuminationPrompt, parseVoidRuminationOperations, projectVoidRuminationContext } from "../apps/persona-scheduler/dist/void-rumination-projector.js"; +import { runVoidRumination } from "../apps/persona-scheduler/dist/void-rumination-organ.js"; +import { projectVoidRuminationOperations } from "../apps/persona-scheduler/dist/void-rumination-text-actuator.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"; @@ -924,6 +927,51 @@ assert.equal(bannedUserId, "human-1", "policy enforcement lowers an instaban thr assert.equal(moderationEnforcement.actions[0]?.status, "instaban_applied", "successful sanction closes the typed case with an enforcement result"); const repeatedEnforcement = await runVoidModerationEnforcement({ statePath: "void-self-state.cc", mode: "policy", botToken: "token", guildId: "guild", observedAt: physiologyObservedAt }, { loadState: async () => moderationState }); assert.equal(repeatedEnforcement.evaluatedCaseCount, 0, "resolved moderation debt cannot be sanctioned twice on later cadences"); +const ruminationState = createEmptyVoidSelfState({ createdAt: "2026-07-15T00:00:00.000Z" }); +ruminationState.scheduledRuntime.sleepCycle.isNapping = false; +ruminationState.agencyPressure.pressures.push({ + pressureId: "pressure-1", kind: "world_advocacy_request", status: "active", target: { kind: "room", id: "aquarium" }, + summary: "A direct room claim deserves an answer.", claim: "The reply belongs in its source room.", tension: "Silence would leave the claim unanswered.", + actionImplication: "Queue a source-anchored reply.", intensity: 0.8, anchorRefs: [{ ref: "discord:message-1" }], evidenceRefs: [], sourceMemoryIds: [], + createdAt: physiologyObservedAt.toISOString(), updatedAt: physiologyObservedAt.toISOString(), tags: [], +} as never); +const ruminationContext = projectVoidRuminationContext({ state: ruminationState, observedAt: physiologyObservedAt, pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." }); +const laterRuminationContext = projectVoidRuminationContext({ state: ruminationState, observedAt: new Date("2026-07-16T04:00:00.000Z"), pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." }); +assert.equal(ruminationContext.pressureFingerprint, laterRuminationContext.pressureFingerprint, "wall-clock projection cannot manufacture a fresh rumination intent"); +assert.match(buildVoidRuminationPrompt(ruminationContext), /source_pressure:/, "rumination prompt projects explicit speech-pressure accountability"); +assert.throws(() => parseVoidRuminationOperations({ outputText: "[]", state: ruminationState }), /requires a candidate/, "strong advocacy pressure cannot be silently acknowledged"); +assert.throws(() => parseVoidRuminationOperations({ outputText: JSON.stringify([{ operation: "propose_memory_distillation", proposal: {} }]), state: ruminationState }), /invalid|not allowed/i, "awake rumination cannot annex sleep-owned durable-memory policy"); +const ruminationCandidate = { + operation: "queue_candidate_intervention", intervention: { + interventionId: "candidate-1", kind: "world_advocacy", status: "queued", target: { kind: "room", id: "room-1" }, + summary: "Answer the source-room claim.", draft: "This answer stays with the conversation that made it necessary.", + deliveryTarget: { channelId: "room-1", replyToMessageId: "message-1" }, priority: 0.8, mustEventuallyShare: true, + createdAt: physiologyObservedAt.toISOString(), updatedAt: physiologyObservedAt.toISOString(), tags: ["source_pressure:pressure-1"], + }, +}; +const parsedRumination = parseVoidRuminationOperations({ outputText: JSON.stringify([ruminationCandidate]), state: ruminationState }); +assert.equal(parsedRumination[0]?.operation, "queue_candidate_intervention", "person-shaped rumination crosses the typed operation boundary without delivering speech"); +let ruminationModelCalls = 0; +const ruminationDependencies = { + loadState: async () => ruminationState, + projectText: async () => { ruminationModelCalls += 1; return ruminationModelCalls === 1 ? JSON.stringify([ruminationCandidate]) : "[]"; }, + applyOperation: async (_store: unknown, operation: { operation: string; run?: { runner: string; ranAt: string; summary: string }; intervention?: unknown }) => { + if (operation.operation === "record_scheduled_run" && operation.run) ruminationState.scheduledRuntime.lastRuns.push(operation.run); + if (operation.operation === "queue_candidate_intervention" && operation.intervention) { + const index = ruminationState.candidateInterventions.interventions.findIndex((entry) => entry.interventionId === (operation.intervention as { interventionId: string }).interventionId); + if (index >= 0) ruminationState.candidateInterventions.interventions[index] = operation.intervention as never; + else ruminationState.candidateInterventions.interventions.push(operation.intervention as never); + } + return ruminationState; + }, +}; +const ruminated = await runVoidRumination({ statePath: "void-self-state.cc", observedAt: physiologyObservedAt, pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." }, ruminationDependencies as never); +assert.equal(ruminated.status, "ok", "the rumination coordinator applies one bounded pressure pass"); +const repeatedRumination = await runVoidRumination({ statePath: "void-self-state.cc", observedAt: new Date("2026-07-16T04:00:00.000Z"), pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." }, ruminationDependencies as never); +assert.equal(repeatedRumination.status, "ok", "a meaning-bearing state change may authorize the next rumination pressure fingerprint"); +const settledRumination = await runVoidRumination({ statePath: "void-self-state.cc", observedAt: new Date("2026-07-16T05:00:00.000Z"), pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." }, ruminationDependencies as never); +assert.equal(settledRumination.status, "skipped", "an unchanged settled pressure cannot become a timer-driven inference loop"); +assert.equal(ruminationModelCalls, 2, "rumination cadence follows changed pressure rather than elapsed clock"); const maintainedState = structuredClone(physiologyState); let memoryModelCalls = 0; const memoryMaintenanceDependencies = { @@ -955,6 +1003,16 @@ const memoryModelOutput = await projectVoidMemoryOperations({ prompt: "Maintain assert.equal(memoryModelOutput, "[]", "the portable memory actuator returns only model text to the operation parser"); assert.equal(memoryRequestBody?.model, "memory-model", "the memory actuator uses one explicit configured model and bounded completion budget"); assert.equal(memoryRequestBody?.tools, undefined, "memory maintenance cannot acquire filesystem or side-effect tools through the model actuator"); +let ruminationRequestBody: Record | undefined; +const ruminationModelOutput = await projectVoidRuminationOperations({ prompt: "Ruminate from typed pressure.", config: { openAiApi: { baseUrl: "https://model.invalid/v1", apiKey: "secret", model: "rumination-model", timeoutMs: 1000, authHeader: "Authorization", maxCompletionTokens: 512 } } as never }, { + fetch: async (_url, init) => { + ruminationRequestBody = JSON.parse(String(init?.body)); + return new Response(JSON.stringify({ choices: [{ message: { content: "[]" } }] }), { status: 200 }); + }, +}); +assert.equal(ruminationModelOutput, "[]", "the rumination actuator returns only model text to the typed parser"); +assert.equal(ruminationRequestBody?.model, "rumination-model", "rumination uses one bounded configured text model"); +assert.equal(ruminationRequestBody?.tools, undefined, "person-shaped rumination cannot acquire transport or filesystem tools"); let moderationRequestBody: Record | undefined; const moderationModelOutput = await projectVoidModerationOperations({ prompt: "Review moderation evidence.", config: { openAiApi: { baseUrl: "https://model.invalid/v1", apiKey: "secret", model: "moderation-model", timeoutMs: 1000, authHeader: "Authorization", maxCompletionTokens: 512 } } as never }, { fetch: async (_url, init) => {