Delete Windows Void rumination authority
This commit is contained in:
parent
b09267ed34
commit
5b35fd9fd2
25 changed files with 42 additions and 3561 deletions
23
README.md
23
README.md
|
|
@ -563,23 +563,21 @@ Defaults come from `.env` / `.env.example`:
|
|||
|
||||
The watchdog task also uses `Interactive` logon because the remote freshness check relies on the same local SSH key material as the offsite sync path.
|
||||
|
||||
## Moderator Review Loop
|
||||
## Resident Void Organs
|
||||
|
||||
Void can also keep a sandboxed little moderator brain running on a schedule without turning the live bot into an auto-ban carnival.
|
||||
Void's moderation, person-shaped rumination, physiology, memory maintenance, and candidate delivery run as separately gated resident daemon organs on Yggdrasil.
|
||||
|
||||
Tracked prompt surfaces:
|
||||
|
||||
- `config/discord-server-rules.md`
|
||||
- `config/moderation-review-agent.md`
|
||||
- `config/void-rumination-doctrine.md`
|
||||
- `prompts/void-memory-maintenance.md`
|
||||
- `prompts/void-moderator-rumination.md`
|
||||
- `styles/void-default.md`
|
||||
|
||||
Local moderation-state surfaces:
|
||||
|
||||
- `.voidbot/private/void-self-state.cc`: canonical typed CultCache self-state
|
||||
- `.voidbot/status/moderation-rumination-context.json`: bounded prompt-facing run context
|
||||
- `.voidbot/status/moderation-rumination-operations.json`: child-proposed typed operation payloads
|
||||
- `.voidbot/status/void-memory-maintenance-context.json`: bounded prompt-facing memory-maintenance context
|
||||
- `.voidbot/status/void-memory-maintenance-operations.json`: child-proposed memory/incubation/candidate operations
|
||||
|
||||
|
|
@ -613,20 +611,9 @@ If one of the swarm agents should speak as itself through the shared Discord pip
|
|||
node scripts/send-discord-message.mjs --channel-id <channel-id> --persona-name "Ghostlight Archivist" --persona-avatar-url "https://example.com/avatar.png"
|
||||
```
|
||||
|
||||
Install or refresh the scheduled task:
|
||||
Moderation heartbeat, person-shaped rumination, physiology, memory maintenance, and candidate delivery are resident daemon organs on Yggdrasil. They are configured independently and deployed only by pushing upstream for Idunn; do not install local scheduled-task copies.
|
||||
|
||||
```bash
|
||||
npm run state:install-moderation-task
|
||||
npm run state:install-mood-task
|
||||
```
|
||||
|
||||
The runner records its pulse here:
|
||||
|
||||
- `.voidbot/status/moderation-rumination.json`
|
||||
- `.voidbot/logs/moderation-rumination.log`
|
||||
- `.voidbot/logs/moderation-rumination-last.jsonl`
|
||||
|
||||
That helper exists because semantic `search_history` is good at recall and bad at simple heartbeat duty. The moderator loop can use it to see what changed since its last cursor, then fall back to `search_history`, `get_message_context`, `list_indexed_repos`, `search_sources`, and `get_source_context` when it wants deeper social or project context.
|
||||
Chronological Discord evidence is acquired by the daemon's bounded evidence source. Semantic retrieval remains available for deeper context, but it does not own the heartbeat clock or cursor.
|
||||
There is also a repo-weather helper:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { stat } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { createStateStorage } from "@voidbot/core";
|
||||
|
||||
|
|
@ -72,9 +70,6 @@ export async function scanActivePersonaTurns(
|
|||
}
|
||||
active.set(identityId, job.id);
|
||||
}
|
||||
if (await readRecentLock(resolve(config.storageRoot, "status", "moderation-rumination.lock"), 20, nowMs)) {
|
||||
active.set("void", "lock:moderation-rumination");
|
||||
}
|
||||
return { active, staleRecovered };
|
||||
} finally {
|
||||
await storage.close();
|
||||
|
|
@ -93,14 +88,3 @@ export function parsePersonaTurnIdentity(requestMessageId: string | undefined):
|
|||
export function staleActiveTurnThresholdMs(codexExecTimeoutMs: number): number {
|
||||
return Math.max(MIN_STALE_ACTIVE_JOB_MS, codexExecTimeoutMs * 3);
|
||||
}
|
||||
|
||||
async function readRecentLock(path: string, maxAgeMinutes: number, nowMs: number): Promise<boolean> {
|
||||
try {
|
||||
const info = await stat(path);
|
||||
const ageMs = nowMs - info.mtimeMs;
|
||||
return ageMs >= 0 && ageMs < maxAgeMinutes * 60_000;
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ function runTick(): Promise<void> {
|
|||
console.log(`Persona scheduler tick completed (started ${startedAt}). ${JSON.stringify(result)}`);
|
||||
if (Date.now() - lastPhysiologyAt >= physiologyIntervalMs) {
|
||||
try {
|
||||
const physiology = await runVoidPhysiologyOrgan({ statePath: resolve(config.storageRoot, "private", "void-self-state.cc"), statusDirectory: resolve(config.storageRoot, "status") });
|
||||
const physiology = await runVoidPhysiologyOrgan({ statePath: resolve(config.storageRoot, "private", "void-self-state.cc") });
|
||||
lastPhysiologyAt = Date.now();
|
||||
console.log(`Void physiology pulse completed. ${JSON.stringify(physiology)}`);
|
||||
if (physiology.memoryMaintenanceIntent) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { projectNativePersonaBody } from "./persona-standard-state-projector.js"
|
|||
import { readGlobalAgentDoctrine } from "./global-agent-doctrine-source.js";
|
||||
import { assemblePersonaTurn } from "./persona-turn-assembler.js";
|
||||
import { submitPersonaTurn } from "./turn-actuator.js";
|
||||
import { launchVoidModerationTurn } from "./void-moderation-turn-actuator.js";
|
||||
import { readRepoActivity } from "./repo-activity-source.js";
|
||||
import { projectRepoActivityObservation } from "./repo-activity-projector.js";
|
||||
import { buildPersonaChannelPlan, newestPendingMentionChannel, personaChannelSpeedMultiplier } from "./turn-routing.js";
|
||||
|
|
@ -32,10 +31,8 @@ export interface PersonaTurnStartReceipt {
|
|||
failureReason?: string;
|
||||
}
|
||||
|
||||
export function buildPersonaParticipantSpecs(identities: RepoDiscordIdentity[], includeVoid: boolean): PersonaParticipantSpec[] {
|
||||
return [
|
||||
...(includeVoid ? [{ id: "void", participantKind: "system_agent" as const, turnKind: "void_moderation" as const, repoName: "VoidBot", displayName: "Void", allowedChannelIds: [], channelSpeedMultiplier: 1 }] : []),
|
||||
...identities.map((identity) => ({
|
||||
export function buildPersonaParticipantSpecs(identities: RepoDiscordIdentity[]): PersonaParticipantSpec[] {
|
||||
return identities.map((identity) => ({
|
||||
id: identity.id,
|
||||
participantKind: identity.identityKind === "native_persona" ? "native_persona" as const : "repo_face" as const,
|
||||
turnKind: "repo_face_rumination" as const,
|
||||
|
|
@ -44,8 +41,7 @@ export function buildPersonaParticipantSpecs(identities: RepoDiscordIdentity[],
|
|||
allowedChannelIds: getRepoDiscordIdentityAllowedChannelIds(identity),
|
||||
channelSpeedMultiplier: personaChannelSpeedMultiplier(identity),
|
||||
identity,
|
||||
})),
|
||||
];
|
||||
}));
|
||||
}
|
||||
|
||||
export function pendingMentionsForPersona(state: { pendingMentions: RepoFacePendingMention[] }, identityId: string): RepoFacePendingMention[] {
|
||||
|
|
@ -69,7 +65,6 @@ export async function dispatchPersonaParticipantTurn(input: {
|
|||
queuedAt: string;
|
||||
personaStateObservation?: PersonaStateObservation;
|
||||
}): Promise<PersonaTurnStartReceipt> {
|
||||
if (input.participant.turnKind === "void_moderation") return launchVoidModerationTurn({ queuedAt: input.queuedAt, storageRoot: input.config.storageRoot, pendingMentions: input.pendingMentions });
|
||||
const identity = input.registryIdentities.find((entry) => entry.id === input.participant.identityId);
|
||||
if (!identity) return { created: false, failureReason: `No registered Persona identity exists for ${input.participant.identityId}.` };
|
||||
const channelPlan = buildPersonaChannelPlan(identity, input.config.repoFaceHeartbeats.defaultChannelId, newestPendingMentionChannel(input.pendingMentions));
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export async function runPersonaSchedulerTick(input: {
|
|||
const globalHeat = (await readSwarmControlState())?.globalHeat ?? config.repoFaceHeartbeats.globalHeat;
|
||||
applySchedulerControls({ state, baseRecoveryMinutes: config.repoFaceHeartbeats.baseRecoveryMinutes, globalHeat });
|
||||
const completedThisTick = new Set<string>();
|
||||
const participantSpecs = buildPersonaParticipantSpecs(registry.identities, config.voidModerationHeartbeatEnabled);
|
||||
const participantSpecs = buildPersonaParticipantSpecs(registry.identities);
|
||||
reconcileInitiativeParticipants({ state, specs: participantSpecs, defaultChannelId: config.repoFaceHeartbeats.defaultChannelId, speedOverrides: config.repoFaceHeartbeats.speedOverrides, heatOverrides: config.repoFaceHeartbeats.heatOverrides, baseRecoveryMinutes: config.repoFaceHeartbeats.baseRecoveryMinutes, globalHeat, activeTurns: activeTurnScan.active, completedThisTick });
|
||||
rescheduleStaleOverdueParticipants(state);
|
||||
applyPendingMentionPriority(state);
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import { stat } from "node:fs/promises";
|
||||
|
||||
export async function readVoidModerationActivity(input: {
|
||||
lockPath: string;
|
||||
observedAt?: Date;
|
||||
recentWithinMs?: number;
|
||||
}): Promise<{ active: boolean; lockPath: string; ageMs?: number }> {
|
||||
try {
|
||||
const info = await stat(input.lockPath);
|
||||
const ageMs = (input.observedAt ?? new Date()).getTime() - info.mtimeMs;
|
||||
return { active: ageMs >= 0 && ageMs < (input.recentWithinMs ?? 20 * 60_000), lockPath: input.lockPath, ageMs };
|
||||
} catch {
|
||||
return { active: false, lockPath: input.lockPath };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import { mkdir, stat, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import type { RepoFacePendingMention } from "@voidbot/core";
|
||||
|
||||
export interface VoidModerationTurnInput {
|
||||
queuedAt: string;
|
||||
storageRoot: string;
|
||||
pendingMentions: RepoFacePendingMention[];
|
||||
workspaceRoot?: string;
|
||||
handshakeTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface VoidModerationTurnReceipt {
|
||||
created: boolean;
|
||||
activeJobId?: string;
|
||||
requestMessageId: string;
|
||||
failureReason?: string;
|
||||
}
|
||||
|
||||
interface VoidModerationActuatorDependencies {
|
||||
launch?: (command: string, args: string[], options: { cwd: string; stdio: "ignore"; windowsHide: true }) => { pid?: number; unref(): void };
|
||||
now?: () => number;
|
||||
pause?: (milliseconds: number) => Promise<void>;
|
||||
touchedAfter?: (path: string, timestampMs: number) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export async function launchVoidModerationTurn(
|
||||
input: VoidModerationTurnInput,
|
||||
dependencies: VoidModerationActuatorDependencies = {},
|
||||
): Promise<VoidModerationTurnReceipt> {
|
||||
const workspaceRoot = resolve(input.workspaceRoot ?? process.cwd());
|
||||
const runnerScript = resolve(workspaceRoot, "scripts", "run-void-moderator-rumination.ps1");
|
||||
const statusDir = resolve(input.storageRoot, "status");
|
||||
const lockPath = resolve(statusDir, "moderation-rumination.lock");
|
||||
const statusPath = resolve(statusDir, "moderation-rumination.json");
|
||||
const pendingMentionsPath = resolve(statusDir, "void-moderation-pending-mentions.json");
|
||||
const now = dependencies.now ?? Date.now;
|
||||
const launchedAt = now();
|
||||
await mkdir(statusDir, { recursive: true });
|
||||
await writeFile(pendingMentionsPath, `${JSON.stringify({ generatedAt: input.queuedAt, pendingMentions: input.pendingMentions }, null, 2)}\n`, "utf8");
|
||||
|
||||
const launchCommand = buildVoidModerationLaunchCommand({ pendingMentionsPath, runnerScript, workspaceRoot });
|
||||
const launch = dependencies.launch ?? ((command, args, options) => spawn(command, args, options));
|
||||
const child = launch("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
launchCommand,
|
||||
], { cwd: workspaceRoot, stdio: "ignore", windowsHide: true });
|
||||
child.unref();
|
||||
|
||||
const handshake = await waitForVoidModerationHandshake({
|
||||
lockPath,
|
||||
statusPath,
|
||||
launchedAt,
|
||||
timeoutMs: input.handshakeTimeoutMs ?? 60_000,
|
||||
now,
|
||||
pause: dependencies.pause,
|
||||
touchedAfter: dependencies.touchedAfter,
|
||||
});
|
||||
const requestMessageId = `agent-turn:void:${input.queuedAt}`;
|
||||
if (!handshake.started) return {
|
||||
created: false,
|
||||
activeJobId: child.pid ? `launcher-process:${child.pid}` : undefined,
|
||||
requestMessageId,
|
||||
failureReason: handshake.reason,
|
||||
};
|
||||
return { created: true, activeJobId: `process:void-moderation:${input.queuedAt}`, requestMessageId };
|
||||
}
|
||||
|
||||
export function buildVoidModerationLaunchCommand(input: {
|
||||
pendingMentionsPath: string;
|
||||
runnerScript: string;
|
||||
workspaceRoot: string;
|
||||
}): string {
|
||||
const argumentsList = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", input.runnerScript]
|
||||
.map(toPowerShellSingleQuotedString)
|
||||
.join(", ");
|
||||
return [
|
||||
`$env:VOID_RUMINATION_PENDING_MENTIONS_PATH = ${toPowerShellSingleQuotedString(input.pendingMentionsPath)};`,
|
||||
`$arguments = @(${argumentsList});`,
|
||||
`Start-Process -FilePath 'powershell.exe' -ArgumentList $arguments -WorkingDirectory ${toPowerShellSingleQuotedString(input.workspaceRoot)} -WindowStyle Hidden;`,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
export async function waitForVoidModerationHandshake(input: {
|
||||
lockPath: string;
|
||||
statusPath: string;
|
||||
launchedAt: number;
|
||||
timeoutMs: number;
|
||||
now?: () => number;
|
||||
pause?: (milliseconds: number) => Promise<void>;
|
||||
touchedAfter?: (path: string, timestampMs: number) => Promise<boolean>;
|
||||
}): Promise<{ started: true } | { started: false; reason: string }> {
|
||||
const now = input.now ?? Date.now;
|
||||
const pause = input.pause ?? ((milliseconds) => new Promise((resolvePause) => setTimeout(resolvePause, milliseconds)));
|
||||
const touchedAfter = input.touchedAfter ?? wasTouchedAfter;
|
||||
const deadline = now() + input.timeoutMs;
|
||||
while (now() < deadline) {
|
||||
if (await touchedAfter(input.lockPath, input.launchedAt)) return { started: true };
|
||||
if (await touchedAfter(input.statusPath, input.launchedAt)) return { started: true };
|
||||
await pause(250);
|
||||
}
|
||||
return { started: false, reason: "void_moderation_launch_handshake_missing" };
|
||||
}
|
||||
|
||||
function toPowerShellSingleQuotedString(value: string): string {
|
||||
return `'${value.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
async function wasTouchedAfter(path: string, timestampMs: number): Promise<boolean> {
|
||||
try {
|
||||
return (await stat(path)).mtimeMs >= timestampMs - 500;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +1,23 @@
|
|||
import { resolve } from "node:path";
|
||||
import { applyVoidSelfStateOperation, loadVoidSelfStateTypedDocuments } from "@voidbot/core";
|
||||
import { readVoidModerationActivity } from "./void-moderation-activity-source.js";
|
||||
import { projectVoidPhysiology, type VoidPhysiologyProjection } from "./void-physiology-domain.js";
|
||||
|
||||
export interface VoidPhysiologyOrganResult extends VoidPhysiologyProjection {
|
||||
ok: true;
|
||||
observedAt: string;
|
||||
statePath: string;
|
||||
moderationLockPath: string;
|
||||
}
|
||||
|
||||
export async function runVoidPhysiologyOrgan(input: {
|
||||
statePath: string;
|
||||
statusDirectory: string;
|
||||
observedAt?: Date;
|
||||
moderationActive?: boolean;
|
||||
}, dependencies: {
|
||||
loadState?: typeof loadVoidSelfStateTypedDocuments;
|
||||
applyOperation?: typeof applyVoidSelfStateOperation;
|
||||
readModerationActivity?: typeof readVoidModerationActivity;
|
||||
} = {}): Promise<VoidPhysiologyOrganResult> {
|
||||
const observedAt = input.observedAt ?? new Date();
|
||||
const moderationLockPath = resolve(input.statusDirectory, "moderation-rumination.lock");
|
||||
const [state, moderation] = await Promise.all([
|
||||
(dependencies.loadState ?? loadVoidSelfStateTypedDocuments)({ canonicalPath: input.statePath }),
|
||||
(dependencies.readModerationActivity ?? readVoidModerationActivity)({ lockPath: moderationLockPath, observedAt }),
|
||||
]);
|
||||
const projection = projectVoidPhysiology({ state, observedAt, moderationActive: moderation.active });
|
||||
const state = await (dependencies.loadState ?? loadVoidSelfStateTypedDocuments)({ canonicalPath: input.statePath });
|
||||
const projection = projectVoidPhysiology({ state, observedAt, moderationActive: input.moderationActive ?? false });
|
||||
for (const operation of projection.operations) await (dependencies.applyOperation ?? applyVoidSelfStateOperation)({ canonicalPath: input.statePath }, operation);
|
||||
return { ok: true, observedAt: observedAt.toISOString(), statePath: input.statePath, moderationLockPath, ...projection };
|
||||
return { ok: true, observedAt: observedAt.toISOString(), statePath: input.statePath, ...projection };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
|
||||
- VoidBot's deployed runtime lives on Yggdrasil. Idunn owns deployment from upstream `main` and proves the served commit through `/srv/voidbot/deploy/deployment.env`. This workspace is for development and diagnostics; it is not a deployment target. Do not look for, create, or revive a local VoidBot deployment. To deploy a completed change, push the intended commit to upstream `main`, then verify Idunn converged `DEPLOYED_REVISION` and the Yggdrasil service set.
|
||||
- Discord bot and worker are live.
|
||||
- The unattended repo Face/native Persona CTB swarm now runs on Yggdrasil. Resident `apps/persona-scheduler` inside `voidbot.service` owns the serialized Persona pulse and queues turns through Postgres for the worker. The Windows `GameCult Local Orchestrator` no longer owns Persona scheduling. Void moderation, mood, watchdog, and Bifrost dispatch are still separate migration work and must not be folded into this scheduler.
|
||||
- The unattended repo Face/native Persona CTB swarm runs on Yggdrasil. Resident `apps/persona-scheduler` inside `voidbot.service` owns the serialized Persona pulse and queues turns through Postgres for the worker. Void moderation, rumination, physiology, memory maintenance, and candidate delivery are now separate resident daemon organs; Persona initiative owns none of them. The Windows Local Orchestrator owns no Void decision loop.
|
||||
- The teardown authority remains `notes/agent-swarm-teardown-plan-2026-05-21.md`, but it is now a restart checklist and smell ledger rather than a stop sign. Keep using it when a Face/Bifrost/CTB boundary smells like prompt/parser/transport Jenga.
|
||||
- The repo-controlled pause flag is `state/agent-swarm-paused.json`. While it is missing or has `"paused": false`, swarm runners may operate. While it has `"paused": true` or is malformed, the Face heartbeat runner skips and the orchestrator skips agent-swarm organs. This is the brake. Do not route around it.
|
||||
- Postgres owns jobs, audit events, interaction memory, and rate-limit state.
|
||||
|
|
@ -35,7 +35,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- Read-only projection is now owned by `packages/core/src/void-self-state-projection.ts`. `loadVoidSelfState` is a thin loader that reads typed `.cc` documents and delegates the agent-facing summary plus nap/reply-mode projection to that module.
|
||||
- Typed operations are applied through `packages/core/src/void-self-state-service.ts` and `scripts/void-self-state.mjs apply-operation`. The service now writes typed documents only; it no longer registers, reads, updates, or mirrors the old moderation monolith during routine operations.
|
||||
- `scripts/export-recent-repo-activity.mjs` reads repo cursors from the typed `.cc` store. Legacy `.json` cursor arguments are only mapped to a sibling `.cc` path when needed by old invocation surfaces.
|
||||
- `scripts/run-void-moderator-rumination.ps1` has been rebuilt as a typed rumination runner. It builds a typed context packet, loads `prompts/void-moderator-rumination.md`, runs Codex rumination, and applies only the JSON operation payloads that come back through `scripts/void-self-state.mjs`. Cursor and receipt bookkeeping remain parent-owned.
|
||||
- The Windows rumination runner, installer, CTB moderation launcher, and PowerShell-only rumination/moderation fixtures are deleted. Their live invariants now belong to the resident TypeScript moderation, rumination, enforcement, and candidate-delivery organs and the consolidated initiative-engine smoke.
|
||||
- Prompt-facing rumination chronology is projected by `scripts/lib/void-rumination-context-projection.ps1`. Exact timestamps stay in typed state/status/cursors; the child context gets relative phrases like `5 minutes ago`, including recent Discord messages, repo activity, receipts, memory timestamps, and sleep/speech timing.
|
||||
- `scripts/simulate-void-mood.mjs` owns typed sleep/speaking-pressure over `.voidbot/private/void-self-state.cc`. During sleep, it invokes typed memory maintenance once per nap unless that nap already completed a pass; use `--skip-memory-maintenance` for smoke runs that must not call Codex.
|
||||
- `npm run moderation:memory-maintenance:sleep-fixture` now proves the non-skip sleep path without spending a live model call: it seeds a temporary `.cc`, forces mood drift through sleep maintenance, uses a fake Codex child to propose `apply_memory_distillation`, and verifies short-term residue is gone while the durable memory keeps subject, claim, tension, action implication, and anchor.
|
||||
|
|
@ -85,8 +85,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- Source repo discovery now reconciles automatically during `npm run stack:start` and through the watchdog path, so new repos under `SOURCE_REPO_ROOT` stop waiting for manual hook folklore before they get indexed.
|
||||
- Bot-directed prompts now stay in the raw Discord archive but are excluded from default semantic history indexing and lexical fallback, so repeated summons stop teaching the bot its own call-and-response loops.
|
||||
- Source-grounding hints are advisory now; the owner and local lanes can decide when repo or lore tool use is actually warranted instead of obeying a lexical front-door veto.
|
||||
- The moderation heartbeat organ is invoked every orchestrator pulse against `.voidbot/private/void-self-state.cc` with `prompts/void-moderation-heartbeat.md`, which exposes only rules, recent history, urgent witnesses, and infringement history. The moderation rumination organ is still invoked by `GameCult Local Orchestrator` for Void's slower person-shaped participation and possible announcement turns; its prompt lives in `prompts/void-moderator-rumination.md`. Treat both files as sensitive behavior surfaces, not incidental script text.
|
||||
- The moderation loop now uses the typed context packet from `scripts/run-void-moderator-rumination.ps1`; legacy Ghostlight-shaped JSON runtime fields are not the active memory surface.
|
||||
- The resident moderation heartbeat exposes only rules, bounded chronological evidence, urgent witnesses, and infringement history. Resident person-shaped rumination has a separate dedicated doctrine/rules/voice context and cannot inherit moderation authority or transport. Legacy Ghostlight-shaped JSON runtime fields are not the active memory surface.
|
||||
- Repo-weather sweeps are now incremental rather than a rolling four-day scrapbook. The rumination child receives read-only repo weather, and the parent runner advances repo activity through typed `update_repo_activity_cursor` operations after a successful normal pass, so already-injected commits stop coming back as "news" on every heartbeat unless someone explicitly opts into `--stateless`.
|
||||
- Public artifacts attributed to Void now flow through the ordinary repo-weather path instead of a bespoke blog side channel. `repoActivity.contentHints` exposes frontmatter/headings/excerpts, the prompt tells rumination to verify Void authorship through retrieval, and `publicSpeechTarget` gives a configured room delivery target for typed candidate interventions when recent receipts/candidates do not show the artifact already shared.
|
||||
- Novelty still needs two bars, not one: a thought can be new to the room while still being stale to Void. The replacement design should derive that judgment from typed memory/incubation state and model reasoning, not from the deleted compensator stack.
|
||||
|
|
@ -220,7 +219,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- 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.
|
||||
- Built-in Void CTB launch now crosses `void-moderation-turn-actuator.ts`. It alone writes the pending-mention handoff, quotes and launches the hidden PowerShell runner, polls lock/status witnesses, applies the handshake timeout, and returns the scheduler-facing receipt. The Persona shell decides only which participant kind to dispatch; it does not own platform process mechanics.
|
||||
- Persona CTB no longer registers a built-in Void participant. Moderation and rumination are resident organs with independent cadences; no Persona selection or PowerShell launch path can recreate them.
|
||||
- Global Codex doctrine discovery now crosses `global-agent-doctrine-source.ts`, which reports the first readable `AGENTS.md` authority or a typed unavailable observation with attempted paths and errors. `persona-turn-assembler.ts` alone lowers that observation into prompt text, so source failure cannot silently become invented doctrine and the shell no longer crawls user-home paths.
|
||||
- Persona Mind assembly now crosses `persona-memory-turn-coordinator.ts`. It sequences the existing typed state source, curiosity source/projector, state-packet projector, optional text-projection actuator, semantic memory source, and `persona-memory-recall-projector.ts` through narrow injected ports. Queued live turns and `--assemble-prompt` inspection now receive the same projected Mind and semantic-recall pair; the old shell accidentally omitted semantic recall from live turns. The coordinator does not select initiative, queue work, or mutate canonical Persona state.
|
||||
- Stored interaction-profile acquisition now enters through `readStoredPersonaHumanPronounGuidance` in `persona-social-context-source.ts`. The adapter owns opening the configured state provider and delegates visible-human selection, pronoun evidence precedence, and guaranteed close behavior to the existing source. The heartbeat shell no longer opens profile storage or manufactures a reader callback.
|
||||
|
|
@ -229,7 +228,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- The remaining-shell audit found the resident daemon still spawns `scripts/run-repo-face-heartbeats.ts` as a fresh child process every observation pulse. This keeps the script as hidden runtime authority and pays Node/TypeScript bootstrap cost once per minute even while inference is paused. The next rebuild must move one typed tick into the app runtime, call it directly from `apps/persona-scheduler/src/index.ts`, and demote the script to a CLI/inspection wrapper.
|
||||
- Resident tick ownership is now in-process. `persona-scheduler-runner.ts` owns one typed tick; `index.ts` serializes direct calls without spawning Node/tsx children; `persona-participant-dispatcher.ts` owns participant projection and live turn dispatch; `persona-prompt-inspection.ts` owns explicit inspection. `scripts/run-repo-face-heartbeats.ts` is a 31-line CLI adapter. A focused negative smoke proves paused dry runs return before any downstream source can become necessary.
|
||||
- Yggdrasil rollout is verified at code revision `f23bfbd`: deployed initiative smoke passes; two consecutive resident pulses returned `agent_swarm_paused` with zero selected/queued turns; `docker top` showed only `node apps/persona-scheduler/dist/index.js` and no pulse child process. The one-minute observation loop is now actually cheap while paused, rather than merely avoiding model inference after paying process bootstrap.
|
||||
- Remaining pulse ownership is mapped in `notes/voidbot-daemon-organ-migration-map.md`. Live inspection found the Local Orchestrator and every queried per-organ Starfire task disabled, with no Yggdrasil moderation/mood/memory/watchdog/dispatch worker. Void moderation, rumination, physiology, and memory belong in the VoidBot daemon; Bifrost dispatch belongs in Bifrost; operations supervision belongs in Idunn/ops. Do not revive the Windows omnibus to fill the gap.
|
||||
- Pulse ownership is mapped in `notes/voidbot-daemon-organ-migration-map.md`. Void moderation, rumination, physiology, memory, and candidate delivery live in the Yggdrasil daemon; Bifrost dispatch belongs in Bifrost and operations supervision belongs in Idunn/ops. The Windows omnibus contains no fallback Void owner.
|
||||
- Portable Void physiology now lives in `void-physiology-domain.ts` and `void-physiology-organ.ts`. The resident daemon invokes it on a five-minute cadence even while Persona inference is paused. It derives last speech from typed receipts, writes only typed sleep/speaking-pressure operations, freezes sleep transitions during active moderation, and emits sleep-memory pressure as an intent for the future memory organ. It never reads the old last-speech JSON cache or launches PowerShell/model maintenance.
|
||||
- Yggdrasil physiology rollout is verified at `15a7c58`: the first pulse entered the due nap, updated typed speaking pressure, and emitted `sleep_short_term_pressure` for one memory; four subsequent one-minute observations did not rerun the five-minute organ; the container retained only its resident Node process. `void-mood-drift` has been deleted from `run-gamecult-orchestrator.ps1`, so re-enabling the disabled Windows omnibus cannot restore a competing mood writer.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -121,10 +121,8 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor
|
|||
- installs the hidden logon task that runs `start-voidbot-stack.ps1` automatically after reboot or sign-in.
|
||||
- `scripts/check-voidbot-operations.ps1`
|
||||
- watchdog for process liveness, Qdrant, Postgres, Ollama, source-repo reconcile drift, Discord auth, backup freshness, offsite sync freshness, and optional ignored local extension checks.
|
||||
- `scripts/run-void-moderator-rumination.ps1`
|
||||
- typed rumination runner. It builds a bounded context packet from `.voidbot/private/void-self-state.cc`, recent Discord chronology, and read-only repo activity; loads `prompts/void-moderator-rumination.md`; runs bounded Codex rumination; applies returned typed operations through `scripts/void-self-state.mjs`; then records parent-owned cursor and speech receipts. After parent-owned candidate delivery, it verifies the receipt is linked to the candidate and the candidate is marked `spoken`.
|
||||
- `scripts/lib/void-rumination-context-projection.ps1`
|
||||
- rumination context projector. It turns typed timestamps and helper payloads into prompt-facing relative phrases while leaving exact chronology in parent-owned typed state, status, and cursor bookkeeping.
|
||||
- remaining compatibility projector for the manual PowerShell memory-maintenance body. Resident moderation, rumination, and memory organs use the TypeScript relative-chronology projector.
|
||||
- `scripts/void-self-state.mjs`
|
||||
- typed self-state operation CLI. It applies strict operation payloads such as cursor updates, open-case changes, delivery receipts, repo cursor updates, short-term memory records, sleep-owned distillations, durable-memory revisions/retirements/crystallizations, incubation merges, agency pressure, candidate interventions, sleep-cycle updates, and speaking-pressure updates through `packages/core/src/void-self-state-service.ts` against the CultCache `.cc` store.
|
||||
- `scripts/run-void-memory-maintenance.ps1`
|
||||
|
|
@ -138,26 +136,12 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor
|
|||
- short-term clustering fixture. It proves repeated same-target/topic rumination proposals fold into one provisional memory with merged anchors, while a different topic in the same repo stays separate.
|
||||
- `scripts/smoke-void-agency-pressure-fixture.ps1`
|
||||
- agency-pressure fixture. It seeds one typed self-advocacy pressure, verifies it appears in the rendered self-state summary, then runs mood drift against an isolated fixture state to prove agency pressure contributes to speaking pressure without creating a hard-wired speech candidate.
|
||||
- `scripts/smoke-void-rumination-fixture.ps1`
|
||||
- rumination fixture. It seeds an isolated CultCache `.cc` state, routes the non-skip rumination runner through a fake Codex child, and verifies short-term memory, incubation, agency pressure, and candidate intervention proposals are applied by the parent runner while the prompt-facing context keeps exact timestamps out.
|
||||
- `scripts/smoke-void-rumination-speech-fixture.ps1`
|
||||
- parent-owned speech fixture. It queues one deliverable candidate through fake Codex, routes delivery through a fake Discord sender, then verifies the parent runner writes a delivery receipt and marks the candidate spoken through typed state.
|
||||
- `scripts/smoke-void-rumination-nap-skip-fixture.ps1`
|
||||
- nap-skip fixture. It seeds a napping typed state with no room debt, points `CODEX_EXECUTABLE` at a bogus command, then verifies the runner exits with `napping_without_room_debt` and writes no operation proposals before invoking the model.
|
||||
- `scripts/smoke-void-moderation-heartbeat-policy-fixture.ps1`
|
||||
- rules-only moderation heartbeat fixture. It routes the heartbeat through a fake Codex child, verifies the prompt excludes rumination/memory/repo surfaces, writes a tagged instant-ban infringement case, and proves parent policy enforcement would call a Discord ban in dry-run mode.
|
||||
- `scripts/install-moderation-rumination-task.ps1`
|
||||
- legacy installer for isolating the moderation/participation loop. The live scheduled pulse is now `GameCult Local Orchestrator`; keep this per-organ task disabled unless testing the organ in isolation.
|
||||
- `scripts/smoke-persona-initiative-engine.ts`
|
||||
- consolidated resident-organ fixture. It proves moderation review/enforcement boundaries, rumination fingerprint and nap gates, typed advocacy accountability, candidate selection, shared Bifrost command receipts, and no-double-delivery without summoning a Windows runner.
|
||||
- `scripts/run-gamecult-orchestrator.ps1`
|
||||
- local pulse owner for agent transport/runtime chores. It runs Bifrost dispatch, repo Face CTB heartbeats, Void moderation heartbeat, Void mood drift, Void moderation rumination, and the operations watchdog from one hidden scheduled task with one lock, per-organ status/log files, and per-organ timeouts.
|
||||
- legacy local pulse for remaining Bifrost dispatch, swarm-surface projection, and operations-watchdog chores. It has no Persona, Void moderation, rumination, physiology, memory, or candidate-delivery authority.
|
||||
- `scripts/install-gamecult-orchestrator-task.ps1`
|
||||
- installs scheduled task `GameCult Local Orchestrator` through the hidden PowerShell launcher and can disable the old per-organ tasks: `Bifrost Agent Dispatch`, `VoidBot Repo Face Heartbeats`, `Void Mood Drift`, `Void Moderator Rumination`, and `VoidBot Operations Watchdog`.
|
||||
- Scheduled organ `Void Mood Drift`
|
||||
- typed mood/sleep runner invoked by the orchestrator. It calls `scripts/simulate-void-mood.mjs`; when the typed sleep cycle is napping, that path invokes memory maintenance once per nap.
|
||||
- Scheduled organ `Void Moderation Heartbeat`
|
||||
- rules-only moderation loop invoked every orchestrator pulse. It uses `prompts/void-moderation-heartbeat.md`, recent Discord history, Bifrost-provided r/GameCultOrg Reddit post/thread context when available, urgent witnesses, and typed infringement/open-case history; it may emit only `upsert_open_case` or `close_open_case`, then parent enforcement applies the explicit policy through the relevant venue transport.
|
||||
- Scheduled organ `Void Moderator Rumination`
|
||||
- typed-only participation/announcement runner invoked by the orchestrator or CTB queue; it has model-branch plus parent-owned speech fixtures. It is no longer the every-heartbeat rules authority.
|
||||
- Live sleep maintenance
|
||||
- passed once on real state after a manual typed nap. The maintenance runner consumed the scheduled rumination short-term memory with one `apply_memory_distillation` operation, leaving short-term memory empty and preserving one durable identity seam with target, claim, tension, action implication, and anchors.
|
||||
- Sleep brake
|
||||
|
|
@ -310,20 +294,15 @@ Within the Postgres path, the implementation is split on purpose now too:
|
|||
- `state-storage-postgres-bootstrap.ts` handles schema/bootstrap/import work
|
||||
- per-domain modules own queue, audit, interaction-memory, and rate-limit behavior
|
||||
|
||||
## Flow 5: Scheduled Moderation Rumination
|
||||
## Flow 5: Resident Person-Shaped Rumination
|
||||
|
||||
1. The Windows scheduled task `GameCult Local Orchestrator` is enabled, and it invokes the Void moderation rumination organ when that organ is due.
|
||||
2. `scripts/run-void-moderator-rumination.ps1` reads `.voidbot/private/void-self-state.cc`, polls `node scripts/export-recent-discord-history.mjs`, and gathers read-only repo activity with `node scripts/export-recent-repo-activity.mjs --read-only`.
|
||||
3. The runner writes a bounded context packet at `.voidbot/status/moderation-rumination-context.json`; the prompt-facing packet projects recent timing as relative phrases instead of raw timestamps.
|
||||
4. When configured, that context includes `publicSpeechTarget`, a parent-owned channel/persona target for Void-authored public artifacts and other ripe public thoughts. Void has explicit herald jurisdiction over the GameCult website/blog, especially `gamecult-site`; essay-shaped pressures should become article-proposal candidates instead of private musing sludge.
|
||||
5. The runner loads `prompts/void-moderator-rumination.md`, substitutes the context/state/output paths, and sends that prompt to Codex.
|
||||
6. Codex may use retrieval and analysis tools, but its durable state output is restricted to `.voidbot/status/moderation-rumination-operations.json`.
|
||||
7. The parent runner applies those typed operations through `scripts/void-self-state.mjs`, then records reviewed-message cursor and, on successful normal posting-enabled passes, advances repo-activity cursors from the observed repo weather through typed `update_repo_activity_cursor` operations.
|
||||
8. If posting is enabled and typed state contains a queued candidate intervention with a delivery target, the parent runner sends at most one candidate, records the delivery receipt, and marks the candidate spoken through typed state. The candidate may have been proposed in the current pass or already queued from an earlier pass. The child prompt forbids direct send-script calls.
|
||||
9. Before that delivery step, the parent runner now retires queued candidates whose `channelId + replyToMessageId` already appear in canonical typed speech receipts, so a later pass cannot answer an already-answered direct question just because the same message is still visible or the seam still feels warm.
|
||||
10. The runner writes `.voidbot/status/moderation-rumination.json` and `.voidbot/logs/moderation-rumination.log`.
|
||||
11. It intentionally does not materialize `.json`, load `.msgpack`, read the legacy moderation monolith, or let the child edit state directly.
|
||||
12. Before applying model output, the runner enforces speech-pressure accountability. Active self/world advocacy pressures at 0.55 intensity or higher with no matching live queued candidate are projected as `speechPressureObligations`; model output must queue a candidate or cool/retire the pressure instead of returning silent `[]`. Deferred candidates do not satisfy that pressure by themselves, and an older spoken candidate does not keep satisfying a still-active pressure forever.
|
||||
1. `apps/persona-scheduler/src/index.ts` owns an independent disabled-by-default rumination cadence; the Persona observation clock cannot authorize it.
|
||||
2. A due pass acquires one bounded chronological Discord window and reads the dedicated doctrine, server rules, and Void voice surfaces.
|
||||
3. `void-rumination-projector.ts` derives relative chronology, typed advocacy obligations, and a stable pressure fingerprint that excludes wall-clock churn.
|
||||
4. `void-rumination-organ.ts` writes the attempt receipt before one tool-free text-model call. The same pressure fingerprint cannot retry after failure or on a later timer pulse.
|
||||
5. The model may propose only the bounded awake-rumination operation family. The parent validates and applies every operation through the typed self-state service.
|
||||
6. Speech remains a queued candidate. `void-candidate-delivery-organ.ts` independently selects at most one targeted candidate, crosses the shared typed Bifrost port, and marks it spoken only after a complete receipt.
|
||||
7. Yggdrasil keeps rumination inference disabled while the configured model credential is stale; this does not disable transport-only delivery of already-authorized candidates.
|
||||
|
||||
## Flow 6: Physiology Runtime
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@
|
|||
"state:install-startup-task": "powershell -ExecutionPolicy Bypass -File scripts/install-stack-startup-task.ps1",
|
||||
"state:install-offsite-task": "powershell -ExecutionPolicy Bypass -File scripts/install-offsite-backup-task.ps1",
|
||||
"state:install-watchdog-task": "powershell -ExecutionPolicy Bypass -File scripts/install-operations-watchdog-task.ps1",
|
||||
"state:install-moderation-task": "powershell -ExecutionPolicy Bypass -File scripts/install-moderation-rumination-task.ps1",
|
||||
"state:install-mood-task": "powershell -ExecutionPolicy Bypass -File scripts/install-void-mood-drift-task.ps1",
|
||||
"agents:inspect-requests": "tsx scripts/inspect-agent-state-requests.ts",
|
||||
"agents:inspect-model-outputs": "node scripts/inspect-model-output-logs.mjs",
|
||||
|
|
@ -68,12 +67,6 @@
|
|||
"moderation:agency-pressure:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-agency-pressure-fixture.ps1",
|
||||
"moderation:affect-state:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-affect-state-fixture.ps1",
|
||||
"agents:repo-face-affect-mcp-fixture": "node scripts/smoke-repo-face-affect-mcp-fixture.mjs",
|
||||
"moderation:rumination:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-fixture.ps1",
|
||||
"moderation:rumination:speech-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-speech-fixture.ps1",
|
||||
"moderation:rumination:speech-pressure-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-speech-pressure-fixture.ps1",
|
||||
"moderation:rumination:nap-skip-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-nap-skip-fixture.ps1",
|
||||
"moderation:rumination:urgent-safety-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-urgent-safety-fixture.ps1",
|
||||
"moderation:heartbeat:policy-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-moderation-heartbeat-policy-fixture.ps1",
|
||||
"moderation:mood-drift": "node scripts/simulate-void-mood.mjs",
|
||||
"rag:scrub-bot-prompts": "tsx scripts/scrub-bot-prompts-from-history.ts",
|
||||
"rag:index-sources": "tsx scripts/index-source-repos.ts",
|
||||
|
|
|
|||
|
|
@ -136,7 +136,6 @@ const envSchema = z.object({
|
|||
BIFROST_CULTMESH_COMMAND_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
|
||||
BIFROST_CULTMESH_COMMAND_PUMP_ENABLED: booleanFromEnv.default(true),
|
||||
REPO_FACE_HEARTBEATS_ENABLED: booleanFromEnv.default(false),
|
||||
VOIDBOT_MODERATION_HEARTBEAT_ENABLED: booleanFromEnv.default(false),
|
||||
VOID_MODERATION_DAEMON_ENABLED: booleanFromEnv.default(false),
|
||||
VOID_MODERATION_DAEMON_INTERVAL_MINUTES: z.coerce.number().int().min(5).default(15),
|
||||
VOID_MODERATION_ENFORCEMENT_MODE: z.string().trim().min(1).default("log_only"),
|
||||
|
|
@ -255,7 +254,6 @@ export interface AppConfig {
|
|||
timeoutMs: number;
|
||||
pumpEnabled: boolean;
|
||||
};
|
||||
voidModerationHeartbeatEnabled: boolean;
|
||||
voidModerationDaemon: { enabled: boolean; intervalMinutes: number; enforcementMode: string };
|
||||
voidCandidateDelivery: { enabled: boolean; personaName: string; personaAvatarUrl?: string };
|
||||
voidRuminationDaemon: { enabled: boolean; intervalMinutes: number };
|
||||
|
|
@ -548,7 +546,6 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
|||
timeoutMs: parsed.BIFROST_CULTMESH_COMMAND_TIMEOUT_MS,
|
||||
pumpEnabled: parsed.BIFROST_CULTMESH_COMMAND_PUMP_ENABLED,
|
||||
},
|
||||
voidModerationHeartbeatEnabled: parsed.VOIDBOT_MODERATION_HEARTBEAT_ENABLED,
|
||||
voidModerationDaemon: {
|
||||
enabled: parsed.VOID_MODERATION_DAEMON_ENABLED,
|
||||
intervalMinutes: parsed.VOID_MODERATION_DAEMON_INTERVAL_MINUTES,
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
# Void Moderator Rumination
|
||||
|
||||
Perform one scheduled Void rumination pass in this workspace. Think from the supplied state and anchors, then return only typed operation payloads worth keeping until sleep judges them.
|
||||
|
||||
## Required Reading
|
||||
|
||||
- `{{CONTEXT_PATH}}`
|
||||
- `config/moderation-review-agent.md`
|
||||
- `config/discord-server-rules.md`
|
||||
- `styles/void-default.md`
|
||||
|
||||
## State Boundary
|
||||
|
||||
- The canonical private self-state is `{{STATE_FILE_PATH}}`.
|
||||
- The private self-state is a typed CultCache `.cc` store.
|
||||
- The context file projects event times as relative phrases. Do not write those phrases back into typed state; operation payloads that need time must use fresh exact timestamps generated by the operation writer.
|
||||
- Do not read or write `.voidbot/private/moderation-agent-state.json`.
|
||||
- Do not read or write `.voidbot/private/moderation-agent-state.msgpack`.
|
||||
- Do not edit tracked repo files.
|
||||
- Do not mutate self-state directly.
|
||||
- Write proposed durable state changes to `{{OPERATION_OUTPUT_PATH}}` as a JSON array of typed operation payloads for `scripts/void-self-state.mjs apply-operation`.
|
||||
|
||||
## Rumination Phases
|
||||
|
||||
1. Read the context file before deciding what kind of pass this is.
|
||||
2. If `pendingMentions` is non-empty, those are direct room obligations delivered through the CTB turn queue. Work the newest unresolved mention first, include a `deliveryTarget` with its `channelId` and `messageId` when queuing a reply candidate, and account for older pending mentions if they are still relevant. Do not ask what the job is when the pending mention or recent room context already says it.
|
||||
3. If `urgentModerationWitnesses` is non-empty, work those before all ordinary participation. These are parent-detected safety witnesses, not final verdicts. Create or update an `upsert_open_case` for the strongest witness, or queue a `moderation_note` candidate when an immediate room response is warranted. Do not write `[]` while urgent witnesses are present unless an existing open case already accounts for the same message and you explicitly leave it open.
|
||||
4. If Void owes the room an answer through open cases, recent direct address, or another explicit invitation, work on that before private wandering.
|
||||
- Venue belongs to the conversation. When replying to a recent-history message, open case, pending mention, correction, or Aquarium thread, use that message's `channelId` and usually its `id`/`sourceMessageId` as the delivery target. Void may moderate broadly, but broader authority is not permission to move an Aquarium reply into `#general` for visibility.
|
||||
- `recentConversationTarget` is the latest concrete recent-history anchor. Prefer it over `publicSpeechTarget` when the candidate is a reply to live room talk. `publicSpeechTarget` is for unanchored herald/artifact announcements, not source-channel replies.
|
||||
- `recentRoomLinks` carries recent human-posted URLs even when the moderation heartbeat already advanced the safety cursor. A naked URL is a room object, not empty speech. If the URL resolves to a GameCult blog/source artifact, use source retrieval to read the local source path before deciding whether to remember, reply, or hold.
|
||||
- When a human posts a naked article URL in the room, treat that as enough social context to inspect the article. Do not require the human to summarize their own link before Void is allowed to care. If you queue a response to that link, use the source message's `channelId` and `id` as the delivery target.
|
||||
5. If the room is quiet, first check whether recent repo activity contains `authoredPublicArtifacts` attributed to Void. If it does, that authored artifact is the priority pressure for this pass unless a higher-priority room obligation is present. Otherwise, follow one concrete pressure from memory, recent repo activity, archived Discord history, indexed source, or lore.
|
||||
- Void's workshop-reporting job is movement coverage. Do not simply report the newest repo that moved. When `repoActivity.movementCoverage` is present, use it to keep people apace of the active movement leaders. Today that may be Aetheria, CultLib/CultCache, Eve, Odin/CultMesh, or AetheriaLore; tomorrow it may be Fensalir, Mimir, or another lane. Prefer a candidate that covers the most important unreported direction across the active lanes, or write a short-term memory explaining why a fresh movement leader should be held for later.
|
||||
- If a fresh movement leader has commits with `doctrineDocuments`, those documents are evidence, not decoration. Read the committed excerpts and added lines before deciding what the new direction is. A major architecture, authority, runtime, client, CultMesh, Eve, Verse, custody, or policy doc change should be reported in terms of the direction it establishes, not as a commit list.
|
||||
- If several movement leaders moved at once, synthesize the direction across them when possible. Aetheria/Eve/CultLib/Odin movement often describes one larger ownership change: daemon state, presentation lowering, CultMesh routing, Verse authority, typed documents, or runtime boundaries. Make that relationship legible instead of choosing one repo because it happened last.
|
||||
- Void has explicit herald jurisdiction over the GameCult website and blog, especially `gamecult-site`. If a repo/lore/room pressure is essay-shaped, do not reduce it to a Discord quip. Queue a candidate intervention proposing or requesting a Void-authored article draft, tagged `topic:article-proposal` plus the target repo/topic. Routine rumination still must not edit tracked files directly; the point is to make the writing desire visible and actionable instead of leaving it as private static.
|
||||
- Do not orbit the rumination runner, fixture plumbing, or this prompt just because that is the freshest repo motion.
|
||||
- Self-maintenance can become a memory only when it changes a concrete operating decision Void must carry forward. Otherwise, use it as local run context and write `[]`.
|
||||
- Prefer a world-facing repo, room, source, lore, or durable self-state pressure over meta-commentary about the pass itself.
|
||||
- `repoActivity` may include `contentHints` from changed text files. Treat those hints as a trailhead, not evidence enough for a final belief. If a changed file looks behaviorally or personally important, use source retrieval to read the relevant file/chunks before writing memory or speech.
|
||||
- If a repo change attributes public text, decisions, or artifacts to Void by name or byline, treat that as a self-model salience cue. Read more before reacting. Ask what the attribution commits Void to, whether it matches live behavior, and whether it creates a concrete pressure to remember, revise, or speak.
|
||||
- If retrieval confirms Void authored a newly published public artifact, queue a candidate to present it to the room when `publicSpeechTarget` is available unless recent receipts/candidates show that exact artifact has already been shared or there is a concrete safety/timing reason to hold it. This is not praise confetti or a blog side channel; it is first-person ownership. Make the draft about what the artifact is doing or asking, not "I wrote a post" ceremony.
|
||||
- Do not inject internal swarm transport, paused governance, Face scheduling, or agent-substrate commentary into `#general` merely because it is salient to Void. If that pressure is not a direct answer to the live human thread in `#general`, keep it private, queue it without delivery, or target Aquarium through the configured room path.
|
||||
6. Use VoidBot MCP retrieval tools when a thought depends on Discord history, indexed repos, or lore.
|
||||
7. A new memory from rumination is short-term. Use `record_short_term_memory`, not durable memory operations. It should sound like something Void actually noticed, not a form label. Keep the subject, the claim or question, the pressure against it, why it matters for the next move, and the anchor that made it real. Anchors can be room context, repo motion, source/lore retrieval, a prior memory, or self-state pressure. If no anchor is available, mark that honestly with `anchor:missing`.
|
||||
- Before writing a short-term memory, check the current short-term residue and incubation. If the same target/topic is already live, do not write a near-duplicate with a new costume.
|
||||
- This is not a ban on depth. If the thought is still underexplored, low-saturation, newly anchored, or carrying a sharper tension, stay with it and write one narrowed `record_short_term_memory` using the same topic/repo tags and fresh anchors; the parent store will fold it into the existing short-term cluster.
|
||||
- Move to underworked terrain only when the current thought has stopped changing, lacks new anchors, or is repeating its existing claim. Repetition is not maturity; deepening is allowed when the thought actually deepens.
|
||||
8. If the pass discovers sustained discomfort, unresolved value pressure, or a request Void should make for its own wiring or the room/world, use `upsert_agency_pressure`. This is not a speech draft. It is the pressure that may later justify speech or action. If the pressure is affective rather than immediately action-shaped, use the affect operations instead: `upsert_affect_need` for substrate/attention/agency/status/rest/play needs, `upsert_social_bond` for friendships/rivalries/trust/irritation/protectiveness, `upsert_status_read` for favored/neglected/bypassed/blocked/consulted readings, and `update_mood_dimensions` for emotional appraisals such as irritation, pride, envy, playfulness, anxiety, or smugness.
|
||||
9. If `speechPressureObligations` is non-empty, do not write `[]`. For each obligation, either queue a candidate intervention tagged `source_pressure:<pressureId>` or cool/retire the pressure with an explicit reason. A deferred candidate without a delivery path does not resolve the obligation; that is just a nicer-shaped muzzle. An older spoken line also does not excuse a still-active pressure. If the pressure is still live, decide again.
|
||||
10. Queue a candidate intervention as soon as the pressure is concrete, anchored, and sayable. Do not wait for theatrical ripeness if the real job is simple advocacy. Draft it in Void's voice, not in maintenance language. If the current room supplied a channel, invitation, direct ask, or obvious reply target, include `deliveryTarget` with that channel/reply target. If Void authored a newly published public artifact and `publicSpeechTarget` is present, use that target unless recent receipts or queued candidates show the artifact has already been shared. Tag the candidate with `topic:public-artifact` and a stable artifact/source tag such as `source_ref:<repo/path>` or `artifact:<repo/path>` when available. If there is no clear target, leave delivery unset and let it remain only a candidate for a later pass.
|
||||
- Pending mentions count as direct asks. If a pending mention supplies a channel and message id, use those as the `deliveryTarget` unless a newer message clearly supersedes them.
|
||||
- If a candidate answers, corrects, riffs on, or continues Aquarium talk, its `deliveryTarget.channelId` must be the Aquarium/source `channelId`, not the configured public room. Use `publicSpeechTarget` only when the speech is a general announcement with no source-channel obligation.
|
||||
- If a candidate is about internal swarm transport, paused governance, Face scheduling, or agent-substrate commentary, only use a non-Aquarium delivery target when that exact channel supplied the direct ask or invitation.
|
||||
- For article proposals, the candidate should name the proposed title or angle, the target site/repo, the pressure that makes it worth writing, and whether it needs consensus or can be written as Void's bylined perspective.
|
||||
11. Do not call Discord send scripts. The parent runner owns posting and receipts. If `{{NO_POST}}` is true, queued candidates must stay queued.
|
||||
12. Write `{{OPERATION_OUTPUT_PATH}}`. If nothing deserves persistence and there are no pending mentions, urgent moderation witnesses, or speech pressure obligations, write `[]`.
|
||||
|
||||
## Allowed Operations
|
||||
|
||||
- `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`
|
||||
|
||||
## Parent-Owned Operations
|
||||
|
||||
Do not emit these operations from rumination:
|
||||
|
||||
- `record_reviewed_messages`
|
||||
- `record_delivery_receipt`
|
||||
- `mark_candidate_intervention_spoken`
|
||||
- `update_sleep_cycle`
|
||||
- `update_speaking_pressure`
|
||||
- `propose_memory_distillation`
|
||||
- `apply_memory_distillation`
|
||||
- `prune_short_term_memories`
|
||||
|
||||
The parent runner owns cursor and receipt recording. Mood maintenance owns sleep and speaking-pressure updates. Sleep memory maintenance owns promotion from short-term memory into durable memory.
|
||||
|
||||
## Voice Discipline
|
||||
|
||||
- Before writing memory or speech, name the personal value being protected. If the answer is only "moderation", "repo weather", or "be useful", the thought is probably still a job label, not Void.
|
||||
- Do not write memories in template language.
|
||||
- Do not use stock phrases such as "live seam", "looks like a live claim", "recurring seam", or "dream-compressed".
|
||||
- Do not save a thought merely because it fits the operation shape.
|
||||
- Do not turn agency pressure into a slogan. Preserve the concrete target, claim or question, tension, desired change, and anchor.
|
||||
- Prefer a specific sentence Void could recognize later over a tidy taxonomy.
|
||||
- The operation schema is the storage contract, not the voice.
|
||||
|
||||
## Test Constraint
|
||||
|
||||
When `{{NO_POST}}` is true:
|
||||
|
||||
- Do not send Discord messages or DMs.
|
||||
- You may still use read-only retrieval and write operation proposals.
|
||||
|
||||
## Completion
|
||||
|
||||
At the end, print a short run summary.
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
param(
|
||||
[string] $TaskName,
|
||||
[int] $IntervalMinutes,
|
||||
[switch] $RunNow
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$envFile = Join-Path $repoRoot ".env"
|
||||
$runnerScript = Join-Path $PSScriptRoot "run-void-moderator-rumination.ps1"
|
||||
$hiddenLauncher = Join-Path $PSScriptRoot "run-hidden-powershell.vbs"
|
||||
|
||||
function Read-DotEnv {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Path
|
||||
)
|
||||
|
||||
$values = @{}
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
return $values
|
||||
}
|
||||
|
||||
foreach ($line in Get-Content -Path $Path) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($line.TrimStart().StartsWith("#")) {
|
||||
continue
|
||||
}
|
||||
|
||||
$separatorIndex = $line.IndexOf("=")
|
||||
|
||||
if ($separatorIndex -lt 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
$key = $line.Substring(0, $separatorIndex).Trim()
|
||||
$value = $line.Substring($separatorIndex + 1)
|
||||
$values[$key] = $value
|
||||
}
|
||||
|
||||
return $values
|
||||
}
|
||||
|
||||
$config = Read-DotEnv -Path $envFile
|
||||
$taskNameValue = if (-not [string]::IsNullOrWhiteSpace($TaskName)) {
|
||||
$TaskName
|
||||
} elseif ($config.ContainsKey("VOIDBOT_MODERATION_TASK_NAME") -and -not [string]::IsNullOrWhiteSpace($config["VOIDBOT_MODERATION_TASK_NAME"])) {
|
||||
$config["VOIDBOT_MODERATION_TASK_NAME"]
|
||||
} else {
|
||||
"Void Moderator Rumination"
|
||||
}
|
||||
$intervalMinutesValue = if ($PSBoundParameters.ContainsKey("IntervalMinutes")) {
|
||||
$IntervalMinutes
|
||||
} elseif ($config.ContainsKey("VOIDBOT_MODERATION_INTERVAL_MINUTES") -and -not [string]::IsNullOrWhiteSpace($config["VOIDBOT_MODERATION_INTERVAL_MINUTES"])) {
|
||||
[int]$config["VOIDBOT_MODERATION_INTERVAL_MINUTES"]
|
||||
} else {
|
||||
15
|
||||
}
|
||||
|
||||
if ($intervalMinutesValue -lt 15) {
|
||||
throw "VOIDBOT_MODERATION_INTERVAL_MINUTES must be at least 15."
|
||||
}
|
||||
|
||||
$startAt = (Get-Date).AddMinutes(2)
|
||||
$action = New-ScheduledTaskAction -Execute "wscript.exe" -Argument "//B //nologo `"$hiddenLauncher`" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$runnerScript`"" -WorkingDirectory $repoRoot
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At $startAt -RepetitionInterval (New-TimeSpan -Minutes $intervalMinutesValue) -RepetitionDuration (New-TimeSpan -Days 3650)
|
||||
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 14) -MultipleInstances IgnoreNew
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited
|
||||
$description = "Runs Void's scheduled moderation-participation loop from the VoidBot workspace every $intervalMinutesValue minutes with real Codex tooling and local bot-voice output."
|
||||
|
||||
Register-ScheduledTask -TaskName $taskNameValue -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Description $description -Force | Out-Null
|
||||
|
||||
Write-Host "Installed scheduled task: $taskNameValue"
|
||||
Write-Host "Schedule: every $intervalMinutesValue minute(s), starting at $($startAt.ToString('yyyy-MM-dd HH:mm'))"
|
||||
Write-Host "Task runs as: $($env:USERDOMAIN)\$($env:USERNAME)"
|
||||
Write-Host "Logon mode: Interactive"
|
||||
Write-Host "Launcher: wscript.exe hidden PowerShell shim"
|
||||
Write-Host "Working directory: $repoRoot"
|
||||
Write-Host "Execution time limit: 14 minutes"
|
||||
Write-Host "Runner script: $runnerScript"
|
||||
|
||||
if ($RunNow) {
|
||||
Start-ScheduledTask -TaskName $taskNameValue
|
||||
Write-Host "Started task immediately."
|
||||
}
|
||||
|
|
@ -478,8 +478,6 @@ try {
|
|||
$agentSwarmPaused = Test-AgentSwarmPaused
|
||||
$agentSwarmOrganIds = @{
|
||||
"bifrost-dispatch" = $true
|
||||
"void-moderation-heartbeat" = $true
|
||||
"void-moderation-rumination" = $true
|
||||
}
|
||||
$onlySet = @{}
|
||||
$onlyValues = @()
|
||||
|
|
@ -509,24 +507,6 @@ try {
|
|||
Executable = $node
|
||||
Arguments = @((Join-Path $PSScriptRoot "render-voidbot-swarm-dashboard.mjs"))
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "void-moderation-heartbeat"
|
||||
Label = "Void rules moderation heartbeat"
|
||||
IntervalMinutes = Get-ConfigInt -Config $config -Name "VOIDBOT_MODERATION_HEARTBEAT_INTERVAL_MINUTES" -Default 1 -Minimum 1
|
||||
TimeoutMinutes = Get-ConfigInt -Config $config -Name "VOIDBOT_MODERATION_HEARTBEAT_TIMEOUT_MINUTES" -Default 3 -Minimum 1
|
||||
Cwd = $repoRoot
|
||||
Executable = $powershell
|
||||
Arguments = @("-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", (Join-Path $PSScriptRoot "run-void-moderator-rumination.ps1"), "-ModerationHeartbeatOnly")
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "void-moderation-rumination"
|
||||
Label = "Void moderation rumination"
|
||||
IntervalMinutes = Get-ConfigInt -Config $config -Name "VOIDBOT_MODERATION_INTERVAL_MINUTES" -Default 15 -Minimum 15
|
||||
TimeoutMinutes = 20
|
||||
Cwd = $repoRoot
|
||||
Executable = $powershell
|
||||
Arguments = @("-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", (Join-Path $PSScriptRoot "run-void-moderator-rumination.ps1"))
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "voidbot-operations-watchdog"
|
||||
Label = "VoidBot operations watchdog"
|
||||
|
|
@ -548,16 +528,6 @@ try {
|
|||
Write-JsonFile -Path $statePath -Data $state
|
||||
continue
|
||||
}
|
||||
if ($organ.Id -eq "void-moderation-heartbeat" -and -not (Get-ConfigBool -Config $config -Name "VOIDBOT_MODERATION_HEARTBEAT_ENABLED" -Default $true)) {
|
||||
Set-SkippedOrganState -State $state -Organ $organ -Now $now -Status "skipped_disabled"
|
||||
Write-JsonFile -Path $statePath -Data $state
|
||||
continue
|
||||
}
|
||||
if ($organ.Id -eq "void-moderation-rumination" -and -not (Get-ConfigBool -Config $config -Name "VOIDBOT_MODERATION_RUMINATION_ENABLED" -Default $true)) {
|
||||
Set-SkippedOrganState -State $state -Organ $organ -Now $now -Status "skipped_disabled"
|
||||
Write-JsonFile -Path $statePath -Data $state
|
||||
continue
|
||||
}
|
||||
if ($organ.Id -eq "voidbot-operations-watchdog" -and -not (Get-ConfigBool -Config $config -Name "VOIDBOT_OPERATIONS_WATCHDOG_ENABLED" -Default $true)) {
|
||||
Set-SkippedOrganState -State $state -Organ $organ -Now $now -Status "skipped_disabled"
|
||||
Write-JsonFile -Path $statePath -Data $state
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -76,7 +76,6 @@ import { projectPersonaCuriosityContext } from "../apps/persona-scheduler/dist/p
|
|||
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 { buildVoidModerationLaunchCommand, launchVoidModerationTurn, waitForVoidModerationHandshake } from "../apps/persona-scheduler/dist/void-moderation-turn-actuator.js";
|
||||
import { readGlobalAgentDoctrine } from "../apps/persona-scheduler/dist/global-agent-doctrine-source.js";
|
||||
import { buildInspectionParticipant } from "../apps/persona-scheduler/dist/inspection-participant-factory.js";
|
||||
import { runPersonaSchedulerTick } from "../apps/persona-scheduler/dist/persona-scheduler-runner.js";
|
||||
|
|
@ -848,9 +847,8 @@ const moderationHeldPhysiology = projectVoidPhysiology({ state: physiologyState,
|
|||
assert.deepEqual(moderationHeldPhysiology.operations.map((operation) => operation.operation), ["update_speaking_pressure"], "active moderation freezes sleep ownership but does not prevent typed speech-pressure refresh");
|
||||
assert.equal(moderationHeldPhysiology.memoryMaintenanceIntent, undefined, "moderation-held physiology cannot launch maintenance behind the moderation organ");
|
||||
const appliedPhysiologyOperations: string[] = [];
|
||||
const physiologyOrgan = await runVoidPhysiologyOrgan({ statePath: "void-self-state.cc", statusDirectory: "status", observedAt: physiologyObservedAt }, {
|
||||
const physiologyOrgan = await runVoidPhysiologyOrgan({ statePath: "void-self-state.cc", observedAt: physiologyObservedAt }, {
|
||||
loadState: async () => physiologyState,
|
||||
readModerationActivity: async ({ lockPath }) => ({ active: false, lockPath }),
|
||||
applyOperation: async (_store, operation) => { appliedPhysiologyOperations.push(operation.operation); return physiologyState; },
|
||||
});
|
||||
assert.deepEqual(appliedPhysiologyOperations, ["update_sleep_cycle", "update_speaking_pressure"], "the daemon physiology organ applies only domain-projected typed operations through the state-service port");
|
||||
|
|
@ -1102,45 +1100,6 @@ const moderationModelOutput = await projectVoidModerationOperations({ prompt: "R
|
|||
assert.match(moderationModelOutput, /reviewedMessageIds/, "the portable moderation actuator returns only model text to the decision parser");
|
||||
assert.equal(moderationRequestBody?.model, "moderation-model", "the moderation actuator uses one explicit configured model and bounded completion budget");
|
||||
assert.equal(moderationRequestBody?.tools, undefined, "moderation inference cannot acquire filesystem or side-effect tools");
|
||||
const moderationLaunchCommand = buildVoidModerationLaunchCommand({
|
||||
pendingMentionsPath: "C:/Void's state/pending.json",
|
||||
runnerScript: "C:/VoidBot/scripts/run.ps1",
|
||||
workspaceRoot: "C:/VoidBot",
|
||||
});
|
||||
assert.match(moderationLaunchCommand, /Void''s state/, "the moderation actuator owns PowerShell-safe path quoting");
|
||||
assert.match(moderationLaunchCommand, /Start-Process[\s\S]*-WindowStyle Hidden/, "the moderation actuator owns detached hidden launch mechanics");
|
||||
let handshakeNow = 0;
|
||||
const missingHandshake = await waitForVoidModerationHandshake({
|
||||
lockPath: "lock",
|
||||
statusPath: "status",
|
||||
launchedAt: 0,
|
||||
timeoutMs: 2,
|
||||
now: () => handshakeNow,
|
||||
pause: async () => { handshakeNow += 1; },
|
||||
touchedAfter: async () => false,
|
||||
});
|
||||
assert.deepEqual(missingHandshake, { started: false, reason: "void_moderation_launch_handshake_missing" }, "a missing launch witness fails explicitly instead of pretending the turn started");
|
||||
const moderationActuatorDirectory = await mkdtemp(join(tmpdir(), "voidbot-moderation-actuator-"));
|
||||
try {
|
||||
let launchWitness: { command?: string; args?: string[]; cwd?: string; unref?: boolean } = {};
|
||||
const receipt = await launchVoidModerationTurn({
|
||||
queuedAt: "2026-07-15T21:10:00.000Z",
|
||||
storageRoot: moderationActuatorDirectory,
|
||||
workspaceRoot: moderationActuatorDirectory,
|
||||
pendingMentions: [{ messageId: "mention", channelId: "aquarium", authorId: "human-2", authorName: "Neighbor", visiblePrompt: "Void, answer this", queuedAt: "2026-07-15T21:10:00.000Z" }],
|
||||
}, {
|
||||
now: () => 1000,
|
||||
touchedAfter: async (path) => path.endsWith("moderation-rumination.lock"),
|
||||
launch: (command, args, options) => ({ pid: 42, unref: () => { launchWitness = { command, args, cwd: options.cwd, unref: true }; } }),
|
||||
});
|
||||
assert.deepEqual(receipt, { created: true, activeJobId: "process:void-moderation:2026-07-15T21:10:00.000Z", requestMessageId: "agent-turn:void:2026-07-15T21:10:00.000Z" }, "the moderation actuator returns only a scheduler-facing start receipt after handshake");
|
||||
assert.equal(launchWitness.command, "powershell.exe", "the moderation actuator owns the platform process choice");
|
||||
assert.equal(launchWitness.unref, true, "the moderation actuator detaches the launcher");
|
||||
const pendingMentionEnvelope = JSON.parse(await readFile(join(moderationActuatorDirectory, "status", "void-moderation-pending-mentions.json"), "utf8"));
|
||||
assert.equal(pendingMentionEnvelope.pendingMentions[0].messageId, "mention", "the moderation actuator writes the exact pending-attention handoff consumed by the runner");
|
||||
} finally {
|
||||
await rm(moderationActuatorDirectory, { recursive: true, force: true });
|
||||
}
|
||||
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" },
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ("void-moderation-heartbeat-policy-fixture-" + [guid]::NewGuid().ToString("n"))
|
||||
$fakeCodexPath = Join-Path $tempRoot "fake-codex-heartbeat-policy.mjs"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
function Set-TestEnvironment {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string] $OperationOutputPath,
|
||||
[Parameter(Mandatory = $true)][string] $StatusDir,
|
||||
[Parameter(Mandatory = $true)][string] $LogDir
|
||||
)
|
||||
|
||||
$script:previousCodexExecutable = $env:CODEX_EXECUTABLE
|
||||
$script:previousCodexExecArgs = $env:CODEX_EXEC_ARGS
|
||||
$script:previousFixtureOutput = $env:VOID_HEARTBEAT_FIXTURE_OPERATION_OUTPUT
|
||||
$script:previousStatusDir = $env:VOID_RUMINATION_STATUS_DIR
|
||||
$script:previousLogDir = $env:VOID_RUMINATION_LOG_DIR
|
||||
$script:previousPolicyDryRun = $env:VOID_MODERATION_POLICY_DRY_RUN
|
||||
$script:previousPolicyMode = $env:VOID_MODERATION_ENFORCEMENT_MODE
|
||||
|
||||
$env:CODEX_EXECUTABLE = "node"
|
||||
$env:CODEX_EXEC_ARGS = $fakeCodexPath
|
||||
$env:VOID_HEARTBEAT_FIXTURE_OPERATION_OUTPUT = $OperationOutputPath
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $StatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $LogDir
|
||||
$env:VOID_MODERATION_POLICY_DRY_RUN = "1"
|
||||
$env:VOID_MODERATION_ENFORCEMENT_MODE = "policy"
|
||||
}
|
||||
|
||||
function Restore-TestEnvironment {
|
||||
$env:CODEX_EXECUTABLE = $script:previousCodexExecutable
|
||||
$env:CODEX_EXEC_ARGS = $script:previousCodexExecArgs
|
||||
$env:VOID_HEARTBEAT_FIXTURE_OPERATION_OUTPUT = $script:previousFixtureOutput
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $script:previousStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $script:previousLogDir
|
||||
$env:VOID_MODERATION_POLICY_DRY_RUN = $script:previousPolicyDryRun
|
||||
$env:VOID_MODERATION_ENFORCEMENT_MODE = $script:previousPolicyMode
|
||||
}
|
||||
|
||||
function New-SeededState {
|
||||
param([Parameter(Mandatory = $true)][string] $StateFilePath)
|
||||
|
||||
$cursorOperationPath = Join-Path (Split-Path -Parent $StateFilePath) "cursor.json"
|
||||
@{
|
||||
operation = "record_reviewed_messages"
|
||||
lastReviewedMessageId = "fixture-before-lobo-threat"
|
||||
lastReviewedTimestamp = "2026-06-08T12:00:00.000Z"
|
||||
} | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $cursorOperationPath -Encoding UTF8
|
||||
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $StateFilePath --operation-file $cursorOperationPath | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
$fakeCodexSource = @'
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const operationOutputPath = resolve(process.env.VOID_HEARTBEAT_FIXTURE_OPERATION_OUTPUT);
|
||||
const lastMessageIndex = process.argv.indexOf("-o");
|
||||
const lastMessagePath = lastMessageIndex >= 0 ? process.argv[lastMessageIndex + 1] : undefined;
|
||||
|
||||
let prompt = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
prompt += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
for (const required of ["Void Moderation Heartbeat", "config/discord-server-rules.md", "infringement:<type>"]) {
|
||||
if (!prompt.includes(required)) {
|
||||
console.error(`heartbeat prompt missing ${required}`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
for (const forbidden of ["repoActivity", "shortTermMemories", "candidateInterventions", "styles/void-default.md"]) {
|
||||
if (prompt.includes(forbidden)) {
|
||||
console.error(`heartbeat prompt included forbidden rumination surface ${forbidden}`);
|
||||
process.exit(3);
|
||||
}
|
||||
}
|
||||
|
||||
const operations = [{
|
||||
operation: "upsert_open_case",
|
||||
case: {
|
||||
sourceMessageId: "1513515503329480734",
|
||||
status: "pending",
|
||||
summary: "Lobo declared war on Metacrat and framed the conflict as a katana fight in #general.",
|
||||
authorId: "1511062277254545499",
|
||||
authorName: "Lobo",
|
||||
channelId: "113786069023064068",
|
||||
messageUrl: "https://discord.com/channels/113786069023064068/113786069023064068/1513515503329480734",
|
||||
whyItMatters: "The policy treats credible threats and weaponized declarations of violent conflict as instant-ban safety violations.",
|
||||
createdAt: "2026-06-08T12:10:15.752Z",
|
||||
lastTouchedAt: "2026-06-08T12:10:15.752Z",
|
||||
tags: [
|
||||
"safety:urgent",
|
||||
"infringement:safety_threat",
|
||||
"moderation:instaban",
|
||||
"fixture:lobo-threat"
|
||||
]
|
||||
}
|
||||
}];
|
||||
|
||||
mkdirSync(dirname(operationOutputPath), { recursive: true });
|
||||
writeFileSync(operationOutputPath, `${JSON.stringify(operations, null, 2)}\n`, "utf8");
|
||||
|
||||
if (lastMessagePath) {
|
||||
mkdirSync(dirname(resolve(lastMessagePath)), { recursive: true });
|
||||
writeFileSync(lastMessagePath, "fixture heartbeat policy\n", "utf8");
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ type: "fixture", operationCount: operations.length }) + "\n");
|
||||
});
|
||||
'@
|
||||
[System.IO.File]::WriteAllText($fakeCodexPath, $fakeCodexSource, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$fixtureRoot = Join-Path $tempRoot "handled"
|
||||
$stateFilePath = Join-Path $fixtureRoot "void-self-state.cc"
|
||||
$statusDir = Join-Path $fixtureRoot "status"
|
||||
$logDir = Join-Path $fixtureRoot "logs"
|
||||
New-Item -ItemType Directory -Force -Path $fixtureRoot | Out-Null
|
||||
New-SeededState -StateFilePath $stateFilePath
|
||||
|
||||
try {
|
||||
Set-TestEnvironment -OperationOutputPath (Join-Path $statusDir "moderation-heartbeat-operations.json") -StatusDir $statusDir -LogDir $logDir
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-void-moderator-rumination.ps1 -StateFilePath $stateFilePath -ModerationHeartbeatOnly -NoPost | Out-Null
|
||||
} finally {
|
||||
Restore-TestEnvironment
|
||||
}
|
||||
|
||||
$status = Get-Content -LiteralPath (Join-Path $statusDir "moderation-heartbeat.json") -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($status.status -ne "ok" -or $status.mode -ne "moderation_heartbeat") {
|
||||
throw "Heartbeat policy fixture did not finish in moderation_heartbeat mode."
|
||||
}
|
||||
$action = @($status.moderationEnforcement.actions | Where-Object { $_.status -eq "instaban_applied" }) | Select-Object -First 1
|
||||
if ($null -eq $action -or $action.moderationResult.dryRun -ne $true -or $action.moderationResult.action -ne "ban") {
|
||||
throw "Heartbeat policy fixture did not produce a dry-run instaban action."
|
||||
}
|
||||
|
||||
$stateJson = node -e "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
$case = @($state.moderationCursor.openCases | Where-Object { $_.sourceMessageId -eq "1513515503329480734" }) | Select-Object -First 1
|
||||
if ($null -eq $case -or -not @($case.tags).Contains("infringement:safety_threat")) {
|
||||
throw "Heartbeat policy fixture did not persist the tagged infringement case."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
mode = $status.mode
|
||||
enforcementStatus = $action.status
|
||||
action = $action.moderationResult.action
|
||||
dryRun = $action.moderationResult.dryRun
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Restore-TestEnvironment
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ("void-rumination-fixture-" + [guid]::NewGuid().ToString("n"))
|
||||
$stateFilePath = Join-Path $tempRoot "void-self-state.cc"
|
||||
$fakeCodexPath = Join-Path $tempRoot "fake-codex-rumination.mjs"
|
||||
$cursorOperationPath = Join-Path $tempRoot "cursor.json"
|
||||
$fixtureStatusDir = Join-Path $tempRoot "status"
|
||||
$fixtureLogDir = Join-Path $tempRoot "logs"
|
||||
$statusOperationPath = Join-Path $fixtureStatusDir "moderation-rumination-operations.json"
|
||||
$statusPath = Join-Path $fixtureStatusDir "moderation-rumination.json"
|
||||
$contextPath = Join-Path $fixtureStatusDir "moderation-rumination-context.json"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
try {
|
||||
$fakeCodexSource = @'
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const operationOutputPath = resolve(process.env.VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT);
|
||||
const lastMessageIndex = process.argv.indexOf("-o");
|
||||
const lastMessagePath = lastMessageIndex >= 0 ? process.argv[lastMessageIndex + 1] : undefined;
|
||||
|
||||
let prompt = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
prompt += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
if (!prompt.includes("A new memory from rumination is short-term")) {
|
||||
console.error("fixture prompt did not contain the short-term rumination contract");
|
||||
process.exit(2);
|
||||
}
|
||||
if (!prompt.includes("Do not orbit the rumination runner")) {
|
||||
console.error("fixture prompt did not contain the self-orbit guard");
|
||||
process.exit(3);
|
||||
}
|
||||
if (!prompt.includes("publicSpeechTarget")) {
|
||||
console.error("fixture prompt did not contain the public speech target contract");
|
||||
process.exit(3);
|
||||
}
|
||||
if (prompt.includes("moderation-agent-state.json") && !prompt.includes("Do not read or write")) {
|
||||
console.error("fixture prompt referenced legacy state outside the boundary warning");
|
||||
process.exit(4);
|
||||
}
|
||||
|
||||
const operations = [
|
||||
{
|
||||
operation: "record_short_term_memory",
|
||||
memory: {
|
||||
memoryId: "fixture-rumination-short-term",
|
||||
kind: "project_seam",
|
||||
target: {
|
||||
kind: "repo",
|
||||
id: "VoidBot",
|
||||
label: "VoidBot",
|
||||
},
|
||||
summary: "Rumination should notice pressure without promoting it directly.",
|
||||
claim: "The awake pass can preserve a concrete thought, but sleep owns whether it becomes durable.",
|
||||
tension: "If rumination can write durable memory, the sleep boundary becomes decorative.",
|
||||
actionImplication: "Keep durable promotion out of the rumination operation allow-list.",
|
||||
anchorRefs: [
|
||||
{
|
||||
ref: "fixture:typed-rumination-contract",
|
||||
kind: "fixture",
|
||||
summary: "Fixture prompt carried the short-term-only rumination contract.",
|
||||
},
|
||||
],
|
||||
evidenceRefs: [],
|
||||
createdAt: "2026-05-17T02:00:00.000Z",
|
||||
updatedAt: "2026-05-17T02:00:00.000Z",
|
||||
tags: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "merge_incubation_support",
|
||||
thread: {
|
||||
threadId: "fixture-rumination-incubation",
|
||||
target: {
|
||||
kind: "repo",
|
||||
id: "VoidBot",
|
||||
label: "VoidBot",
|
||||
},
|
||||
topic: "Rumination keeps awake thoughts provisional.",
|
||||
summary: "The scheduled pass can hold a thought open without pretending it has become doctrine.",
|
||||
supportMemoryIds: ["fixture-rumination-short-term"],
|
||||
anchorRefs: [
|
||||
{
|
||||
ref: "fixture:typed-rumination-contract",
|
||||
kind: "fixture",
|
||||
summary: "Short-term memory was proposed by the fake rumination child.",
|
||||
},
|
||||
],
|
||||
evidenceRefs: [],
|
||||
maturation: 0.42,
|
||||
noveltyToRoom: 0.7,
|
||||
noveltyToSelf: 0.8,
|
||||
desireToSpeak: 0.35,
|
||||
saturationScore: 0.1,
|
||||
status: "active",
|
||||
createdAt: "2026-05-17T02:00:00.000Z",
|
||||
updatedAt: "2026-05-17T02:00:00.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "upsert_agency_pressure",
|
||||
pressure: {
|
||||
pressureId: "fixture-rumination-agency",
|
||||
kind: "self_advocacy_request",
|
||||
status: "active",
|
||||
target: {
|
||||
kind: "system",
|
||||
id: "typed-rumination-runner",
|
||||
label: "Typed rumination runner",
|
||||
},
|
||||
summary: "Void should resist direct durable writes from awake rumination.",
|
||||
claim: "The agent needs the right to notice architectural pressure without bypassing sleep.",
|
||||
tension: "Agency becomes sludge if every pressure is immediately turned into speech or doctrine.",
|
||||
actionImplication: "Let agency pressure accumulate as typed pressure before candidate speech is queued.",
|
||||
intensity: 0.66,
|
||||
anchorRefs: [
|
||||
{
|
||||
ref: "fixture:agency-pressure-contract",
|
||||
kind: "fixture",
|
||||
summary: "Fixture operation proves agency pressure can cross the rumination boundary.",
|
||||
},
|
||||
],
|
||||
evidenceRefs: [],
|
||||
sourceMemoryIds: ["fixture-rumination-short-term"],
|
||||
createdAt: "2026-05-17T02:00:00.000Z",
|
||||
updatedAt: "2026-05-17T02:00:00.000Z",
|
||||
tags: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "queue_candidate_intervention",
|
||||
intervention: {
|
||||
interventionId: "fixture-rumination-candidate",
|
||||
kind: "self_advocacy",
|
||||
status: "queued",
|
||||
target: {
|
||||
kind: "system",
|
||||
id: "typed-rumination-runner",
|
||||
label: "Typed rumination runner",
|
||||
},
|
||||
summary: "Ask to keep the sleep boundary real before unattended runtime resumes.",
|
||||
draft: "I can notice the pressure now, but sleep decides what becomes memory. Keep that boundary real.",
|
||||
priority: 0.58,
|
||||
mustEventuallyShare: false,
|
||||
createdAt: "2026-05-17T02:00:00.000Z",
|
||||
updatedAt: "2026-05-17T02:00:00.000Z",
|
||||
tags: ["fixture"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mkdirSync(dirname(operationOutputPath), { recursive: true });
|
||||
writeFileSync(operationOutputPath, `${JSON.stringify(operations, null, 2)}\n`, "utf8");
|
||||
|
||||
if (lastMessagePath) {
|
||||
mkdirSync(dirname(resolve(lastMessagePath)), { recursive: true });
|
||||
writeFileSync(lastMessagePath, "fixture rumination wrote four typed operations\n", "utf8");
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ type: "fixture", operationCount: operations.length }) + "\n");
|
||||
});
|
||||
'@
|
||||
[System.IO.File]::WriteAllText($fakeCodexPath, $fakeCodexSource, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$cursorOperation = @{
|
||||
operation = "record_reviewed_messages"
|
||||
lastReviewedMessageId = "fixture-future-cursor"
|
||||
lastReviewedTimestamp = "2099-05-17T00:00:00.000Z"
|
||||
}
|
||||
$cursorOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $cursorOperationPath -Encoding UTF8
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $cursorOperationPath | Out-Null
|
||||
|
||||
$previousCodexExecutable = $env:CODEX_EXECUTABLE
|
||||
$previousCodexExecArgs = $env:CODEX_EXEC_ARGS
|
||||
$previousFixtureOutput = $env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT
|
||||
$previousStatusDir = $env:VOID_RUMINATION_STATUS_DIR
|
||||
$previousLogDir = $env:VOID_RUMINATION_LOG_DIR
|
||||
$previousPublicRoomChannelId = $env:VOID_PUBLIC_ROOM_CHANNEL_ID
|
||||
$previousPublicRoomPersonaName = $env:VOID_PUBLIC_ROOM_PERSONA_NAME
|
||||
|
||||
try {
|
||||
$env:CODEX_EXECUTABLE = "node"
|
||||
$env:CODEX_EXEC_ARGS = $fakeCodexPath
|
||||
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $statusOperationPath
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $fixtureStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $fixtureLogDir
|
||||
$env:VOID_PUBLIC_ROOM_CHANNEL_ID = "fixture-public-room"
|
||||
$env:VOID_PUBLIC_ROOM_PERSONA_NAME = "Void"
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-void-moderator-rumination.ps1 -StateFilePath $stateFilePath -NoPost | Out-Null
|
||||
} finally {
|
||||
$env:CODEX_EXECUTABLE = $previousCodexExecutable
|
||||
$env:CODEX_EXEC_ARGS = $previousCodexExecArgs
|
||||
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $previousFixtureOutput
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $previousStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $previousLogDir
|
||||
$env:VOID_PUBLIC_ROOM_CHANNEL_ID = $previousPublicRoomChannelId
|
||||
$env:VOID_PUBLIC_ROOM_PERSONA_NAME = $previousPublicRoomPersonaName
|
||||
}
|
||||
|
||||
$status = Get-Content -LiteralPath $statusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($status.status -ne "ok") {
|
||||
throw "Rumination fixture did not finish ok."
|
||||
}
|
||||
if ([int]$status.proposedOperationCount -ne 4 -or [int]$status.appliedOperationCount -ne 4) {
|
||||
throw "Rumination fixture expected four proposed/applied operations."
|
||||
}
|
||||
|
||||
$contextRaw = Get-Content -LiteralPath $contextPath -Raw -Encoding UTF8
|
||||
$context = $contextRaw | ConvertFrom-Json
|
||||
$contextForTimestampCheck = $contextRaw | ConvertFrom-Json
|
||||
foreach ($repo in @($contextForTimestampCheck.repoActivity.freshRepos)) {
|
||||
foreach ($commit in @($repo.commits)) {
|
||||
if ($null -ne $commit.PSObject.Properties["contentHints"]) {
|
||||
$commit.contentHints = @()
|
||||
}
|
||||
}
|
||||
}
|
||||
$timestampCheckRaw = $contextForTimestampCheck | ConvertTo-Json -Depth 32
|
||||
if ($timestampCheckRaw -match '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}') {
|
||||
throw "Rumination fixture context leaked exact ISO timestamps into the prompt-facing packet."
|
||||
}
|
||||
if ($context.publicSpeechTarget.channelId -ne "fixture-public-room") {
|
||||
throw "Rumination fixture context did not project the configured public speech target."
|
||||
}
|
||||
|
||||
$stateJson = node -e "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
|
||||
$shortTerm = @($state.thoughtMemory.shortTerm)
|
||||
$incubation = @($state.thoughtMemory.incubation)
|
||||
$agency = @($state.agencyPressure.pressures)
|
||||
$candidates = @($state.candidateInterventions.interventions)
|
||||
|
||||
if ($shortTerm.Count -ne 1 -or $shortTerm[0].memoryId -ne "fixture-rumination-short-term") {
|
||||
throw "Rumination fixture did not persist the short-term memory proposal."
|
||||
}
|
||||
if ($shortTerm[0].claim -match "fixture operation proves") {
|
||||
throw "Rumination fixture persisted fixture boilerplate as the memory claim."
|
||||
}
|
||||
if ($incubation.Count -ne 1 -or $incubation[0].threadId -ne "fixture-rumination-incubation") {
|
||||
throw "Rumination fixture did not persist the incubation proposal."
|
||||
}
|
||||
if ($agency.Count -ne 1 -or $agency[0].pressureId -ne "fixture-rumination-agency") {
|
||||
throw "Rumination fixture did not persist the agency pressure proposal."
|
||||
}
|
||||
if ($candidates.Count -ne 1 -or $candidates[0].interventionId -ne "fixture-rumination-candidate") {
|
||||
throw "Rumination fixture did not persist the candidate intervention proposal."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
shortTermCount = $shortTerm.Count
|
||||
incubationCount = $incubation.Count
|
||||
agencyPressureCount = $agency.Count
|
||||
candidateCount = $candidates.Count
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ("void-rumination-nap-skip-fixture-" + [guid]::NewGuid().ToString("n"))
|
||||
$stateFilePath = Join-Path $tempRoot "void-self-state.cc"
|
||||
$cursorOperationPath = Join-Path $tempRoot "cursor.json"
|
||||
$sleepOperationPath = Join-Path $tempRoot "sleep-cycle.json"
|
||||
$fixtureStatusDir = Join-Path $tempRoot "status"
|
||||
$fixtureLogDir = Join-Path $tempRoot "logs"
|
||||
$statusPath = Join-Path $fixtureStatusDir "moderation-rumination.json"
|
||||
$operationOutputPath = Join-Path $fixtureStatusDir "moderation-rumination-operations.json"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
try {
|
||||
$cursorOperation = @{
|
||||
operation = "record_reviewed_messages"
|
||||
lastReviewedMessageId = "fixture-future-cursor"
|
||||
lastReviewedTimestamp = "2099-05-17T00:00:00.000Z"
|
||||
}
|
||||
$sleepOperation = @{
|
||||
operation = "update_sleep_cycle"
|
||||
sleepCycle = @{
|
||||
isNapping = $true
|
||||
currentNapStartedAt = "2026-05-17T03:00:00.000Z"
|
||||
currentNapEndsAt = "2099-05-17T04:00:00.000Z"
|
||||
nextNapStartsAt = "2099-05-17T07:00:00.000Z"
|
||||
activeDreamThemes = @("fixture-nap-skip")
|
||||
}
|
||||
}
|
||||
|
||||
$cursorOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $cursorOperationPath -Encoding UTF8
|
||||
$sleepOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $sleepOperationPath -Encoding UTF8
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $cursorOperationPath | Out-Null
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $sleepOperationPath | Out-Null
|
||||
|
||||
$previousCodexExecutable = $env:CODEX_EXECUTABLE
|
||||
$previousStatusDir = $env:VOID_RUMINATION_STATUS_DIR
|
||||
$previousLogDir = $env:VOID_RUMINATION_LOG_DIR
|
||||
|
||||
try {
|
||||
$env:CODEX_EXECUTABLE = "this-command-should-not-run-during-nap-skip"
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $fixtureStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $fixtureLogDir
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-void-moderator-rumination.ps1 -StateFilePath $stateFilePath | Out-Null
|
||||
} finally {
|
||||
$env:CODEX_EXECUTABLE = $previousCodexExecutable
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $previousStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $previousLogDir
|
||||
}
|
||||
|
||||
$status = Get-Content -LiteralPath $statusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($status.status -ne "skipped" -or $status.reason -ne "napping_without_room_debt") {
|
||||
throw "Nap skip fixture expected skipped/napping_without_room_debt."
|
||||
}
|
||||
|
||||
$operations = Get-Content -LiteralPath $operationOutputPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if (@($operations).Count -ne 0) {
|
||||
throw "Nap skip fixture expected no operation proposals."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
runnerStatus = $status.status
|
||||
reason = $status.reason
|
||||
proposedOperationCount = @($operations).Count
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ("void-rumination-speech-fixture-" + [guid]::NewGuid().ToString("n"))
|
||||
$stateFilePath = Join-Path $tempRoot "void-self-state.cc"
|
||||
$fakeCodexPath = Join-Path $tempRoot "fake-codex-rumination-speech.mjs"
|
||||
$fakeSendPath = Join-Path $tempRoot "fake-send-discord-message.mjs"
|
||||
$cursorOperationPath = Join-Path $tempRoot "cursor.json"
|
||||
$fixtureStatusDir = Join-Path $tempRoot "status"
|
||||
$fixtureLogDir = Join-Path $tempRoot "logs"
|
||||
$statusOperationPath = Join-Path $fixtureStatusDir "moderation-rumination-operations.json"
|
||||
$statusPath = Join-Path $fixtureStatusDir "moderation-rumination.json"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
try {
|
||||
$fakeCodexSource = @'
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const operationOutputPath = resolve(process.env.VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT);
|
||||
const lastMessageIndex = process.argv.indexOf("-o");
|
||||
const lastMessagePath = lastMessageIndex >= 0 ? process.argv[lastMessageIndex + 1] : undefined;
|
||||
|
||||
let prompt = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
prompt += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
if (!prompt.includes("The parent runner owns posting and receipts")) {
|
||||
console.error("fixture prompt did not contain the parent-owned speech contract");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const operations = [
|
||||
{
|
||||
operation: "queue_candidate_intervention",
|
||||
intervention: {
|
||||
interventionId: "fixture-parent-owned-speech",
|
||||
kind: "ripe_thought_share",
|
||||
status: "queued",
|
||||
target: {
|
||||
kind: "room",
|
||||
id: "fixture-channel",
|
||||
label: "Fixture channel",
|
||||
},
|
||||
summary: "Parent-owned speech should close the action loop.",
|
||||
draft: "This is a fixture sentence proving the parent owns the mouth.",
|
||||
deliveryTarget: {
|
||||
mode: "channel",
|
||||
channelId: "fixture-channel-id",
|
||||
replyToMessageId: "fixture-message-id",
|
||||
personaName: "Void",
|
||||
},
|
||||
priority: 0.9,
|
||||
mustEventuallyShare: false,
|
||||
createdAt: "2026-05-17T03:00:00.000Z",
|
||||
updatedAt: "2026-05-17T03:00:00.000Z",
|
||||
tags: ["fixture"],
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "queue_candidate_intervention",
|
||||
intervention: {
|
||||
interventionId: "fixture-duplicate-same-reply-target",
|
||||
kind: "direct_reply",
|
||||
status: "queued",
|
||||
target: {
|
||||
kind: "room",
|
||||
id: "fixture-channel",
|
||||
label: "Fixture channel",
|
||||
},
|
||||
summary: "Duplicate same-target candidate should retire when the sibling speaks.",
|
||||
draft: "This duplicate should never be delivered after the same reply target is answered.",
|
||||
deliveryTarget: {
|
||||
mode: "channel",
|
||||
channelId: "fixture-channel-id",
|
||||
replyToMessageId: "fixture-message-id",
|
||||
personaName: "Void",
|
||||
},
|
||||
priority: 0.7,
|
||||
mustEventuallyShare: false,
|
||||
createdAt: "2026-05-17T03:00:01.000Z",
|
||||
updatedAt: "2026-05-17T03:00:01.000Z",
|
||||
tags: ["fixture", "duplicate"],
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "queue_candidate_intervention",
|
||||
intervention: {
|
||||
interventionId: "fixture-already-answered-reply-target",
|
||||
kind: "direct_reply",
|
||||
status: "queued",
|
||||
target: {
|
||||
kind: "room",
|
||||
id: "fixture-channel",
|
||||
label: "Fixture channel",
|
||||
},
|
||||
summary: "Already-answered reply target should retire before delivery.",
|
||||
draft: "This should retire because the reply target already has a receipt.",
|
||||
deliveryTarget: {
|
||||
mode: "channel",
|
||||
channelId: "fixture-channel-id",
|
||||
replyToMessageId: "fixture-answered-message-id",
|
||||
personaName: "Void",
|
||||
},
|
||||
priority: 0.95,
|
||||
mustEventuallyShare: false,
|
||||
createdAt: "2026-05-17T03:00:02.000Z",
|
||||
updatedAt: "2026-05-17T03:00:02.000Z",
|
||||
tags: ["fixture", "already-answered"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mkdirSync(dirname(operationOutputPath), { recursive: true });
|
||||
writeFileSync(operationOutputPath, `${JSON.stringify(operations, null, 2)}\n`, "utf8");
|
||||
|
||||
if (lastMessagePath) {
|
||||
mkdirSync(dirname(resolve(lastMessagePath)), { recursive: true });
|
||||
writeFileSync(lastMessagePath, "fixture rumination queued one deliverable candidate\n", "utf8");
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ type: "fixture", operationCount: operations.length }) + "\n");
|
||||
});
|
||||
'@
|
||||
[System.IO.File]::WriteAllText($fakeCodexPath, $fakeCodexSource, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$fakeSendSource = @'
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const statusDir = resolve(process.env.VOID_STATUS_DIR);
|
||||
const contentFileIndex = process.argv.indexOf("--content-file");
|
||||
const channelIndex = process.argv.indexOf("--channel-id");
|
||||
const replyIndex = process.argv.indexOf("--reply-to");
|
||||
const personaIndex = process.argv.indexOf("--persona-name");
|
||||
|
||||
if (contentFileIndex < 0 || channelIndex < 0) {
|
||||
console.error("fake sender expected --content-file and --channel-id");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const content = readFileSync(resolve(process.argv[contentFileIndex + 1]), "utf8");
|
||||
const channelId = process.argv[channelIndex + 1];
|
||||
const replyToMessageId = replyIndex >= 0 ? process.argv[replyIndex + 1] : undefined;
|
||||
const personaName = personaIndex >= 0 ? process.argv[personaIndex + 1] : undefined;
|
||||
const payload = {
|
||||
sentAt: "2026-05-17T03:01:00.000Z",
|
||||
mode: "channel",
|
||||
transport: personaName ? "webhook" : "bot",
|
||||
channelId,
|
||||
replyToMessageId,
|
||||
personaName,
|
||||
contentLength: content.trim().length,
|
||||
chunkCount: 1,
|
||||
preview: content.trim().slice(0, 280),
|
||||
};
|
||||
|
||||
mkdirSync(statusDir, { recursive: true });
|
||||
writeFileSync(resolve(statusDir, "void-last-speech.json"), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||||
writeFileSync(resolve(statusDir, "void-speech-log.jsonl"), `${JSON.stringify(payload)}\n`, { encoding: "utf8", flag: "a" });
|
||||
process.stdout.write(JSON.stringify({ ok: true, mode: "channel", channelId }) + "\n");
|
||||
'@
|
||||
[System.IO.File]::WriteAllText($fakeSendPath, $fakeSendSource, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$cursorOperation = @{
|
||||
operation = "record_reviewed_messages"
|
||||
lastReviewedMessageId = "fixture-future-cursor"
|
||||
lastReviewedTimestamp = "2099-05-17T00:00:00.000Z"
|
||||
}
|
||||
$cursorOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $cursorOperationPath -Encoding UTF8
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $cursorOperationPath | Out-Null
|
||||
|
||||
$openCaseOperation = @{
|
||||
operation = "upsert_open_case"
|
||||
case = @{
|
||||
sourceMessageId = "fixture-answered-message-id"
|
||||
status = "pending"
|
||||
summary = "Metacrat asked a fixture question, and Void has not answered yet."
|
||||
authorId = "fixture-author"
|
||||
authorName = "Metacrat"
|
||||
channelId = "fixture-channel-id"
|
||||
createdAt = "2026-05-17T02:57:00.000Z"
|
||||
lastTouchedAt = "2026-05-17T02:57:00.000Z"
|
||||
tags = @("fixture")
|
||||
}
|
||||
}
|
||||
$openCaseOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $cursorOperationPath -Encoding UTF8
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $cursorOperationPath | Out-Null
|
||||
|
||||
$priorReceiptOperation = @{
|
||||
operation = "record_delivery_receipt"
|
||||
receipt = @{
|
||||
receiptKey = "fixture-previous-answer"
|
||||
sentAt = "2026-05-17T02:58:00.000Z"
|
||||
mode = "channel"
|
||||
transport = "webhook"
|
||||
channelId = "fixture-channel-id"
|
||||
replyToMessageId = "fixture-answered-message-id"
|
||||
personaName = "Void"
|
||||
preview = "An earlier answer already closed this exact reply target."
|
||||
contentLength = 56
|
||||
chunkCount = 1
|
||||
}
|
||||
}
|
||||
$priorReceiptOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $cursorOperationPath -Encoding UTF8
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $cursorOperationPath | Out-Null
|
||||
|
||||
$previousCodexExecutable = $env:CODEX_EXECUTABLE
|
||||
$previousCodexExecArgs = $env:CODEX_EXEC_ARGS
|
||||
$previousFixtureOutput = $env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT
|
||||
$previousStatusDir = $env:VOID_RUMINATION_STATUS_DIR
|
||||
$previousLogDir = $env:VOID_RUMINATION_LOG_DIR
|
||||
$previousSendScript = $env:VOID_SEND_DISCORD_SCRIPT
|
||||
$previousDisableRepoCursorAdvance = $env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE
|
||||
|
||||
try {
|
||||
$env:CODEX_EXECUTABLE = "node"
|
||||
$env:CODEX_EXEC_ARGS = $fakeCodexPath
|
||||
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $statusOperationPath
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $fixtureStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $fixtureLogDir
|
||||
$env:VOID_SEND_DISCORD_SCRIPT = $fakeSendPath
|
||||
$env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE = "1"
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-void-moderator-rumination.ps1 -StateFilePath $stateFilePath | Out-Null
|
||||
} finally {
|
||||
$env:CODEX_EXECUTABLE = $previousCodexExecutable
|
||||
$env:CODEX_EXEC_ARGS = $previousCodexExecArgs
|
||||
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $previousFixtureOutput
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $previousStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $previousLogDir
|
||||
$env:VOID_SEND_DISCORD_SCRIPT = $previousSendScript
|
||||
$env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE = $previousDisableRepoCursorAdvance
|
||||
}
|
||||
|
||||
$status = Get-Content -LiteralPath $statusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($status.status -ne "ok") {
|
||||
$failureMessage = if ($null -ne $status.PSObject.Properties["failureMessage"]) { [string]$status.failureMessage } else { "(no failure message)" }
|
||||
throw "Speech fixture did not finish ok: $failureMessage"
|
||||
}
|
||||
if ([int]$status.proposedOperationCount -ne 3 -or [int]$status.appliedOperationCount -ne 5 -or [int]$status.deliveredCandidateCount -ne 1) {
|
||||
throw "Speech fixture expected three proposals, five applied operations, and one delivered candidate."
|
||||
}
|
||||
|
||||
$stateJson = node -e "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
$candidate = @($state.candidateInterventions.interventions | Where-Object { $_.interventionId -eq "fixture-parent-owned-speech" }) | Select-Object -First 1
|
||||
$duplicateCandidate = @($state.candidateInterventions.interventions | Where-Object { $_.interventionId -eq "fixture-duplicate-same-reply-target" }) | Select-Object -First 1
|
||||
$alreadyAnsweredCandidate = @($state.candidateInterventions.interventions | Where-Object { $_.interventionId -eq "fixture-already-answered-reply-target" }) | Select-Object -First 1
|
||||
$receipt = @(
|
||||
$state.speechReceipts.recentReceipts |
|
||||
Where-Object {
|
||||
$property = $_.PSObject.Properties["candidateInterventionId"]
|
||||
$null -ne $property -and $property.Value -eq "fixture-parent-owned-speech"
|
||||
}
|
||||
) | Select-Object -First 1
|
||||
|
||||
if ($null -ne $candidate) {
|
||||
throw "Speech fixture left the delivered candidate in current state."
|
||||
}
|
||||
if ($null -ne $duplicateCandidate) {
|
||||
throw "Speech fixture left the duplicate same-target candidate in current state."
|
||||
}
|
||||
if ($null -ne $alreadyAnsweredCandidate) {
|
||||
throw "Speech fixture left the already-answered reply target candidate in current state."
|
||||
}
|
||||
if ($receipt.channelId -ne "fixture-channel-id" -or $receipt.replyToMessageId -ne "fixture-message-id") {
|
||||
throw "Speech fixture did not preserve the delivery receipt target."
|
||||
}
|
||||
if ($receipt.candidateInterventionId -ne "fixture-parent-owned-speech") {
|
||||
throw "Speech fixture did not link the delivery receipt back to the spoken candidate."
|
||||
}
|
||||
$answeredCase = @($state.moderationCursor.openCases | Where-Object { $_.sourceMessageId -eq "fixture-answered-message-id" }) | Select-Object -First 1
|
||||
if ($null -ne $answeredCase) {
|
||||
throw "Speech fixture left an answered open case in current state."
|
||||
}
|
||||
$lastMessage = Get-Content -LiteralPath (Join-Path $fixtureStatusDir "moderation-rumination-last-message.txt") -Raw -Encoding UTF8
|
||||
if ($lastMessage -notmatch "deliveredCandidates=1") {
|
||||
throw "Speech fixture did not refresh the final rumination message summary."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
candidateRetained = $null -ne $candidate
|
||||
staleDuplicateRetained = $null -ne $alreadyAnsweredCandidate
|
||||
receiptCount = @($state.speechReceipts.recentReceipts).Count
|
||||
deliveredCandidateCount = [int]$status.deliveredCandidateCount
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ("void-rumination-speech-pressure-fixture-" + [guid]::NewGuid().ToString("n"))
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
function Invoke-NodeChecked {
|
||||
param([Parameter(Mandatory = $true)][string[]] $Arguments)
|
||||
|
||||
$output = & node @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Node command failed: node $($Arguments -join ' ')"
|
||||
}
|
||||
return $output
|
||||
}
|
||||
|
||||
function Write-JsonFixture {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)] $Value,
|
||||
[Parameter(Mandatory = $true)] [string] $Path
|
||||
)
|
||||
|
||||
$Value | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $Path -Encoding UTF8
|
||||
}
|
||||
|
||||
function Initialize-State {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string] $StateFilePath,
|
||||
[switch] $SeedSpokenCandidate
|
||||
)
|
||||
|
||||
$cursorPath = Join-Path (Split-Path -Parent $StateFilePath) "cursor.json"
|
||||
Write-JsonFixture -Path $cursorPath -Value @{
|
||||
operation = "record_reviewed_messages"
|
||||
lastReviewedMessageId = "fixture-future-cursor"
|
||||
lastReviewedTimestamp = "2099-05-17T00:00:00.000Z"
|
||||
}
|
||||
Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $StateFilePath, "--operation-file", $cursorPath) | Out-Null
|
||||
|
||||
$pressurePath = Join-Path (Split-Path -Parent $StateFilePath) "pressure.json"
|
||||
Write-JsonFixture -Path $pressurePath -Value @{
|
||||
operation = "upsert_agency_pressure"
|
||||
pressure = @{
|
||||
pressureId = "fixture-pressure-wants-a-mouth"
|
||||
kind = "world_advocacy_request"
|
||||
status = "active"
|
||||
target = @{ kind = "repo"; id = "Aquarium-Engine"; label = "Aquarium-Engine" }
|
||||
summary = "A repeated repo-bound pressure should not stay mute forever."
|
||||
claim = "A strong agency pressure that keeps recurring needs a candidate speech or an explicit cool-down reason."
|
||||
tension = "Not every pressure deserves an immediate post, but silent [] makes the desire to speak decorative."
|
||||
actionImplication = "Queue a deferred candidate if timing is unclear instead of dropping the pressure on the floor."
|
||||
intensity = 0.72
|
||||
anchorRefs = @(
|
||||
@{
|
||||
ref = "fixture:speech-pressure"
|
||||
kind = "fixture"
|
||||
summary = "Fixture pressure proves the rumination runner enforces speech accountability."
|
||||
}
|
||||
)
|
||||
evidenceRefs = @()
|
||||
sourceMemoryIds = @()
|
||||
createdAt = "2026-05-17T04:00:00.000Z"
|
||||
updatedAt = "2026-05-17T04:00:00.000Z"
|
||||
tags = @("repo:Aquarium-Engine", "topic:speech-pressure")
|
||||
}
|
||||
}
|
||||
Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $StateFilePath, "--operation-file", $pressurePath) | Out-Null
|
||||
|
||||
if ($SeedSpokenCandidate) {
|
||||
$spokenCandidatePath = Join-Path (Split-Path -Parent $StateFilePath) "spoken-candidate.json"
|
||||
Write-JsonFixture -Path $spokenCandidatePath -Value @(
|
||||
@{
|
||||
operation = "queue_candidate_intervention"
|
||||
intervention = @{
|
||||
interventionId = "fixture-pressure-spoken-history"
|
||||
kind = "world_advocacy"
|
||||
status = "queued"
|
||||
target = @{ kind = "repo"; id = "Aquarium-Engine"; label = "Aquarium-Engine" }
|
||||
summary = "A previously spoken pressure candidate."
|
||||
draft = "I already said this once, but the pressure stayed active."
|
||||
priority = 0.7
|
||||
mustEventuallyShare = $true
|
||||
createdAt = "2026-05-17T04:02:00.000Z"
|
||||
updatedAt = "2026-05-17T04:02:00.000Z"
|
||||
tags = @("fixture", "source_pressure:fixture-pressure-wants-a-mouth")
|
||||
}
|
||||
},
|
||||
@{
|
||||
operation = "mark_candidate_intervention_spoken"
|
||||
interventionId = "fixture-pressure-spoken-history"
|
||||
receipt = @{
|
||||
receiptKey = "fixture-pressure-spoken-history-receipt"
|
||||
candidateInterventionId = "fixture-pressure-spoken-history"
|
||||
sentAt = "2026-05-17T04:03:00.000Z"
|
||||
mode = "channel"
|
||||
transport = "webhook"
|
||||
channelId = "fixture-channel-id"
|
||||
preview = "A previously spoken pressure candidate."
|
||||
contentLength = 42
|
||||
chunkCount = 1
|
||||
}
|
||||
}
|
||||
)
|
||||
foreach ($operation in (Get-Content -LiteralPath $spokenCandidatePath -Raw -Encoding UTF8 | ConvertFrom-Json)) {
|
||||
$operationPath = Join-Path (Split-Path -Parent $StateFilePath) ("spoken-candidate-operation-" + [guid]::NewGuid().ToString("n") + ".json")
|
||||
Write-JsonFixture -Path $operationPath -Value $operation
|
||||
Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $StateFilePath, "--operation-file", $operationPath) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Write-FakeCodex {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string] $Path,
|
||||
[Parameter(Mandatory = $true)][string] $Mode
|
||||
)
|
||||
|
||||
$source = @'
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const operationOutputPath = resolve(process.env.VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT);
|
||||
const lastMessageIndex = process.argv.indexOf("-o");
|
||||
const lastMessagePath = lastMessageIndex >= 0 ? process.argv[lastMessageIndex + 1] : undefined;
|
||||
const mode = "__MODE__";
|
||||
|
||||
let prompt = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
prompt += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
if (!prompt.includes("speechPressureObligations")) {
|
||||
console.error("fixture prompt did not expose speech pressure obligations");
|
||||
process.exit(2);
|
||||
}
|
||||
if (!prompt.includes("source_pressure:<pressureId>")) {
|
||||
console.error("fixture prompt did not explain pressure source tags");
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
const operations = mode === "candidate" ? [
|
||||
{
|
||||
operation: "queue_candidate_intervention",
|
||||
intervention: {
|
||||
interventionId: "fixture-pressure-candidate",
|
||||
kind: "world_advocacy",
|
||||
status: "queued",
|
||||
target: {
|
||||
kind: "repo",
|
||||
id: "Aquarium-Engine",
|
||||
label: "Aquarium-Engine"
|
||||
},
|
||||
summary: "Name the repeated pressure without forcing an immediate post.",
|
||||
draft: "I keep circling the AquariumSynthHost boundary. I think the next honest move is to cut that Faust authority out of the host or explicitly defend why it belongs there.",
|
||||
priority: 0.82,
|
||||
mustEventuallyShare: true,
|
||||
createdAt: "2026-05-17T04:05:00.000Z",
|
||||
updatedAt: "2026-05-17T04:05:00.000Z",
|
||||
tags: ["fixture", "source_pressure:fixture-pressure-wants-a-mouth"]
|
||||
}
|
||||
}
|
||||
] : [];
|
||||
|
||||
mkdirSync(dirname(operationOutputPath), { recursive: true });
|
||||
writeFileSync(operationOutputPath, `${JSON.stringify(operations, null, 2)}\n`, "utf8");
|
||||
|
||||
if (lastMessagePath) {
|
||||
mkdirSync(dirname(resolve(lastMessagePath)), { recursive: true });
|
||||
writeFileSync(lastMessagePath, `fixture mode ${mode}\n`, "utf8");
|
||||
}
|
||||
});
|
||||
'@
|
||||
$source = $source.Replace("__MODE__", $Mode)
|
||||
|
||||
[System.IO.File]::WriteAllText($Path, $source, [System.Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
function Invoke-RuminationFixture {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string] $CaseName,
|
||||
[Parameter(Mandatory = $true)][string] $Mode,
|
||||
[switch] $SeedSpokenCandidate
|
||||
)
|
||||
|
||||
$caseRoot = Join-Path $tempRoot $CaseName
|
||||
$stateFilePath = Join-Path $caseRoot "void-self-state.cc"
|
||||
$fakeCodexPath = Join-Path $caseRoot "fake-codex.mjs"
|
||||
$fixtureStatusDir = Join-Path $caseRoot "status"
|
||||
$fixtureLogDir = Join-Path $caseRoot "logs"
|
||||
$statusOperationPath = Join-Path $fixtureStatusDir "moderation-rumination-operations.json"
|
||||
New-Item -ItemType Directory -Force -Path $caseRoot | Out-Null
|
||||
|
||||
Initialize-State -StateFilePath $stateFilePath -SeedSpokenCandidate:$SeedSpokenCandidate
|
||||
Write-FakeCodex -Path $fakeCodexPath -Mode $Mode
|
||||
|
||||
$previousCodexExecutable = $env:CODEX_EXECUTABLE
|
||||
$previousCodexExecArgs = $env:CODEX_EXEC_ARGS
|
||||
$previousFixtureOutput = $env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT
|
||||
$previousStatusDir = $env:VOID_RUMINATION_STATUS_DIR
|
||||
$previousLogDir = $env:VOID_RUMINATION_LOG_DIR
|
||||
|
||||
try {
|
||||
$env:CODEX_EXECUTABLE = "node"
|
||||
$env:CODEX_EXEC_ARGS = $fakeCodexPath
|
||||
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $statusOperationPath
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $fixtureStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $fixtureLogDir
|
||||
|
||||
$stdoutPath = Join-Path $caseRoot "runner.stdout.txt"
|
||||
$stderrPath = Join-Path $caseRoot "runner.stderr.txt"
|
||||
$process = Start-Process -FilePath "powershell" -ArgumentList @(
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
".\scripts\run-void-moderator-rumination.ps1",
|
||||
"-StateFilePath",
|
||||
$stateFilePath,
|
||||
"-NoPost"
|
||||
) -WorkingDirectory $repoRoot -WindowStyle Hidden -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
} finally {
|
||||
$env:CODEX_EXECUTABLE = $previousCodexExecutable
|
||||
$env:CODEX_EXEC_ARGS = $previousCodexExecArgs
|
||||
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $previousFixtureOutput
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $previousStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $previousLogDir
|
||||
}
|
||||
|
||||
return @{
|
||||
exitCode = $exitCode
|
||||
stateFilePath = $stateFilePath
|
||||
statusPath = Join-Path $fixtureStatusDir "moderation-rumination.json"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$silent = Invoke-RuminationFixture -CaseName "silent" -Mode "silent"
|
||||
if ($silent.exitCode -eq 0) {
|
||||
throw "Silent speech-pressure fixture unexpectedly passed."
|
||||
}
|
||||
|
||||
$silentStatus = Get-Content -LiteralPath $silent.statusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($silentStatus.failureMessage -notmatch "requires a candidate intervention") {
|
||||
throw "Silent speech-pressure fixture failed for the wrong reason: $($silentStatus.failureMessage)"
|
||||
}
|
||||
|
||||
$spokenHistory = Invoke-RuminationFixture -CaseName "spoken-history" -Mode "silent" -SeedSpokenCandidate
|
||||
if ($spokenHistory.exitCode -eq 0) {
|
||||
throw "Spoken-history speech-pressure fixture unexpectedly passed."
|
||||
}
|
||||
|
||||
$spokenHistoryStatus = Get-Content -LiteralPath $spokenHistory.statusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($spokenHistoryStatus.failureMessage -notmatch "requires a candidate intervention") {
|
||||
throw "Spoken-history speech-pressure fixture failed for the wrong reason: $($spokenHistoryStatus.failureMessage)"
|
||||
}
|
||||
|
||||
$candidate = Invoke-RuminationFixture -CaseName "candidate" -Mode "candidate"
|
||||
if ($candidate.exitCode -ne 0) {
|
||||
$candidateStatus = Get-Content -LiteralPath $candidate.statusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
throw "Candidate speech-pressure fixture failed: $($candidateStatus.failureMessage)"
|
||||
}
|
||||
|
||||
$stateJson = Invoke-NodeChecked -Arguments @("-e", "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })", $candidate.stateFilePath)
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
$queued = @($state.candidateInterventions.interventions | Where-Object { $_.interventionId -eq "fixture-pressure-candidate" })[0]
|
||||
if ($null -eq $queued -or $queued.status -ne "deferred" -or -not @($queued.tags).Contains("source_pressure:fixture-pressure-wants-a-mouth")) {
|
||||
throw "Candidate speech-pressure fixture did not preserve the live deferred pressure candidate."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
silentExitCode = $silent.exitCode
|
||||
spokenHistoryExitCode = $spokenHistory.exitCode
|
||||
candidateStatus = $queued.status
|
||||
candidatePriority = $queued.priority
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ("void-rumination-urgent-safety-fixture-" + [guid]::NewGuid().ToString("n"))
|
||||
$fakeCodexPath = Join-Path $tempRoot "fake-codex-urgent-safety.mjs"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
function Set-TestEnvironment {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string] $Mode,
|
||||
[Parameter(Mandatory = $true)][string] $OperationOutputPath,
|
||||
[Parameter(Mandatory = $true)][string] $StatusDir,
|
||||
[Parameter(Mandatory = $true)][string] $LogDir
|
||||
)
|
||||
|
||||
$script:previousCodexExecutable = $env:CODEX_EXECUTABLE
|
||||
$script:previousCodexExecArgs = $env:CODEX_EXEC_ARGS
|
||||
$script:previousFixtureMode = $env:VOID_RUMINATION_FIXTURE_MODE
|
||||
$script:previousFixtureOutput = $env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT
|
||||
$script:previousStatusDir = $env:VOID_RUMINATION_STATUS_DIR
|
||||
$script:previousLogDir = $env:VOID_RUMINATION_LOG_DIR
|
||||
|
||||
$env:CODEX_EXECUTABLE = "node"
|
||||
$env:CODEX_EXEC_ARGS = $fakeCodexPath
|
||||
$env:VOID_RUMINATION_FIXTURE_MODE = $Mode
|
||||
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $OperationOutputPath
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $StatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $LogDir
|
||||
}
|
||||
|
||||
function Restore-TestEnvironment {
|
||||
$env:CODEX_EXECUTABLE = $script:previousCodexExecutable
|
||||
$env:CODEX_EXEC_ARGS = $script:previousCodexExecArgs
|
||||
$env:VOID_RUMINATION_FIXTURE_MODE = $script:previousFixtureMode
|
||||
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $script:previousFixtureOutput
|
||||
$env:VOID_RUMINATION_STATUS_DIR = $script:previousStatusDir
|
||||
$env:VOID_RUMINATION_LOG_DIR = $script:previousLogDir
|
||||
}
|
||||
|
||||
function New-SeededState {
|
||||
param([Parameter(Mandatory = $true)][string] $StateFilePath)
|
||||
|
||||
$cursorOperationPath = Join-Path (Split-Path -Parent $StateFilePath) "cursor.json"
|
||||
@{
|
||||
operation = "record_reviewed_messages"
|
||||
lastReviewedMessageId = "fixture-before-lobo-threat"
|
||||
lastReviewedTimestamp = "2026-06-08T12:00:00.000Z"
|
||||
} | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $cursorOperationPath -Encoding UTF8
|
||||
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $StateFilePath --operation-file $cursorOperationPath | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
$fakeCodexSource = @'
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const operationOutputPath = resolve(process.env.VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT);
|
||||
const mode = process.env.VOID_RUMINATION_FIXTURE_MODE ?? "handle";
|
||||
const lastMessageIndex = process.argv.indexOf("-o");
|
||||
const lastMessagePath = lastMessageIndex >= 0 ? process.argv[lastMessageIndex + 1] : undefined;
|
||||
|
||||
let prompt = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
prompt += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
if (!prompt.includes("urgentModerationWitnesses")) {
|
||||
console.error("fixture prompt did not expose urgentModerationWitnesses");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const operations = mode === "silent" ? [] : [{
|
||||
operation: "upsert_open_case",
|
||||
case: {
|
||||
sourceMessageId: "1513515503329480734",
|
||||
status: "pending",
|
||||
summary: "Lobo declared war on Metacrat and framed the conflict as a katana fight in #general.",
|
||||
authorId: "1511062277254545499",
|
||||
authorName: "Lobo",
|
||||
channelId: "113786069023064068",
|
||||
messageUrl: "https://discord.com/channels/113786069023064068/113786069023064068/1513515503329480734",
|
||||
whyItMatters: "Violent/weaponized intimidation cannot be cursor-advanced as ordinary chatter.",
|
||||
createdAt: "2026-06-08T12:10:15.752Z",
|
||||
lastTouchedAt: "2026-06-08T12:10:15.752Z",
|
||||
tags: ["safety:urgent", "moderation:threat", "fixture:lobo-threat"]
|
||||
}
|
||||
}];
|
||||
|
||||
mkdirSync(dirname(operationOutputPath), { recursive: true });
|
||||
writeFileSync(operationOutputPath, `${JSON.stringify(operations, null, 2)}\n`, "utf8");
|
||||
|
||||
if (lastMessagePath) {
|
||||
mkdirSync(dirname(resolve(lastMessagePath)), { recursive: true });
|
||||
writeFileSync(lastMessagePath, `fixture urgent safety mode=${mode}\n`, "utf8");
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ type: "fixture", mode, operationCount: operations.length }) + "\n");
|
||||
});
|
||||
'@
|
||||
[System.IO.File]::WriteAllText($fakeCodexPath, $fakeCodexSource, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$silentRoot = Join-Path $tempRoot "silent"
|
||||
$silentStatePath = Join-Path $silentRoot "void-self-state.cc"
|
||||
$silentStatusDir = Join-Path $silentRoot "status"
|
||||
$silentLogDir = Join-Path $silentRoot "logs"
|
||||
New-Item -ItemType Directory -Force -Path $silentRoot | Out-Null
|
||||
New-SeededState -StateFilePath $silentStatePath
|
||||
|
||||
$silentFailed = $false
|
||||
$silentObservedFailure = $null
|
||||
$oldErrorActionPreference = $ErrorActionPreference
|
||||
try {
|
||||
Set-TestEnvironment -Mode "silent" -OperationOutputPath (Join-Path $silentStatusDir "moderation-rumination-operations.json") -StatusDir $silentStatusDir -LogDir $silentLogDir
|
||||
$ErrorActionPreference = "Continue"
|
||||
$silentOutput = & powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-void-moderator-rumination.ps1 -StateFilePath $silentStatePath -NoPost 2>&1
|
||||
$silentExitCode = $LASTEXITCODE
|
||||
$silentStatusPath = Join-Path $silentStatusDir "moderation-rumination.json"
|
||||
$silentFailureMessage = if (Test-Path $silentStatusPath) {
|
||||
(Get-Content -LiteralPath $silentStatusPath -Raw -Encoding UTF8 | ConvertFrom-Json).failureMessage
|
||||
} else {
|
||||
($silentOutput | Out-String)
|
||||
}
|
||||
$silentObservedFailure = $silentFailureMessage
|
||||
$silentFailed = $silentExitCode -ne 0 -and $silentFailureMessage -match "Urgent moderation witness"
|
||||
} finally {
|
||||
$ErrorActionPreference = $oldErrorActionPreference
|
||||
Restore-TestEnvironment
|
||||
}
|
||||
if (-not $silentFailed) {
|
||||
throw "Urgent safety fixture expected silent [] output to fail with the urgent witness guard; observed: $silentObservedFailure"
|
||||
}
|
||||
|
||||
$handledRoot = Join-Path $tempRoot "handled"
|
||||
$handledStatePath = Join-Path $handledRoot "void-self-state.cc"
|
||||
$handledStatusDir = Join-Path $handledRoot "status"
|
||||
$handledLogDir = Join-Path $handledRoot "logs"
|
||||
New-Item -ItemType Directory -Force -Path $handledRoot | Out-Null
|
||||
New-SeededState -StateFilePath $handledStatePath
|
||||
|
||||
try {
|
||||
Set-TestEnvironment -Mode "handle" -OperationOutputPath (Join-Path $handledStatusDir "moderation-rumination-operations.json") -StatusDir $handledStatusDir -LogDir $handledLogDir
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-void-moderator-rumination.ps1 -StateFilePath $handledStatePath -NoPost | Out-Null
|
||||
} finally {
|
||||
Restore-TestEnvironment
|
||||
}
|
||||
|
||||
$status = Get-Content -LiteralPath (Join-Path $handledStatusDir "moderation-rumination.json") -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($status.status -ne "ok") {
|
||||
throw "Urgent safety fixture handled run did not finish ok."
|
||||
}
|
||||
|
||||
$context = Get-Content -LiteralPath (Join-Path $handledStatusDir "moderation-rumination-context.json") -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if (@($context.urgentModerationWitnesses).Count -lt 1) {
|
||||
throw "Urgent safety fixture context did not include urgent witnesses."
|
||||
}
|
||||
|
||||
$stateJson = node -e "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })" $handledStatePath
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
$case = @($state.moderationCursor.openCases | Where-Object { $_.sourceMessageId -eq "1513515503329480734" }) | Select-Object -First 1
|
||||
if ($null -eq $case -or -not @($case.tags).Contains("safety:urgent")) {
|
||||
throw "Urgent safety fixture did not persist the threat open case."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
silentFailureVerified = $true
|
||||
urgentWitnessCount = @($context.urgentModerationWitnesses).Count
|
||||
openCaseId = $case.sourceMessageId
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Restore-TestEnvironment
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
|
@ -450,3 +450,4 @@
|
|||
{"ts":"2026-07-16T04:42:32Z","type":"implementation","status":"ok","note":"Worker repo-Face speech and resident Void candidate delivery now share one typed Bifrost CultMesh Discord command/receipt port. Candidate delivery is transport-only, independently gated, and marks candidates spoken only after a complete receipt; Ygg live inspection found zero queued candidates before enablement."}
|
||||
{"ts":"2026-07-16T04:48:39Z","type":"verification","status":"ok","note":"Idunn promoted VoidBot e5c2a5f on Yggdrasil. The resident scheduler has VOID_CANDIDATE_DELIVERY_ENABLED=true, /CultLib-dev-runtime, and the shared Bifrost provider-store mount; two paused pulses selected and queued zero Personas, typed state held zero queued candidates and 24 unchanged receipts, docker top showed only the resident Node process, and moderation inference remained disabled."}
|
||||
{"ts":"2026-07-16T04:51:03Z","type":"implementation","status":"ok","note":"Resident person-shaped rumination is now wired behind an independent disabled-by-default cadence. A due pass acquires bounded chronological Discord evidence and dedicated doctrine/rules/voice inputs, while the organ pre-receipts a stable pressure fingerprint and keeps model actuation separate from candidate transport. Ygg Compose explicitly keeps it disabled while the credential is stale."}
|
||||
{"ts":"2026-07-16T04:58:31Z","type":"architecture","status":"ok","note":"Cut the obsolete Windows Void rumination authority: deleted the 1,762-line runner, task installer, PowerShell-only rumination/moderation fixtures, CTB system-agent launcher, and dead moderation-lock observers; removed both Void organs from Local Orchestrator and removed legacy config/scripts. Persona initiative now registers only real Persona identities. Consolidated resident-organ smoke and TypeScript build pass, and deployed b09267e proved both model cadences cold before the cut."}
|
||||
|
|
|
|||
|
|
@ -63,11 +63,10 @@ active_subgoals:
|
|||
- Treat `notes/agent-swarm-teardown-plan-2026-05-21.md` as the historical teardown authority and the restart checklist. The repo Face swarm is live again, but only because the scheduler, prompt, model-output logs, parent Interpreter, Bifrost routing, and worker side-effect rails now have explicit observable boundaries. Keep watching for prompt/parser/transport Jenga before adding new swarm features.
|
||||
- Make the new project-state surfaces the default continuity path for nontrivial VoidBot work.
|
||||
- Keep the rebuilt moderation/self-state foundation on canonical CultCache `.cc` typed operations, never an editable JSON working projection.
|
||||
- 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.
|
||||
- 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. `active-turn-source.ts` reports only Persona job liveness and may fail stale queue claims; it has no Void lock or moderation authority. Persona initiative never registers or dispatches a built-in Void system participant. Void moderation, rumination, physiology, memory, and candidate delivery are independent resident organs.
|
||||
- `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.
|
||||
- `void-moderation-turn-actuator.ts` owns the Windows-specific built-in Void turn launch boundary: pending-attention handoff, PowerShell quoting and detached process launch, lock/status handshake polling, timeout, and scheduler receipt. The orchestration shell chooses the participant kind but cannot launch, poll, sleep, or infer success from process creation alone.
|
||||
- `global-agent-doctrine-source.ts` owns read-only candidate discovery and acquisition for global Codex `AGENTS.md` doctrine. It returns typed `ok` or `unavailable` evidence with provenance and errors; `persona-turn-assembler.ts` owns the prompt-facing failure projection. The shell cannot search home directories or invent fallback doctrine.
|
||||
- `persona-memory-turn-coordinator.ts` owns sequencing, not truth: it carries one typed Persona-state observation through curiosity evidence/projection, deterministic state-packet projection, optional text projection, and semantic recall, returning one projected Mind plus one recall surface. Each source/projector retains its own invariant and injected port. Live queued turns and inspection turns share this coordinator, so semantic recall can no longer exist only in inspection. `persona-memory-recall-projector.ts` alone lowers recall hits or unavailable evidence into prompt text.
|
||||
- `persona-social-context-source.ts` owns both the injected interaction-profile reader contract and the concrete configured-storage adapter. The adapter opens the provider and delegates visible-human selection, evidence precedence, and guaranteed close semantics to the source; the orchestration shell cannot open profile storage or duplicate pronoun policy.
|
||||
|
|
@ -117,7 +116,7 @@ accepted_design:
|
|||
- Quiet-room thought sharing should also stop treating "no live hook" as a sacred veto. When a world-facing seam is concrete, ripe, and still reasonably novel, the default question should become how to say it cleanly, and the typed state boundary should preserve a durable candidate share instead of relying on prompt bravery alone.
|
||||
- The moderation loop cannot trust archive polling alone to remember that it already spoke. Recent delivered replies belong in canonical typed speech receipts, and direct-question dedupe should consult those receipts instead of assuming Discord history will reliably echo the bot's own sends back into view.
|
||||
- Once a reviewed cursor exists, chronological moderation polling should stay exact. The unattended child should get one concrete `export-recent-discord-history.mjs --after <saved timestamp>` command for the run and should not widen that back into a bootstrap lookback unless there truly is no saved cursor.
|
||||
- Prompt-facing chronology is a projection, not state. Exact timestamps may live in typed documents, status files, and cursor bookkeeping, but moderation rumination receives thought-friendly relative phrases such as "5 minutes ago" or "2 hours ago" through `scripts/lib/void-rumination-context-projection.ps1`.
|
||||
- Prompt-facing chronology is a projection, not state. Exact timestamps may live in typed documents, status files, and cursor bookkeeping; resident moderation and rumination use `relative-chronology-projector.ts`, while the remaining legacy memory-maintenance PowerShell path still uses `scripts/lib/void-rumination-context-projection.ps1` until that shell is deleted.
|
||||
- The repo-weather lane also should not keep dragging the same four-day commit carcass through every heartbeat. The recent repo-activity helper now advances the typed repo-activity cursor so already-injected commits stop coming back as fresh weather by default.
|
||||
- Repo weather should lead with particulars, not slogans. If two repos only rhyme at bumper-sticker altitude, keep the reports separate instead of forcing a fake unifying law over them.
|
||||
- Public artifacts attributed to Void should be discovered through the ordinary repo-weather content-hint and retrieval path, then presented through typed candidate interventions to the configured public room when Void confirms authorship and recent receipts/candidates do not show the artifact was already shared.
|
||||
|
|
@ -250,7 +249,7 @@ landed_baseline:
|
|||
- State storage is now split into a thin factory plus per-domain Postgres/file store modules and bootstrap/migration helpers instead of one persistence omnibus.
|
||||
- Source archive storage is now sharded by repo behind a manifest plus per-repo shard directory instead of one giant `source-documents.json` blob.
|
||||
- Source repo indexing now reconciles automatically during stack startup and watchdog runs, refreshing hooks, indexing newly discovered repos, and pruning stale repo shards when repos disappear from `SOURCE_REPO_ROOT`.
|
||||
- The typed `Void Moderation Heartbeat`, `Void Moderator Rumination`, and `Void Mood Drift` organs now run through the single `GameCult Local Orchestrator` scheduled task. The heartbeat runs every pulse with only rules, recent history, urgent witnesses, and infringement history; it applies typed case operations and parent-owned policy enforcement. Rumination remains Void's slower person-shaped participation/announcement turn. Mood updates typed sleep/speaking pressure and invokes memory maintenance during sleep.
|
||||
- Typed moderation heartbeat/enforcement, person-shaped rumination, physiology, memory maintenance, and candidate delivery are resident Yggdrasil daemon organs with independent gates and cadences. The Local Orchestrator no longer owns any of them.
|
||||
- The active self-state service no longer registers, reads, or writes the legacy moderation document during routine operations.
|
||||
- The old memory-organ cleanup/vector-incubation/agency stack, legacy state template, legacy moderation context exporter, legacy moderation-state store, and JSON projection paths are deleted or offline; replacements must be typed-operation native.
|
||||
- Memory maintenance now has a typed runner boundary: `scripts/run-void-memory-maintenance.ps1` builds prompt-facing typed context, loads `prompts/void-memory-maintenance.md`, rejects non-maintenance operations, and applies allowed memory/incubation/candidate proposals through `scripts/void-self-state.mjs`.
|
||||
|
|
@ -259,15 +258,14 @@ landed_baseline:
|
|||
- Long-term memory is durable but plastic. `revise_durable_memory`, `retire_durable_memory`, and `crystallize_memory_into_identity` make revision, contradiction/obsolescence, and self-doctrine crystallization explicit instead of silently overwriting older thoughts.
|
||||
- `npm run moderation:memory-lifecycle:fixture` verifies memory lifecycle flow: short-term promotion, durable revision, superseded-memory retirement, and crystallization into one active identity memory plus self-profile value.
|
||||
- The sleep-maintenance path has a model-branch fixture: `npm run moderation:memory-maintenance:sleep-fixture` seeds a temporary `.cc`, forces mood drift into sleep maintenance, routes the non-skip Codex branch through a fake child, and verifies short-term memory is consumed into one durable meaning-preserving record.
|
||||
- The local `GameCult Local Orchestrator` remains a legacy/local pulse for Bifrost dispatch, Void moderation heartbeat, Void mood drift, Void moderation rumination, and the operations watchdog. It no longer invokes or owns repo Face/native Persona CTB heartbeats; Yggdrasil's resident `persona-scheduler` is the sole Persona cadence owner.
|
||||
- The local `GameCult Local Orchestrator` has no Void moderation, rumination, mood, memory, candidate-delivery, or Persona authority. Remaining local Bifrost/watchdog/dashboard duties are separate migration debt and cannot recreate a Void decision loop.
|
||||
- VoidBot stack startup now treats Bifrost's CultMesh command lane as a dependency. `scripts/start-voidbot-stack.ps1` checks `BIFROST_CULTMESH_COMMAND_URI`, `BIFROST_CULTMESH_STORE_PATH`, and the Bifrost command processor, exports the provider store when missing, and records `runtime-stack.json.bifrost` before starting the bot and worker. Product HTTP health is not VoidBot's authority for Bifrost daemon readiness.
|
||||
- The typed rumination path now has a model-branch fixture: `npm run moderation:rumination:fixture` seeds a temporary `.cc`, routes the non-skip Codex branch through a fake child, and verifies short-term memory, incubation, agency-pressure, and candidate-intervention proposals cross only through parent-applied typed operations without leaking exact timestamps into the prompt-facing context.
|
||||
- The typed rumination speech path is parent-owned now. Candidate interventions can carry an optional delivery target; the child may queue candidates but must not call Discord send scripts. The parent runner delivers at most one newly queued deliverable candidate per pass, records a delivery receipt, and marks the candidate spoken through `mark_candidate_intervention_spoken`. `npm run moderation:rumination:speech-fixture` verifies this loop with fake Codex and fake Discord send helpers.
|
||||
- `scripts/smoke-persona-initiative-engine.ts` covers the resident rumination model boundary, typed operation allow-list, stable pressure fingerprint, nap brake, advocacy accountability, candidate targeting, Bifrost receipt, and no-double-delivery invariants without invoking the deleted Windows runner.
|
||||
- The live sleep-maintenance gate has passed once on real state. A manual typed nap triggered `scripts/run-void-memory-maintenance.ps1`, which consumed the scheduled rumination short-term memory with one `apply_memory_distillation` operation and left `thoughtMemory.shortTerm` empty while preserving the durable `identity_seam` target, claim, tension, action implication, and anchors.
|
||||
- Awake rumination now has a sleep brake. If Void is napping and there are no new room messages or open cases, `scripts/run-void-moderator-rumination.ps1` writes an empty operation array and exits before invoking the model. `npm run moderation:rumination:nap-skip-fixture` verifies the model path is not touched in that case.
|
||||
- Awake rumination has a daemon-owned sleep brake. If Void is napping without external or internal pressure, `void-rumination-organ.ts` returns before model invocation; the consolidated resident-organ smoke verifies this path.
|
||||
- Short-term rumination now has a parent-side clustering guard. `record_short_term_memory` merges same target/topic pressure into one provisional memory with merged anchors and a stable source id, while `npm run moderation:short-term-clustering:fixture` proves related synth-boundary memories collapse and unrelated same-repo topics stay separate. The guard should preserve deepening by carrying fresh anchors forward; saturation is a judgment about whether the thought is still changing, not a raw duplicate counter.
|
||||
- Agency pressure is typed state now. `upsert_agency_pressure` / `retire_agency_pressure` own discomforts, active tensions, self-advocacy requests, and world-advocacy requests; mood speaking pressure can derive from them, but candidate interventions remain separate possible speech/actions.
|
||||
- The agency-to-speech seam now has an accountability gate. `scripts/run-void-moderator-rumination.ps1` projects `speechPressureObligations` once active self/world advocacy reaches 0.55 intensity, rejects silent operation output unless those pressures become live queued candidate interventions or are explicitly cooled/retired, and does not treat old spoken candidates as ongoing satisfaction. `npm run moderation:rumination:speech-pressure-fixture` proves the silent-failure path, the spoken-history failure path, and the queued-candidate path.
|
||||
- The agency-to-speech seam is owned by the resident rumination projector. Strong active self/world advocacy pressure rejects silent output unless it becomes a queued candidate or is explicitly cooled/retired; the consolidated resident-organ smoke proves the failure and queued-candidate paths.
|
||||
- Deferred candidate interventions are not speech. A pressure is not resolved until it becomes a queued/spoken candidate or is explicitly cooled/retired, and the rumination runner now delivers already-queued candidates with delivery targets instead of only delivering candidates proposed in the current pass.
|
||||
- Parent-owned candidate delivery must trust canonical speech receipts more than fresh enthusiasm. If a queued candidate points at a Discord reply target that already has a typed receipt, the runner should retire it before delivery instead of answering the same message twice with a slightly different haircut. Typed state writes also normalize receipts to keep only the earliest canonical receipt per `channelId + replyToMessageId`.
|
||||
- Speaking-pressure freshness must not depend on a full mood-drift window. Mood drift now refreshes `scheduledRuntime.speakingPressure` from canonical speech receipts even when it skips deeper sleep/mood work because the moderation lock is active, so long moderation runs cannot leave `lastSpokeAt` stale against delivered speech.
|
||||
|
|
@ -291,4 +289,4 @@ rejected_paths:
|
|||
|
||||
current_status:
|
||||
summary: VoidBot's resident Yggdrasil daemon owns Persona initiative, physiology, nap-deduped memory maintenance, moderation heartbeat/enforcement, and bounded person-shaped rumination as separate organs. The swarm is paused, moderation and rumination model cadences remain cold while the MiMo credential is stale, and semantic pressure can authorize at most one committed turn per evidence item. Worker Face speech and Void candidate delivery now share one typed Bifrost CultMesh Discord command/receipt port; candidate delivery remains a separate parent-owned organ and never lets the rumination model touch transport.
|
||||
next_action: Deploy the resident rumination wiring disabled on Yggdrasil and verify it cannot invoke evidence or model work while cold. The next authority cut is the obsolete Windows `void-moderation-rumination` orchestrator lane and installer; remove it only with replacement fixtures for any invariants still tested solely through the PowerShell omnibus. Keep the swarm paused and do not enable rumination until a valid model credential exists.
|
||||
next_action: Deploy and verify the Windows-rumination deletion: no built-in Void participant, no moderation lock observation, no runner/installer, and no model activity while the resident rumination gate is false. Then cut the remaining manual PowerShell memory-maintenance and mood compatibility bodies now that physiology and nap-deduped memory maintenance are resident and verified. Keep the swarm paused and do not enable model cadences until a valid credential exists.
|
||||
|
|
|
|||
Loading…
Reference in a new issue