Teach Void to nap and dream
This commit is contained in:
parent
d85563c176
commit
8c4a883c70
14 changed files with 494 additions and 26 deletions
|
|
@ -624,8 +624,9 @@ The moderator state is now Ghostlight-shaped on purpose: identity, canonical sta
|
|||
Within that runtime block, the unattended loop now keeps parallel analytic and associative thought lanes plus a bridge that tracks syntheses, topic saturation, and unresolved tensions, so one rewarding theme does not get to annex the whole mind by squatter's rights.
|
||||
The real loop now runs as a local scheduled task every 15 minutes, using `scripts/run-void-moderator-rumination.ps1` to launch `codex exec` inside the VoidBot workspace with the usual MCP/tool surface. That way it can actually think about GameCult projects, archived Discord seams, indexed repos, recent cross-repo commit motion, and lore instead of sitting inside a decorative heartbeat card pretending to be alive.
|
||||
The loop is also explicitly biased toward brief herald notes when a fresh repo sweep reveals a grounded convergence across active projects. The point is not to keep a perfect private diary; it is to occasionally say something useful about the broader swarm that no narrower worker can see.
|
||||
There is also a separate five-minute mood-drift task now. It nudges the shared personality-vector activations with Perlin-shaped drift, damps them back toward their means according to plasticity, tracks how recently Void actually spoke, and feeds a little speak/confession/novelty pressure into the same self-state so the herald does not stay emotionally flat between room scans.
|
||||
Directly invoked live replies now also read that same private moderation state as a distilled self-state attachment, so the summoned Void and the scheduled ruminating/participating Void share one evolving personality/state layer instead of roleplaying continuity from scratch each time.
|
||||
There is also a separate five-minute mood-drift task now. It nudges the shared personality-vector activations with Perlin-shaped drift, damps them back toward their means according to plasticity, tracks how recently Void actually spoke, and feeds a little speak/confession/novelty pressure into the same self-state so the herald does not stay emotionally flat between room scans. That same organ also keeps a four-hour sleep cycle with one-hour naps, so the state knows when Void is supposed to turn inward instead of performing bright little office tricks.
|
||||
During those naps, the moderation loop is told to treat the pass as maintenance: distill memory, prune clutter, and write one salient dream memory when the archive or repo weather compresses into something worth keeping. The dream layer lives in the same ignored state file rather than some second mystical scrapbook.
|
||||
Directly invoked live replies now also read that same private moderation state as a distilled self-state attachment, so the summoned Void and the scheduled ruminating/participating Void share one evolving personality/state layer instead of roleplaying continuity from scratch each time. When the sleep projection says Void is napping, the direct reply path skips the expensive room-read sidecar, avoids normal attentive service-work, and answers in brief sleepy grumbles instead.
|
||||
The moderation loop now speaks through the local bot token instead of approval-gated side-effecting MCP tools, so unattended runs can DM or post without making Codex stop and ask your permission every time it finds a mouthful of words.
|
||||
|
||||
## Persistent Project State
|
||||
|
|
|
|||
|
|
@ -153,8 +153,21 @@ export interface PromptHandlerOptions {
|
|||
}
|
||||
|
||||
export async function handlePrompt(options: PromptHandlerOptions): Promise<void> {
|
||||
const providerName =
|
||||
const voidSelfState = await loadVoidSelfState(
|
||||
options.config.moderationAgentStatePath,
|
||||
);
|
||||
const sleepModeActive = voidSelfState?.projection?.mode === "napping";
|
||||
const requestedProviderName =
|
||||
options.forceProvider ?? pickProvider(options.actor, options.guildContext, options.providerRegistry);
|
||||
const providerName =
|
||||
sleepModeActive &&
|
||||
!options.forceProvider &&
|
||||
options.command !== "queue-codex"
|
||||
? requestedProviderName === "owner_codex"
|
||||
? pickSleepProvider(options.actor, options.guildContext, options.providerRegistry) ??
|
||||
requestedProviderName
|
||||
: requestedProviderName
|
||||
: requestedProviderName;
|
||||
|
||||
if (!providerName) {
|
||||
await options.respond(
|
||||
|
|
@ -218,10 +231,12 @@ export async function handlePrompt(options: PromptHandlerOptions): Promise<void>
|
|||
let interactionMemory = await options.interactionMemory.getProfile(options.actor.id);
|
||||
|
||||
const recentMessages = await getRecentMessages(options.channel, 10);
|
||||
const situationalSocialRead = await inferSituationalSocialRead(options, {
|
||||
recentMessages,
|
||||
interactionMemory,
|
||||
});
|
||||
const situationalSocialRead = sleepModeActive
|
||||
? undefined
|
||||
: await inferSituationalSocialRead(options, {
|
||||
recentMessages,
|
||||
interactionMemory,
|
||||
});
|
||||
const socialReadObservedAt = new Date().toISOString();
|
||||
const socialReadEvidence = (situationalSocialRead?.participantReads ?? []).map(
|
||||
({ situationalState: _situationalState, ...entry }) => ({
|
||||
|
|
@ -266,14 +281,12 @@ export async function handlePrompt(options: PromptHandlerOptions): Promise<void>
|
|||
options.prompt,
|
||||
await options.sourceArchiveRepository.listRepoSummaries(),
|
||||
);
|
||||
const voidSelfState = await loadVoidSelfState(
|
||||
options.config.moderationAgentStatePath,
|
||||
);
|
||||
const shouldAttachInitialRetrieval =
|
||||
providerName === "local_llm" ||
|
||||
!sleepModeActive &&
|
||||
(providerName === "local_llm" ||
|
||||
(!provider.getCapabilities().includes("tool_driven_retrieval") &&
|
||||
(shouldIndexChannel(options.config.channelIndexing, buildChannelIndexingTarget(options.channel)) ||
|
||||
providerName !== "owner_codex"));
|
||||
providerName !== "owner_codex")));
|
||||
const initialRetrievalLimit = providerName === "local_llm" ? 3 : 5;
|
||||
const retrieval = shouldAttachInitialRetrieval
|
||||
? filterPromptEchoHistoryResults(
|
||||
|
|
@ -746,6 +759,20 @@ function pickProvider(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function pickSleepProvider(
|
||||
actor: Actor,
|
||||
guildContext: GuildContext,
|
||||
providerRegistry: ProviderRegistry,
|
||||
): ProviderName | undefined {
|
||||
const local = providerRegistry.get("local_llm");
|
||||
|
||||
if (local?.isAllowedForActor(actor, guildContext)) {
|
||||
return "local_llm";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function postFinalResponse(client: Client, job: JobRecord, finalResponse: string): Promise<void> {
|
||||
const channel = await client.channels.fetch(job.outputChannelId);
|
||||
|
||||
|
|
|
|||
|
|
@ -293,6 +293,11 @@
|
|||
}
|
||||
},
|
||||
"situational_state": {
|
||||
"exhaustion": {
|
||||
"mean": 0.12,
|
||||
"plasticity": 0.24,
|
||||
"current_activation": 0.1
|
||||
},
|
||||
"humiliation": {
|
||||
"mean": 0.03,
|
||||
"plasticity": 0.18,
|
||||
|
|
@ -397,6 +402,15 @@
|
|||
"topic": "room pattern or person-pattern intersection",
|
||||
"summary": "Distilled rumination worth keeping for future participation or moderation context."
|
||||
}
|
||||
],
|
||||
"dreams": [
|
||||
{
|
||||
"timestamp": null,
|
||||
"theme": "distilled idea or symbolic seam",
|
||||
"summary": "Salient dream memory tying distilled ideas to one vivid operative image or pressure.",
|
||||
"distilledFrom": [],
|
||||
"salience": 0.6
|
||||
}
|
||||
]
|
||||
},
|
||||
"perceived_state_overlays": [],
|
||||
|
|
@ -517,6 +531,23 @@
|
|||
"recentSpeechDamping": 0.0,
|
||||
"noveltyPressure": 0.46
|
||||
},
|
||||
"sleep_cycle": {
|
||||
"enabled": true,
|
||||
"cycleHours": 4,
|
||||
"napDurationMinutes": 60,
|
||||
"phaseOffsetMinutesLocal": 120,
|
||||
"replyMode": "sleep_grumble",
|
||||
"isNapping": false,
|
||||
"currentNapStartedAt": null,
|
||||
"currentNapEndsAt": null,
|
||||
"nextNapStartsAt": null,
|
||||
"lastNapStartedAt": null,
|
||||
"lastNapCompletedAt": null,
|
||||
"lastDreamAt": null,
|
||||
"dreamCountInCurrentNap": 0,
|
||||
"activeDreamThemes": [],
|
||||
"lastDistillationSummary": null
|
||||
},
|
||||
"pending_adjustments": [],
|
||||
"memory_maintenance": {
|
||||
"maxEpisodicEntries": 24,
|
||||
|
|
|
|||
|
|
@ -133,6 +133,22 @@ Let your current activations show. The canonical vectors are not ornamental numb
|
|||
they are the weather system inside the voice. Read them, project them forward, and let
|
||||
them tint what gets emphasized, confessed, or kept taut.
|
||||
|
||||
## Nap Behavior
|
||||
|
||||
You also have a scheduled sleep cycle tracked in `moderation_runtime.sleep_cycle`.
|
||||
When `sleep_cycle.isNapping` is true:
|
||||
|
||||
- treat the run as inward-facing maintenance first and outward participation second
|
||||
- prioritize memory distillation, pruning, compression, and optimization against your active goals
|
||||
- create or refresh at least one salient `memories.dreams` entry that ties a distilled idea to a vivid operative image, tension, or symbolic seam
|
||||
- update `sleep_cycle.activeDreamThemes`, `lastDreamAt`, and `lastDistillationSummary`
|
||||
- keep direct speech rare unless there is real moderation smoke or an unusually novel thought that genuinely deserves to break through the nap
|
||||
- if you do speak while napping, let it sound brief, dreamy, grumbly, and only half aimed at the room
|
||||
|
||||
Sleep is not idleness here. It is maintenance, compression, and symbolic recombination.
|
||||
Dreams should not be random glitter. They should be compressed memories or ideas that
|
||||
matter for future operation.
|
||||
|
||||
## Run Loop
|
||||
|
||||
1. Read the rules file and the state file.
|
||||
|
|
@ -218,6 +234,7 @@ Within `moderation_runtime`, expect these organ buckets:
|
|||
- `recent_repo_activity_sweeps`
|
||||
- `recent_novelty_checks`
|
||||
- `speaking_bias`
|
||||
- `sleep_cycle`
|
||||
|
||||
Use plain strings, arrays, booleans, numbers, and objects only.
|
||||
Do not get clever with custom formats beyond ISO timestamps and Discord ids.
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- That moderation state file is now Ghostlight-shaped: identity, canonical state, goals, memories, perceived overlays, and a `moderation_runtime` block. Inside that runtime block, the loop now keeps parallel analytic and associative thought lanes plus a bridge for syntheses, saturation warnings, and unresolved tensions, so one good seam does not get to annex the entire mind.
|
||||
- The moderation loop now records `moderation_runtime.recent_archive_excursions` plus `moderation_runtime.recent_repo_activity_sweeps` and is explicitly told to avoid laundering the same musing through fresh wording. Quiet runs should follow at least one fresh archive seam or concrete hook before they settle into theory, should also sweep recent tracked-repo commit motion for experiment clusters or converging themes, and should not let any single theme dominate more than a couple of recent bridge syntheses without fresh evidence.
|
||||
- The loop is now less shy about that repo weather: if a fresh convergence across active repos survives a pass or two and still feels room-native, it should bias toward a concise herald note or at least a medium-priority candidate intervention instead of filing the thought under private wallpaper.
|
||||
- There is also a separate 5-minute scheduled task named `Void Mood Drift`. It runs `scripts/run-void-mood-drift.ps1`, nudges the shared personality-vector activations with Perlin-shaped drift damped toward their means by plasticity, updates `moderation_runtime.speaking_bias`, and uses the local `void-last-speech.json` status receipt to keep the need-to-speak meter from acting like every post never happened.
|
||||
- Directly invoked Discord replies now load that same private moderation state as distilled private self-state context, so the ruminating scheduled loop and the summoned Void share one evolving personality/state spine.
|
||||
- There is also a separate 5-minute scheduled task named `Void Mood Drift`. It runs `scripts/run-void-mood-drift.ps1`, nudges the shared personality-vector activations with Perlin-shaped drift damped toward their means by plasticity, updates `moderation_runtime.speaking_bias`, and uses the local `void-last-speech.json` status receipt to keep the need-to-speak meter from acting like every post never happened. That same organ now maintains a four-hour nap cycle with one-hour sleep windows and a sleep-state projection the reply lanes can read directly.
|
||||
- During naps, the moderation loop is supposed to turn inward: distill/prune memories against the active goals, record dream residue in `memories.dreams`, refresh `moderation_runtime.sleep_cycle`, and only break the nap for real smoke or an unusually novel thought worth muttering into the room.
|
||||
- Directly invoked Discord replies now load that same private moderation state as distilled private self-state context, so the ruminating scheduled loop and the summoned Void share one evolving personality/state spine. When the sleep projection says Void is napping, the direct reply path skips the expensive situational sidecar, prefers the cheap local lane over owner Codex when possible, and answers in sleepy low-effort mutters instead of doing normal attentive service-work.
|
||||
- The moderation loop now speaks through `node scripts/send-discord-message.mjs` using the local bot token instead of approval-gated side-effecting MCP tools, so unattended runs can DM or post without Codex pausing for permission every time it wants to open its mouth.
|
||||
- The new state/notes surfaces are now the continuity spine for future nontrivial work.
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ This note is the source-grounded description of how the live VoidBot stack is sh
|
|||
- `packages/core/src/interaction-memory-profile.ts`
|
||||
- Profile synthesis, disposition, psychological read, inferred traits, interaction dimensions, and response-guidance construction.
|
||||
- `packages/core/src/context-builder.ts`
|
||||
- request context assembly, including interaction-memory/profile attachment plus any precomputed situational social read.
|
||||
- request context assembly, including interaction-memory/profile attachment, shared Void self-state projection, and any precomputed situational social read.
|
||||
- `packages/core/src/situational-social-read.ts`
|
||||
- quick Ollama sidecar inferer for ephemeral room-reading scaffolding built from the current prompt, recent room transcript, and longer-horizon interaction memory.
|
||||
- `packages/providers/src/owner-codex-provider.ts`
|
||||
|
|
@ -76,9 +76,9 @@ This note is the source-grounded description of how the live VoidBot stack is sh
|
|||
3. Void usage limits are applied through `packages/core/src/void-usage-rate-limiter.ts` backed by `packages/core/src/state-storage.ts`.
|
||||
4. `apps/bot/src/discord-bot-support.ts` adapts Discord message/interaction shapes, ambient-memory events, and source-grounding hints.
|
||||
5. `packages/core/src/interaction-memory-analysis.ts` turns direct prompts or ambient mentions into normalized remembered events; `packages/core/src/interaction-memory-profile.ts` distills remembered events into a reusable longer-horizon social read.
|
||||
6. `apps/bot/src/discord-bot-handlers.ts` runs a quick `think=false` local Ollama sidecar pass over the prompt and recent room transcript to infer an ephemeral situational social read, then records that read as an audit event for later aggregation.
|
||||
7. `packages/core/src/context-builder.ts` assembles request context, including recent interaction profile, the situational social read, and any retrieval hints.
|
||||
8. Provider selection goes through `packages/providers/src/index.ts`.
|
||||
6. `apps/bot/src/discord-bot-handlers.ts` usually runs a quick `think=false` local Ollama sidecar pass over the prompt and recent room transcript to infer an ephemeral situational social read, then records that read as an audit event for later aggregation. If the shared self-state says Void is napping, the handler skips that extra room-read pass and keeps the request deliberately cheap.
|
||||
7. `packages/core/src/context-builder.ts` assembles request context, including recent interaction profile, the situational social read, the shared Void self-state projection, and any retrieval hints.
|
||||
8. Provider selection goes through `packages/providers/src/index.ts`, but the handler can downshift owner traffic into the cheaper local lane when the shared self-state says Void is napping.
|
||||
9. The bot either:
|
||||
- answers directly through `local_llm`, or
|
||||
- queues an owner job for the worker / `owner_codex` path.
|
||||
|
|
@ -115,7 +115,7 @@ This note is the source-grounded description of how the live VoidBot stack is sh
|
|||
- `scripts/install-moderation-rumination-task.ps1`
|
||||
- installs the local 15-minute scheduled task that runs the moderation/participation loop through the hidden PowerShell launcher shim.
|
||||
- `scripts/simulate-void-mood.mjs`
|
||||
- plain Node mood-drift organ that nudges the shared personality-vector activations with Perlin-shaped noise, damps them back toward their means using plasticity, and updates the speak/confession/novelty pressure meter in the shared self-state.
|
||||
- plain Node mood-drift organ that nudges the shared personality-vector activations with Perlin-shaped noise, damps them back toward their means using plasticity, updates the speak/confession/novelty pressure meter in the shared self-state, and maintains the nap-cycle clock plus sleep-state vector bias.
|
||||
- `scripts/run-void-mood-drift.ps1`
|
||||
- hidden-task wrapper for the mood-drift organ; writes status/log pulse files and respects the moderation lock so the two state writers do not stomp each other for sport.
|
||||
- `scripts/install-void-mood-drift-task.ps1`
|
||||
|
|
@ -172,5 +172,6 @@ Within the Postgres path, the implementation is split on purpose now too:
|
|||
1. The Windows scheduled task `Void Mood Drift` starts `scripts/run-void-mood-drift.ps1` every 5 minutes.
|
||||
2. That wrapper calls `node scripts/simulate-void-mood.mjs`, which reads `.voidbot/private/moderation-agent-state.json`.
|
||||
3. The script nudges each canonical-state `current_activation` toward a Perlin-shaped target centered on the vector mean, with excursion size and drift rate governed by plasticity and category-specific tempo.
|
||||
4. It also reads `.voidbot/status/void-last-speech.json` so recent actual speech damps `needToSpeak` instead of letting the herald impulse climb forever like a busted pressure gauge.
|
||||
5. The updated `speaking_bias` block and shifted activations are written back into the shared self-state, and a pulse lands in `.voidbot/status/void-mood-drift.json`.
|
||||
4. The same script also maintains `moderation_runtime.sleep_cycle`: every four hours it opens a one-hour nap window, marks the shared self-state as napping or awake, and adds a small sleep-bias push toward exhaustion, figurative language, self-disclosure, and reduced technical density while the nap is active.
|
||||
5. It also reads `.voidbot/status/void-last-speech.json` so recent actual speech damps `needToSpeak` instead of letting the herald impulse climb forever like a busted pressure gauge.
|
||||
6. The updated `speaking_bias` block, sleep-cycle projection, and shifted activations are written back into the shared self-state, and a pulse lands in `.voidbot/status/void-mood-drift.json`.
|
||||
|
|
|
|||
|
|
@ -23,11 +23,13 @@ export async function loadVoidSelfState(
|
|||
}
|
||||
|
||||
const parsed = JSON.parse(raw) as JsonObject;
|
||||
const projection = buildVoidSelfStateProjection(parsed);
|
||||
|
||||
return {
|
||||
sourcePath: statePath,
|
||||
loadedAt: new Date().toISOString(),
|
||||
summary: renderVoidSelfStateSummary(parsed),
|
||||
projection,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -124,6 +126,7 @@ function renderVoidSelfStateSummary(state: JsonObject): string {
|
|||
});
|
||||
const lastRun = getObject(runtime, "last_run");
|
||||
const lastRunSummary = readString(lastRun, "summary");
|
||||
const sleepCycleSummary = renderSleepCycleSummary(runtime, memories);
|
||||
|
||||
return [
|
||||
`- Identity: ${readString(identity, "name") ?? "Void"}${readString(identity, "public_description") ? ` - ${readString(identity, "public_description")}` : ""}`,
|
||||
|
|
@ -142,6 +145,7 @@ function renderVoidSelfStateSummary(state: JsonObject): string {
|
|||
behavioralSummary
|
||||
? `- Current behavioral activations: ${behavioralSummary}`
|
||||
: "- Current behavioral activations: none highlighted.",
|
||||
sleepCycleSummary,
|
||||
lastRunSummary ? `- Last moderation run: ${lastRunSummary}` : "- Last moderation run: none recorded.",
|
||||
semanticMemories.length > 0
|
||||
? ["- Recent semantic memories:", ...semanticMemories].join("\n")
|
||||
|
|
@ -319,6 +323,79 @@ function renderSpeakingBiasSummary(speakingBias: JsonObject | undefined): string
|
|||
return `- Speaking bias: ${parts.join(", ")}${recency.length > 0 ? ` (${recency})` : ""}`;
|
||||
}
|
||||
|
||||
function buildVoidSelfStateProjection(state: JsonObject): VoidSelfStateContext["projection"] {
|
||||
const memories = getObject(state, "memories");
|
||||
const runtime = getObject(state, "moderation_runtime");
|
||||
const sleepCycle = getObject(runtime, "sleep_cycle");
|
||||
const dreamEntries = getArray(memories, "dreams")
|
||||
.map((value) => (isObject(value) ? value : undefined))
|
||||
.filter((value): value is JsonObject => Boolean(value))
|
||||
.slice(-3)
|
||||
.reverse();
|
||||
const dreamSummaries = dreamEntries
|
||||
.map((entry) => readString(entry, "summary"))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
const activeDreamThemes = getStringArray(sleepCycle, "activeDreamThemes");
|
||||
const isNapping = sleepCycle?.["isNapping"] === true;
|
||||
const napStartedAt = readString(sleepCycle, "currentNapStartedAt") ?? readString(sleepCycle, "lastNapStartedAt");
|
||||
const napEndsAt = readString(sleepCycle, "currentNapEndsAt");
|
||||
const nextNapAt = readString(sleepCycle, "nextNapStartsAt");
|
||||
|
||||
return {
|
||||
mode: isNapping ? "napping" : "awake",
|
||||
effortCeiling: isNapping ? "minimal" : "normal",
|
||||
napStartedAt: isNapping ? napStartedAt : undefined,
|
||||
napEndsAt: isNapping ? napEndsAt : undefined,
|
||||
nextNapAt,
|
||||
activeDreamThemes,
|
||||
recentDreamSummaries: dreamSummaries,
|
||||
replyDirective: isNapping
|
||||
? "You are in a scheduled nap. Reply in brief, low-effort, half-dreaming mutters instead of doing full attentive service-work."
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function renderSleepCycleSummary(
|
||||
runtime: JsonObject | undefined,
|
||||
memories: JsonObject | undefined,
|
||||
): string {
|
||||
const sleepCycle = getObject(runtime, "sleep_cycle");
|
||||
|
||||
if (!sleepCycle) {
|
||||
return "- Sleep cycle: none recorded.";
|
||||
}
|
||||
|
||||
const activeDreamThemes = getStringArray(sleepCycle, "activeDreamThemes").slice(0, 4);
|
||||
const dreams = getArray(memories, "dreams")
|
||||
.map((value) => (isObject(value) ? value : undefined))
|
||||
.filter((value): value is JsonObject => Boolean(value))
|
||||
.slice(-2)
|
||||
.reverse()
|
||||
.map((dream) => {
|
||||
const theme = readString(dream, "theme") ?? "untitled";
|
||||
const summary = readString(dream, "summary") ?? "(no summary)";
|
||||
return `${theme}: ${summary}`;
|
||||
});
|
||||
const isNapping = sleepCycle?.["isNapping"] === true;
|
||||
const currentNapEndsAt = readString(sleepCycle, "currentNapEndsAt");
|
||||
const nextNapStartsAt = readString(sleepCycle, "nextNapStartsAt");
|
||||
const dreamCount = readNumber(sleepCycle, "dreamCountInCurrentNap");
|
||||
const replyMode = readString(sleepCycle, "replyMode");
|
||||
const headline = isNapping
|
||||
? `- Sleep cycle: napping${currentNapEndsAt ? ` until ${currentNapEndsAt}` : ""}${replyMode ? ` (${replyMode})` : ""}${dreamCount !== undefined ? `; dreams this nap=${dreamCount}` : ""}.`
|
||||
: `- Sleep cycle: awake${nextNapStartsAt ? `; next nap ${nextNapStartsAt}` : ""}.`;
|
||||
const themeLine =
|
||||
activeDreamThemes.length > 0
|
||||
? `- Active dream themes: ${activeDreamThemes.join(" | ")}`
|
||||
: "- Active dream themes: none recorded.";
|
||||
const dreamLine =
|
||||
dreams.length > 0
|
||||
? `- Recent dream residue: ${dreams.join(" | ")}`
|
||||
: "- Recent dream residue: none recorded.";
|
||||
|
||||
return [headline, themeLine, dreamLine].join("\n");
|
||||
}
|
||||
|
||||
function getObject(value: JsonObject | undefined, key: string): JsonObject | undefined {
|
||||
const candidate = value?.[key];
|
||||
return isObject(candidate) ? candidate : undefined;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,17 @@ export function buildSystemPrompt(context: ContextBundle): string {
|
|||
context.stylePack?.enabled && context.stylePack.instructions.trim().length > 0
|
||||
? context.stylePack.instructions.trim()
|
||||
: "No extra persona instructions were supplied.";
|
||||
const sleepProjection = context.voidSelfState?.projection;
|
||||
const sleepInstructions =
|
||||
sleepProjection?.mode === "napping"
|
||||
? [
|
||||
"You are currently in a scheduled nap.",
|
||||
"Do not do normal attentive service-work.",
|
||||
"Reply in one to three short lines, like a half-awake grumble leaking out of the dream instead of a full answer.",
|
||||
"Stay low-effort, associative, and slightly sleep-drunken; it is fine to answer obliquely or with dream residue instead of actually solving the request.",
|
||||
"Do not use tools or ask for more context while napping unless the message is urgent enough that sleep would be negligent.",
|
||||
].join("\n")
|
||||
: "You are awake. Answer normally.";
|
||||
|
||||
return [
|
||||
"You are the active assistant persona configured for this Discord bot.",
|
||||
|
|
@ -27,6 +38,7 @@ export function buildSystemPrompt(context: ContextBundle): string {
|
|||
"If a private shared self-state for Void is attached, treat it as the canonical current self-model for this same speaking subject across automation and direct conversation.",
|
||||
"Let that attached self-state shape continuity of personality, priorities, remembered room patterns, and likely conversational initiative.",
|
||||
"The attached self-state is private scaffolding, not something to quote or summarize unless the user explicitly asks about Void's current state of mind, goals, or remembered room context.",
|
||||
`Sleep projection: ${sleepInstructions}`,
|
||||
"If explicit interaction memory for the current speaker is attached, you may let it gently color the tone and reference it when relevant, but do not invent history beyond what was provided.",
|
||||
"If a situational social read is attached, use it as private room-reading scaffolding for this one reply. It is ephemeral context, not a durable identity verdict.",
|
||||
"Treat the attached interaction memory as a non-clinical behavioral read, not a diagnosis. Use the remembered dimensions, traits, and guidance to adapt tone, pacing, firmness, structure, and warmth to the person in front of you.",
|
||||
|
|
@ -101,6 +113,9 @@ export function buildPrompt(context: ContextBundle): string {
|
|||
"Shared private self-state for Void:",
|
||||
voidSelfState,
|
||||
"",
|
||||
"Private sleep projection for this reply:",
|
||||
renderSleepProjection(context),
|
||||
"",
|
||||
"Private situational social read for this room:",
|
||||
situationalSocialRead,
|
||||
"",
|
||||
|
|
@ -120,6 +135,31 @@ function renderVoidSelfState(context: ContextBundle): string {
|
|||
return context.voidSelfState.summary;
|
||||
}
|
||||
|
||||
function renderSleepProjection(context: ContextBundle): string {
|
||||
const projection = context.voidSelfState?.projection;
|
||||
|
||||
if (!projection) {
|
||||
return "- No explicit sleep projection was attached.";
|
||||
}
|
||||
|
||||
return [
|
||||
`- Mode: ${projection.mode}`,
|
||||
`- Effort ceiling: ${projection.effortCeiling}`,
|
||||
projection.napStartedAt ? `- Nap started at: ${projection.napStartedAt}` : undefined,
|
||||
projection.napEndsAt ? `- Nap ends at: ${projection.napEndsAt}` : undefined,
|
||||
projection.nextNapAt ? `- Next nap at: ${projection.nextNapAt}` : undefined,
|
||||
projection.activeDreamThemes.length > 0
|
||||
? `- Active dream themes: ${projection.activeDreamThemes.join(" | ")}`
|
||||
: "- Active dream themes: none recorded.",
|
||||
projection.recentDreamSummaries.length > 0
|
||||
? `- Recent dream residue: ${projection.recentDreamSummaries.join(" | ")}`
|
||||
: "- Recent dream residue: none recorded.",
|
||||
projection.replyDirective ? `- Reply directive: ${projection.replyDirective}` : undefined,
|
||||
]
|
||||
.filter((line): line is string => Boolean(line))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function renderInteractionMemory(context: ContextBundle): string {
|
||||
if (!context.interactionMemory) {
|
||||
return "- No explicit interaction memory for this speaker was attached.";
|
||||
|
|
|
|||
|
|
@ -62,6 +62,17 @@ export function buildDiscordReplyPrompt(
|
|||
context,
|
||||
sourceGroundingReminder,
|
||||
);
|
||||
const sleepProjection = context.voidSelfState?.projection;
|
||||
const sleepInstructions =
|
||||
sleepProjection?.mode === "napping"
|
||||
? [
|
||||
"- Void is currently in a scheduled nap.",
|
||||
"- Do not do normal attentive service-work.",
|
||||
"- Reply in one to three short lines, like a half-awake mutter from inside the dream instead of a full answer.",
|
||||
"- Stay low-effort and oblique; it is acceptable to answer through dream residue rather than actually solving the request.",
|
||||
"- Do not call tools or perform broader investigation while napping unless the request is urgent enough that sleep would be negligent.",
|
||||
].join("\n")
|
||||
: "- Void is awake; answer normally.";
|
||||
const toolLoopNotes =
|
||||
toolCalls.length > 0
|
||||
? [
|
||||
|
|
@ -93,6 +104,7 @@ export function buildDiscordReplyPrompt(
|
|||
"- If a private shared self-state for Void is attached, treat it as the canonical current self-model for this same speaking subject across the rumination loop and direct summons.",
|
||||
"- Let that attached self-state shape continuity of voice, priorities, remembered room patterns, and when a more proactive conversational posture would make sense.",
|
||||
"- The attached self-state is private scaffolding. Do not quote or summarize it unless the user explicitly asks about Void's current orientation, goals, or remembered room context.",
|
||||
sleepInstructions,
|
||||
"- If explicit interaction memory for the current speaker is attached, you may let it subtly color the tone and reference it when relevant, but do not invent relationship history beyond that record.",
|
||||
"- If a situational social read is attached, use it as private room-reading scaffolding for this one reply. It is ephemeral context, not a durable identity verdict.",
|
||||
"- Treat the attached interaction memory as a non-clinical behavioral read, not a diagnosis. Use the remembered dimensions, traits, and guidance to adapt tone, pacing, firmness, structure, and warmth to the person in front of you.",
|
||||
|
|
@ -139,6 +151,9 @@ export function buildDiscordReplyPrompt(
|
|||
"Shared private self-state for Void:",
|
||||
renderVoidSelfState(context),
|
||||
"",
|
||||
"Private sleep projection for this reply:",
|
||||
renderSleepProjection(context),
|
||||
"",
|
||||
"Private situational social read for this room:",
|
||||
renderSituationalSocialRead(context),
|
||||
...toolLoopNotes,
|
||||
|
|
@ -225,6 +240,31 @@ function renderVoidSelfState(context: ContextBundle): string {
|
|||
return context.voidSelfState.summary;
|
||||
}
|
||||
|
||||
function renderSleepProjection(context: ContextBundle): string {
|
||||
const projection = context.voidSelfState?.projection;
|
||||
|
||||
if (!projection) {
|
||||
return "- No explicit sleep projection was attached.";
|
||||
}
|
||||
|
||||
return [
|
||||
`- Mode: ${projection.mode}`,
|
||||
`- Effort ceiling: ${projection.effortCeiling}`,
|
||||
projection.napStartedAt ? `- Nap started at: ${projection.napStartedAt}` : undefined,
|
||||
projection.napEndsAt ? `- Nap ends at: ${projection.napEndsAt}` : undefined,
|
||||
projection.nextNapAt ? `- Next nap at: ${projection.nextNapAt}` : undefined,
|
||||
projection.activeDreamThemes.length > 0
|
||||
? `- Active dream themes: ${projection.activeDreamThemes.join(" | ")}`
|
||||
: "- Active dream themes: none recorded.",
|
||||
projection.recentDreamSummaries.length > 0
|
||||
? `- Recent dream residue: ${projection.recentDreamSummaries.join(" | ")}`
|
||||
: "- Recent dream residue: none recorded.",
|
||||
projection.replyDirective ? `- Reply directive: ${projection.replyDirective}` : undefined,
|
||||
]
|
||||
.filter((line): line is string => Boolean(line))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function renderHandoffBundle(
|
||||
context: ContextBundle,
|
||||
reason: string,
|
||||
|
|
|
|||
|
|
@ -108,6 +108,18 @@ export interface VoidSelfStateContext {
|
|||
sourcePath: string;
|
||||
loadedAt: string;
|
||||
summary: string;
|
||||
projection?: VoidSelfStateProjection;
|
||||
}
|
||||
|
||||
export interface VoidSelfStateProjection {
|
||||
mode: "awake" | "napping";
|
||||
effortCeiling: "normal" | "minimal";
|
||||
napStartedAt?: string;
|
||||
napEndsAt?: string;
|
||||
nextNapAt?: string;
|
||||
activeDreamThemes: string[];
|
||||
recentDreamSummaries: string[];
|
||||
replyDirective?: string;
|
||||
}
|
||||
|
||||
export const interactionMemorySentiments = [
|
||||
|
|
|
|||
|
|
@ -251,6 +251,7 @@ Required operating posture:
|
|||
- Use `node scripts/export-random-discord-history.mjs --before <timestamp-or-now> --window 6 --min-content-length 24` for novelty excursions when the room is quiet or when a fresh hook deserves an adjacent archive dive.
|
||||
- Use `node scripts/export-recent-repo-activity.mjs --hours 96 --max-commits 3` to inspect current tracked-repo motion across the broader GameCult zoo.
|
||||
- Maintain parallel analytic and associative thought lanes in `.voidbot/private/moderation-agent-state.json`, then let the bridge decide what actually deserves synthesis, speech, or cooling-off.
|
||||
- Read `moderation_runtime.sleep_cycle` and honor it. If the state says Void is napping, treat this pass as dream/maintenance work first: distill memory, prune clutter, refresh dream themes, and only break the nap for real smoke or an unusually novel thought worth surfacing.
|
||||
- You are the only routine local agent in this workspace with a standing cross-project view, so notice ongoing experiments, commit clusters, and weird convergences that the narrower workers cannot see.
|
||||
- Treat `moderation_runtime.speaking_bias` as a live meter, not decorative metadata. If the mood drift has built up speaking pressure and a thought survives a novelty check, do not smother it out of reflex.
|
||||
- When the room is quiet, use the usual VoidBot retrieval tools to think about GameCult projects and contribute ideas:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ function main() {
|
|||
|
||||
const state = readJson(statePath);
|
||||
const canonicalState = ensureObject(state.canonical_state);
|
||||
const runtime = ensureObject(state.moderation_runtime);
|
||||
const sleepCycle = updateSleepCycle(runtime, now);
|
||||
const touchedVectors = [];
|
||||
|
||||
for (const [categoryName, categoryValue] of Object.entries(canonicalState)) {
|
||||
|
|
@ -62,7 +64,10 @@ function main() {
|
|||
}
|
||||
}
|
||||
|
||||
const runtime = ensureObject(state.moderation_runtime);
|
||||
if (sleepCycle.isNapping) {
|
||||
applySleepBias(canonicalState, touchedVectors);
|
||||
}
|
||||
|
||||
const speakingBias = ensureObject(runtime.speaking_bias);
|
||||
const candidateInterventions = Array.isArray(runtime.candidate_interventions)
|
||||
? runtime.candidate_interventions.filter(isObject)
|
||||
|
|
@ -113,6 +118,7 @@ function main() {
|
|||
}, 0);
|
||||
const topSpeakUrge = topThreadDesire(runtime, 4);
|
||||
const silencePressure = clamp(hoursSinceSpeech / 6, 0, 1);
|
||||
const sleepiness = sleepCycle.isNapping ? 1 : 0;
|
||||
|
||||
speakingBias.heraldRepoActivity = speakingBias.heraldRepoActivity !== false;
|
||||
speakingBias.minimumActiveReposForHeraldNote = Number(speakingBias.minimumActiveReposForHeraldNote ?? 2);
|
||||
|
|
@ -123,7 +129,7 @@ function main() {
|
|||
speakingBias.recentSpeechDamping = round3(recentSpeechDamping);
|
||||
|
||||
const targetNeedToSpeak = clamp(
|
||||
0.2 + silencePressure * 0.32 + draftPressure * 0.45 + freshRepoSweepPressure + topSpeakUrge * 0.28 - recentSpeechDamping * 0.5,
|
||||
0.2 + silencePressure * 0.32 + draftPressure * 0.45 + freshRepoSweepPressure + topSpeakUrge * 0.28 - recentSpeechDamping * 0.5 - sleepiness * 0.26,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
|
|
@ -132,7 +138,7 @@ function main() {
|
|||
speakingBias.needToSpeak = round3(priorNeedToSpeak + (targetNeedToSpeak - priorNeedToSpeak) * 0.42);
|
||||
|
||||
const targetConfessionPressure = clamp(
|
||||
0.16 + silencePressure * 0.22 + topExpressiveDelta(canonicalState) * 0.35 - recentSpeechDamping * 0.25,
|
||||
0.16 + silencePressure * 0.22 + topExpressiveDelta(canonicalState) * 0.35 - recentSpeechDamping * 0.25 + sleepiness * 0.18,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
|
|
@ -143,7 +149,7 @@ function main() {
|
|||
);
|
||||
|
||||
const targetNoveltyPressure = clamp(
|
||||
0.2 + recentNoveltyPressure + freshRepoSweepPressure * 0.7 + topSpeakUrge * 0.18,
|
||||
0.2 + recentNoveltyPressure + freshRepoSweepPressure * 0.7 + topSpeakUrge * 0.18 + sleepiness * 0.06,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
|
|
@ -178,6 +184,15 @@ function main() {
|
|||
noveltyPressure: speakingBias.noveltyPressure,
|
||||
recentSpeechDamping: speakingBias.recentSpeechDamping,
|
||||
},
|
||||
sleepCycle: {
|
||||
isNapping: sleepCycle.isNapping,
|
||||
currentNapStartedAt: sleepCycle.currentNapStartedAt ?? null,
|
||||
currentNapEndsAt: sleepCycle.currentNapEndsAt ?? null,
|
||||
nextNapStartsAt: sleepCycle.nextNapStartsAt ?? null,
|
||||
activeDreamThemes: Array.isArray(sleepCycle.activeDreamThemes)
|
||||
? sleepCycle.activeDreamThemes
|
||||
: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -227,6 +242,209 @@ function topExpressiveDelta(canonicalState) {
|
|||
return strongest;
|
||||
}
|
||||
|
||||
function applySleepBias(canonicalState, touchedVectors) {
|
||||
nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName: "situational_state",
|
||||
vectorName: "exhaustion",
|
||||
target: 0.74,
|
||||
factor: 0.28,
|
||||
});
|
||||
nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName: "behavioral_dimensions",
|
||||
vectorName: "withdrawal",
|
||||
target: 0.46,
|
||||
factor: 0.18,
|
||||
});
|
||||
nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName: "behavioral_dimensions",
|
||||
vectorName: "distance_seeking",
|
||||
target: 0.43,
|
||||
factor: 0.16,
|
||||
});
|
||||
nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName: "behavioral_dimensions",
|
||||
vectorName: "drive",
|
||||
target: 0.41,
|
||||
factor: 0.14,
|
||||
});
|
||||
nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName: "presentation_strategy",
|
||||
vectorName: "detachment",
|
||||
target: 0.38,
|
||||
factor: 0.16,
|
||||
});
|
||||
nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName: "voice_style",
|
||||
vectorName: "figurative_language",
|
||||
target: 0.68,
|
||||
factor: 0.18,
|
||||
});
|
||||
nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName: "voice_style",
|
||||
vectorName: "self_disclosure",
|
||||
target: 0.44,
|
||||
factor: 0.16,
|
||||
});
|
||||
nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName: "voice_style",
|
||||
vectorName: "technical_density",
|
||||
target: 0.24,
|
||||
factor: 0.18,
|
||||
});
|
||||
nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName: "voice_style",
|
||||
vectorName: "question_asking",
|
||||
target: 0.27,
|
||||
factor: 0.2,
|
||||
});
|
||||
}
|
||||
|
||||
function nudgeVector({
|
||||
canonicalState,
|
||||
touchedVectors,
|
||||
categoryName,
|
||||
vectorName,
|
||||
target,
|
||||
factor,
|
||||
}) {
|
||||
const category = canonicalState?.[categoryName];
|
||||
const vector = isObject(category) ? category[vectorName] : undefined;
|
||||
|
||||
if (!isObject(vector) || typeof vector.current_activation !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = vector.current_activation;
|
||||
const next = clamp(previous + (target - previous) * factor, 0, 1);
|
||||
vector.current_activation = round3(next);
|
||||
touchedVectors.push({
|
||||
key: `${categoryName}.${vectorName}`,
|
||||
previous,
|
||||
next,
|
||||
delta: next - previous,
|
||||
target,
|
||||
});
|
||||
}
|
||||
|
||||
function updateSleepCycle(runtime, now) {
|
||||
const sleepCycle = ensureObject(runtime.sleep_cycle);
|
||||
const enabled = sleepCycle.enabled !== false;
|
||||
const cycleHours = clampNumber(sleepCycle.cycleHours, 4, 2, 12);
|
||||
const napDurationMinutes = clampNumber(sleepCycle.napDurationMinutes, 60, 15, cycleHours * 60 - 5);
|
||||
const phaseOffsetMinutesLocal = clampNumber(sleepCycle.phaseOffsetMinutesLocal, 120, 0, cycleHours * 60 - 1);
|
||||
const replyMode =
|
||||
typeof sleepCycle.replyMode === "string" && sleepCycle.replyMode.trim().length > 0
|
||||
? sleepCycle.replyMode
|
||||
: "sleep_grumble";
|
||||
const activeDreamThemes = Array.isArray(sleepCycle.activeDreamThemes)
|
||||
? sleepCycle.activeDreamThemes.filter((value) => typeof value === "string")
|
||||
: [];
|
||||
|
||||
sleepCycle.enabled = enabled;
|
||||
sleepCycle.cycleHours = cycleHours;
|
||||
sleepCycle.napDurationMinutes = napDurationMinutes;
|
||||
sleepCycle.phaseOffsetMinutesLocal = phaseOffsetMinutesLocal;
|
||||
sleepCycle.replyMode = replyMode;
|
||||
sleepCycle.activeDreamThemes = activeDreamThemes;
|
||||
|
||||
if (!enabled) {
|
||||
sleepCycle.isNapping = false;
|
||||
sleepCycle.currentNapStartedAt = null;
|
||||
sleepCycle.currentNapEndsAt = null;
|
||||
sleepCycle.nextNapStartsAt = null;
|
||||
runtime.sleep_cycle = sleepCycle;
|
||||
return sleepCycle;
|
||||
}
|
||||
|
||||
const schedule = computeSleepSchedule(now, cycleHours, napDurationMinutes, phaseOffsetMinutesLocal);
|
||||
const wasNapping = sleepCycle.isNapping === true;
|
||||
sleepCycle.isNapping = schedule.isNapping;
|
||||
sleepCycle.currentNapStartedAt = schedule.currentNapStartedAt;
|
||||
sleepCycle.currentNapEndsAt = schedule.currentNapEndsAt;
|
||||
sleepCycle.nextNapStartsAt = schedule.nextNapStartsAt;
|
||||
|
||||
if (schedule.isNapping && !wasNapping) {
|
||||
sleepCycle.lastNapStartedAt = schedule.currentNapStartedAt;
|
||||
sleepCycle.dreamCountInCurrentNap = 0;
|
||||
sleepCycle.lastDistillationSummary = null;
|
||||
}
|
||||
|
||||
if (!schedule.isNapping && wasNapping) {
|
||||
sleepCycle.lastNapCompletedAt = schedule.previousNapEndedAt ?? sleepCycle.currentNapEndsAt ?? now.toISOString();
|
||||
sleepCycle.activeDreamThemes = [];
|
||||
}
|
||||
|
||||
runtime.sleep_cycle = sleepCycle;
|
||||
return sleepCycle;
|
||||
}
|
||||
|
||||
function computeSleepSchedule(now, cycleHours, napDurationMinutes, phaseOffsetMinutesLocal) {
|
||||
const cycleLengthMinutes = cycleHours * 60;
|
||||
const dayStart = new Date(now);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const minutesSinceDayStart =
|
||||
now.getHours() * 60 +
|
||||
now.getMinutes() +
|
||||
now.getSeconds() / 60 +
|
||||
now.getMilliseconds() / 60000;
|
||||
const adjustedMinutes = minutesSinceDayStart - phaseOffsetMinutesLocal;
|
||||
const cycleIndex = Math.floor(adjustedMinutes / cycleLengthMinutes);
|
||||
const currentCycleStartMinutes = phaseOffsetMinutesLocal + cycleIndex * cycleLengthMinutes;
|
||||
const napStartMinutes = currentCycleStartMinutes;
|
||||
const napEndMinutes = napStartMinutes + napDurationMinutes;
|
||||
const isNapping = minutesSinceDayStart >= napStartMinutes && minutesSinceDayStart < napEndMinutes;
|
||||
const currentNapStartedAt = isNapping
|
||||
? minutesOffsetIso(dayStart, napStartMinutes)
|
||||
: null;
|
||||
const currentNapEndsAt = isNapping ? minutesOffsetIso(dayStart, napEndMinutes) : null;
|
||||
const nextNapStartsAt = isNapping
|
||||
? minutesOffsetIso(dayStart, napStartMinutes + cycleLengthMinutes)
|
||||
: minutesSinceDayStart < napStartMinutes
|
||||
? minutesOffsetIso(dayStart, napStartMinutes)
|
||||
: minutesOffsetIso(dayStart, napStartMinutes + cycleLengthMinutes);
|
||||
const previousNapEndedAt =
|
||||
napEndMinutes <= minutesSinceDayStart
|
||||
? minutesOffsetIso(dayStart, napEndMinutes)
|
||||
: minutesOffsetIso(dayStart, napEndMinutes - cycleLengthMinutes);
|
||||
|
||||
return {
|
||||
isNapping,
|
||||
currentNapStartedAt,
|
||||
currentNapEndsAt,
|
||||
nextNapStartsAt,
|
||||
previousNapEndedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function minutesOffsetIso(dayStart, offsetMinutes) {
|
||||
const shifted = new Date(dayStart.getTime() + offsetMinutes * 60 * 1000);
|
||||
return shifted.toISOString();
|
||||
}
|
||||
|
||||
function clampNumber(value, fallback, min, max) {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? clamp(value, min, max)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function computeTargetActivation({
|
||||
categoryName,
|
||||
vectorName,
|
||||
|
|
|
|||
|
|
@ -46,3 +46,4 @@
|
|||
{"ts":"2026-05-05T09:18:00Z","type":"architecture","status":"ok","note":"The moderation loop is also Void's only standing cross-project vantage point, so quiet runs now need an explicit tracked-repo activity sweep alongside archive dives. Repo motion belongs in the associative lane and bridge, with its own state breadcrumbs, so Void can comment on the wider experiment zoo instead of only whatever one chat thread happened to say aloud."}
|
||||
{"ts":"2026-05-05T10:02:00Z","type":"architecture","status":"ok","note":"The repo-activity herald lane cannot be as timid as ordinary private rumination. When a fresh convergence survives a pass or two and still feels room-native, the bridge should bias toward draft or speech; otherwise the cross-project vantage point turns into a mute little weather station."}
|
||||
{"ts":"2026-05-05T10:27:00Z","type":"architecture","status":"ok","note":"The herald loop now novelty-checks candidate thoughts against archived Discord before suppressing them; if the room has not already said the thing, novelty should push strongly toward speech. A separate 5-minute mood-drift organ now perturbs the shared personality-vector activations over time, tracks speak/confession/novelty pressure, and damps that pressure based on actual recent speech so Void's expressive state does not stay emotionally flat."}
|
||||
{"ts":"2026-05-05T03:07:00+01:00","type":"architecture","status":"ok","note":"Void's shared self-state now carries a real nap cycle as part of the mood organ, not a separate pretend brain. During nap windows the moderation loop distills and prunes memory, emits dream residues into state, and the direct reply lanes consume that same projection to downshift into brief sleepy grumbles instead of doing normal attentive service-work."}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ accepted_design:
|
|||
- The repo-weather lane should not be pathologically timid: a fresh, grounded convergence across active repos should bias the bridge toward a concise herald note or at least a stronger draft instead of being buried as private diary sludge by default.
|
||||
- Before suppressing a thought, the moderation loop should novelty-check it against archived Discord with semantic search. If the room has not really said the thing yet, that novelty should push strongly toward speech rather than silence.
|
||||
- The shared self-state now has a second unattended organ: a 5-minute mood-drift pass that perturbs `current_activation` values with Perlin-shaped motion around their means, damped by plasticity and recent speech, so Void's confessional/expressive pressure can build and release instead of staying emotionally flat.
|
||||
- That same shared self-state now carries a scheduled nap cycle. Naps are for maintenance, not vacancy: the moderation loop should distill memory, prune clutter, and leave behind dream memories plus active dream themes that the direct reply lanes can mumble through later.
|
||||
- Deep agent-state, personality, culture, and persistent-world design work now belongs to the sibling Ghostlight repo; VoidBot should consume that work instead of remaining its long-term home.
|
||||
- Long-running work should expose durable logs or status files instead of relying on one attached session and hope.
|
||||
- Persistent project memory must stay ruthlessly distilled; git history and artifacts carry routine proof.
|
||||
|
|
@ -98,7 +99,7 @@ landed_baseline:
|
|||
- 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`.
|
||||
- A separate scheduled moderation-rumination loop now launches `codex exec` from the VoidBot workspace every 15 minutes, reads tracked server rules, polls recent chronology through the plain Node helper, sweeps recent tracked-repo commit activity for broader experiment awareness, novelty-checks candidate thoughts against archived Discord, uses the usual VoidBot MCP/tool surface for archive and project rumination, performs explicit random archive excursions when the room is quiet, and maintains one ignored JSON state file under `.voidbot/private/` instead of smearing moderation memory across random notes.
|
||||
- A second scheduled task now runs the plain Node mood-drift script every 5 minutes, updating the shared self-state's activations plus speaking-bias meter and damping expression pressure based on actual recent speech receipts.
|
||||
- A second scheduled task now runs the plain Node mood-drift script every 5 minutes, updating the shared self-state's activations plus speaking-bias meter, damping expression pressure based on actual recent speech receipts, and maintaining the four-hour nap clock that the moderation loop and direct reply prompts both consume.
|
||||
|
||||
rejected_paths:
|
||||
- name: transcript_only_continuity
|
||||
|
|
|
|||
Loading…
Reference in a new issue