diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 462ddd5..061ebd5 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -30,6 +30,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - 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. +- 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` has been rebuilt as a typed-only sleep/speaking-pressure pass over `.voidbot/private/void-self-state.cc`. The old memory organ, vector incubation, cleanup, dream residue, and legacy mirror writes are offline until rebuilt on typed operations. - `scripts/void-memory-organ.mjs` and its split helper modules are old machinery now. They may still be useful as forensic evidence, but the next design should not preserve their cleanup/agency compensators just because they exist. - Most of the deterministic legacy translation and retention code is recent compensator cruft from failed attempts to manually clean an exploded state file. Do not preserve it as an earned organ. Architect the state boundary so that bad state cannot enter, and keep only narrow historical migrations for old debris. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index 6d50db2..0f1b6b6 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -116,6 +116,8 @@ This note is the source-grounded description of how the live VoidBot stack is sh - 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 Codex rumination; applies returned typed operations through `scripts/void-self-state.mjs`; then records parent-owned cursor and speech receipts. +- `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. - `scripts/moderation-state-store.mjs` - legacy moderation-state wrapper. It is not part of the active typed self-state path. - `scripts/void-self-state.mjs` @@ -174,7 +176,7 @@ Within the Postgres path, the implementation is split on purpose now too: 1. The Windows scheduled task `Void Moderator Rumination` is disabled. 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`. +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. The runner loads `prompts/void-moderator-rumination.md`, substitutes the context/state/output paths, and sends that prompt to Codex. 5. Codex may use retrieval and analysis tools, but its durable state output is restricted to `.voidbot/status/moderation-rumination-operations.json`. 6. The parent runner applies those typed operations through `scripts/void-self-state.mjs`, then records reviewed-message cursor and any speech receipt itself. diff --git a/notes/voidbot-implementation-plan.md b/notes/voidbot-implementation-plan.md index 914e785..fe3bb4d 100644 --- a/notes/voidbot-implementation-plan.md +++ b/notes/voidbot-implementation-plan.md @@ -124,6 +124,7 @@ The runner can hand these to a CLI/MCP tool. The store decides what survives. - Self-state prompt summaries are derived from typed documents. - Recent room context is derived from Discord archive and the current poll. +- Prompt-facing recent chronology is derived as relative phrases. Exact timestamps remain state/status/cursor data and should not be fed to the child rumination loop unless a tool or parent-owned operation specifically needs them. - Topic saturation is derived from incubation/memory support, not separately stored as another truth. - Source coverage is derived from recent evidence refs and repo/archive metadata. - Need-to-speak can keep a small pressure field, but its inputs remain receipts, candidates, and time since last speech. @@ -159,6 +160,7 @@ The runner can hand these to a CLI/MCP tool. The store decides what survives. - legacy projection/mirror state still exists on disk as failed-boundary residue and must not become source material for the rebuild - the old memory organ is split by concern, but its legacy translation, retention/compaction, identity crystallization, candidate interventions, and value-pressure inference are offline scaffolding, not earned architecture - the scheduled moderation runner has a typed rumination contract again: bounded context in, reviewed prompt template, typed operation payloads out, parent-owned cursor/receipt recording +- recent event timing in that bounded context now crosses a projection boundary first, so the child sees "5 minutes ago" language while the parent keeps exact timestamps for bookkeeping - deterministic cleanup is mostly recent scar tissue from failed manual cleanup passes; it should be deleted rather than ported unless it protects a real invariant in the new model - hard-wired agency policy is mostly current scaffolding, not the final authority; identity, advocacy, and speech candidates should emerge from typed memory/state operations plus model-owned judgment under validation - sleep/distillation currently lacks a hard meaning-preservation implementation in the active runner, so it remains offline until rebuilt diff --git a/packages/core/src/void-self-state-projection.ts b/packages/core/src/void-self-state-projection.ts index 57b4152..70d03b9 100644 --- a/packages/core/src/void-self-state-projection.ts +++ b/packages/core/src/void-self-state-projection.ts @@ -171,9 +171,10 @@ export function buildVoidSelfStateProjection( .reverse() .map((memory) => memory.summary); const isNapping = sleepCycle.isNapping === true; - const napStartedAt = sleepCycle.currentNapStartedAt; - const napEndsAt = sleepCycle.currentNapEndsAt; - const nextNapAt = sleepCycle.nextNapStartsAt; + const now = new Date(); + const napStartedAt = formatRelativeTime(sleepCycle.currentNapStartedAt, now); + const napEndsAt = formatRelativeTime(sleepCycle.currentNapEndsAt, now); + const nextNapAt = formatRelativeTime(sleepCycle.nextNapStartsAt, now); return { mode: isNapping ? "napping" : "awake", @@ -241,14 +242,15 @@ function renderTypedSleepCycleSummary( thoughtMemory: VoidThoughtMemory, ): string { const sleepCycle = runtime.sleepCycle; + const now = new Date(); const dreams = thoughtMemory.memories .filter((memory) => memory.kind === "dream_residue") .slice(-2) .reverse() .map((memory) => `${memory.target.label ?? memory.target.id}: ${memory.summary}`); const headline = sleepCycle.isNapping - ? `- Sleep: napping${sleepCycle.currentNapEndsAt ? ` until ${sleepCycle.currentNapEndsAt}` : ""}.` - : `- Sleep cycle: awake${sleepCycle.nextNapStartsAt ? `; next nap ${sleepCycle.nextNapStartsAt}` : ""}.`; + ? `- Sleep: napping${sleepCycle.currentNapEndsAt ? `; ends ${formatRelativeTime(sleepCycle.currentNapEndsAt, now)}` : ""}.` + : `- Sleep cycle: awake${sleepCycle.nextNapStartsAt ? `; next nap ${formatRelativeTime(sleepCycle.nextNapStartsAt, now)}` : ""}.`; const themeLine = sleepCycle.activeDreamThemes.length > 0 ? `- Dream themes in reach: ${sleepCycle.activeDreamThemes.join(" | ")}` @@ -305,7 +307,7 @@ function renderTransientRoomLog( const lines = recentMessages.map( (message) => - `- [${message.timestamp}] ${message.authorName}: ${collapseWhitespace(message.content || "(no text content)")}`, + `- ${formatRelativeTime(message.timestamp)} ${message.authorName}: ${collapseWhitespace(message.content || "(no text content)")}`, ); return [ @@ -318,3 +320,59 @@ function renderTransientRoomLog( function collapseWhitespace(value: string): string { return value.replace(/\s+/g, " ").trim(); } + +function formatRelativeTime(value: string | undefined, now = new Date()): string { + if (!value) { + return "recently"; + } + + const timestamp = Date.parse(value); + if (!Number.isFinite(timestamp)) { + return "recently"; + } + + const deltaMs = timestamp - now.getTime(); + const future = deltaMs > 0; + const absoluteSeconds = Math.abs(deltaMs) / 1000; + const phrase = describeRelativeDuration(absoluteSeconds); + + if (phrase === "just now") { + return phrase; + } + + return future ? `in ${phrase}` : `${phrase} ago`; +} + +function describeRelativeDuration(absoluteSeconds: number): string { + if (absoluteSeconds < 45) { + return "just now"; + } + + const minutes = Math.round(absoluteSeconds / 60); + if (minutes < 90) { + return `${minutes} minute${minutes === 1 ? "" : "s"}`; + } + + const hours = Math.round(minutes / 60); + if (hours < 36) { + return `${hours} hour${hours === 1 ? "" : "s"}`; + } + + const days = Math.round(hours / 24); + if (days < 21) { + return `${days} day${days === 1 ? "" : "s"}`; + } + + const weeks = Math.round(days / 7); + if (weeks < 10) { + return `${weeks} week${weeks === 1 ? "" : "s"}`; + } + + const months = Math.round(days / 30); + if (months < 18) { + return `${months} month${months === 1 ? "" : "s"}`; + } + + const years = Math.round(days / 365); + return `${years} year${years === 1 ? "" : "s"}`; +} diff --git a/prompts/void-moderator-rumination.md b/prompts/void-moderator-rumination.md index 3c40321..b6922a3 100644 --- a/prompts/void-moderator-rumination.md +++ b/prompts/void-moderator-rumination.md @@ -13,6 +13,7 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup - 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. diff --git a/scripts/lib/void-rumination-context-projection.ps1 b/scripts/lib/void-rumination-context-projection.ps1 new file mode 100644 index 0000000..50f4ef4 --- /dev/null +++ b/scripts/lib/void-rumination-context-projection.ps1 @@ -0,0 +1,368 @@ +Set-StrictMode -Version Latest + +function Convert-ToValueArray { + param($Value) + + if ($null -eq $Value) { + return @() + } + + if ($Value -is [System.Array]) { + return @($Value) + } + + return @($Value) +} + +function Get-ObjectPropertyValue { + param( + $Value, + [Parameter(Mandatory = $true)] + [string] $Name + ) + + if ($null -eq $Value) { + return $null + } + + $property = $Value.PSObject.Properties[$Name] + if ($null -eq $property) { + return $null + } + return $property.Value +} + +function Get-ObjectPropertyString { + param( + $Value, + [Parameter(Mandatory = $true)] + [string] $Name + ) + + $value = Get-ObjectPropertyValue -Value $Value -Name $Name + if ($null -eq $value) { + return $null + } + + $text = [string]$value + if ([string]::IsNullOrWhiteSpace($text)) { + return $null + } + return $text +} + +function Format-RelativeTime { + param( + [string] $Value, + [Parameter(Mandatory = $true)] + [DateTime] $Now + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return $null + } + + $timestamp = [DateTime]::MinValue + if (-not [DateTime]::TryParse($Value, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$timestamp)) { + return $null + } + + $timestampUtc = $timestamp.ToUniversalTime() + $delta = $timestampUtc - $Now.ToUniversalTime() + $future = $delta.TotalSeconds -gt 0 + $seconds = [Math]::Abs($delta.TotalSeconds) + + if ($seconds -lt 45) { + return "just now" + } + + if ($seconds -lt 5400) { + $minutes = [Math]::Max(1, [int][Math]::Round($seconds / 60)) + $unit = if ($minutes -eq 1) { "minute" } else { "minutes" } + $phrase = "$minutes $unit" + } elseif ($seconds -lt 129600) { + $hours = [Math]::Max(1, [int][Math]::Round($seconds / 3600)) + $unit = if ($hours -eq 1) { "hour" } else { "hours" } + $phrase = "$hours $unit" + } elseif ($seconds -lt 1814400) { + $days = [Math]::Max(1, [int][Math]::Round($seconds / 86400)) + $unit = if ($days -eq 1) { "day" } else { "days" } + $phrase = "$days $unit" + } elseif ($seconds -lt 6048000) { + $weeks = [Math]::Max(1, [int][Math]::Round($seconds / 604800)) + $unit = if ($weeks -eq 1) { "week" } else { "weeks" } + $phrase = "$weeks $unit" + } elseif ($seconds -lt 47304000) { + $months = [Math]::Max(1, [int][Math]::Round($seconds / 2592000)) + $unit = if ($months -eq 1) { "month" } else { "months" } + $phrase = "$months $unit" + } else { + $years = [Math]::Max(1, [int][Math]::Round($seconds / 31536000)) + $unit = if ($years -eq 1) { "year" } else { "years" } + $phrase = "$years $unit" + } + + if ($future) { + return "in $phrase" + } + return "$phrase ago" +} + +function Project-RelativeTimestamp { + param( + $Value, + [string] $Name, + [DateTime] $Now + ) + + return Format-RelativeTime -Value (Get-ObjectPropertyString -Value $Value -Name $Name) -Now $Now +} + +function Project-RecentHistoryForRumination { + param($History, [DateTime] $Now) + + $messages = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $History -Name "messages")) + return @{ + source = "discord_history" + messageCount = [int]$messages.Count + messages = @( + $messages | ForEach-Object { + @{ + id = Get-ObjectPropertyString -Value $_ -Name "id" + authorId = Get-ObjectPropertyString -Value $_ -Name "authorId" + authorName = Get-ObjectPropertyString -Value $_ -Name "authorName" + isBot = [bool](Get-ObjectPropertyValue -Value $_ -Name "isBot") + when = Project-RelativeTimestamp -Value $_ -Name "timestamp" -Now $Now + content = Get-ObjectPropertyString -Value $_ -Name "content" + } + } + ) + } +} + +function Project-OpenCasesForRumination { + param($Cases, [DateTime] $Now) + + return @( + @(Convert-ToValueArray -Value $Cases) | ForEach-Object { + @{ + sourceMessageId = Get-ObjectPropertyString -Value $_ -Name "sourceMessageId" + status = Get-ObjectPropertyString -Value $_ -Name "status" + summary = Get-ObjectPropertyString -Value $_ -Name "summary" + authorName = Get-ObjectPropertyString -Value $_ -Name "authorName" + channelId = Get-ObjectPropertyString -Value $_ -Name "channelId" + messageUrl = Get-ObjectPropertyString -Value $_ -Name "messageUrl" + whyItMatters = Get-ObjectPropertyString -Value $_ -Name "whyItMatters" + created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now + lastTouched = Project-RelativeTimestamp -Value $_ -Name "lastTouchedAt" -Now $Now + resolved = Project-RelativeTimestamp -Value $_ -Name "resolvedAt" -Now $Now + tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags")) + } + } + ) +} + +function Project-SpeechReceiptsForRumination { + param($Receipts, [DateTime] $Now) + + return @( + @(Convert-ToValueArray -Value $Receipts) | ForEach-Object { + @{ + receiptKey = Get-ObjectPropertyString -Value $_ -Name "receiptKey" + sent = Project-RelativeTimestamp -Value $_ -Name "sentAt" -Now $Now + mode = Get-ObjectPropertyString -Value $_ -Name "mode" + transport = Get-ObjectPropertyString -Value $_ -Name "transport" + channelId = Get-ObjectPropertyString -Value $_ -Name "channelId" + replyToMessageId = Get-ObjectPropertyString -Value $_ -Name "replyToMessageId" + preview = Get-ObjectPropertyString -Value $_ -Name "preview" + } + } + ) +} + +function Project-MemoriesForRumination { + param($Memories, [DateTime] $Now) + + return @( + @(Convert-ToValueArray -Value $Memories) | ForEach-Object { + @{ + memoryId = Get-ObjectPropertyString -Value $_ -Name "memoryId" + kind = Get-ObjectPropertyString -Value $_ -Name "kind" + target = Get-ObjectPropertyValue -Value $_ -Name "target" + summary = Get-ObjectPropertyString -Value $_ -Name "summary" + claim = Get-ObjectPropertyString -Value $_ -Name "claim" + question = Get-ObjectPropertyString -Value $_ -Name "question" + tension = Get-ObjectPropertyString -Value $_ -Name "tension" + actionImplication = Get-ObjectPropertyString -Value $_ -Name "actionImplication" + created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now + updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now + retired = Project-RelativeTimestamp -Value $_ -Name "retiredAt" -Now $Now + evidenceRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "evidenceRefs")) + tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags")) + } + } + ) +} + +function Project-IncubationForRumination { + param($Threads, [DateTime] $Now) + + return @( + @(Convert-ToValueArray -Value $Threads) | ForEach-Object { + @{ + threadId = Get-ObjectPropertyString -Value $_ -Name "threadId" + target = Get-ObjectPropertyValue -Value $_ -Name "target" + topic = Get-ObjectPropertyString -Value $_ -Name "topic" + summary = Get-ObjectPropertyString -Value $_ -Name "summary" + supportMemoryIds = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "supportMemoryIds")) + evidenceRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "evidenceRefs")) + maturation = Get-ObjectPropertyValue -Value $_ -Name "maturation" + noveltyToRoom = Get-ObjectPropertyValue -Value $_ -Name "noveltyToRoom" + noveltyToSelf = Get-ObjectPropertyValue -Value $_ -Name "noveltyToSelf" + desireToSpeak = Get-ObjectPropertyValue -Value $_ -Name "desireToSpeak" + saturationScore = Get-ObjectPropertyValue -Value $_ -Name "saturationScore" + status = Get-ObjectPropertyString -Value $_ -Name "status" + created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now + updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now + } + } + ) +} + +function Project-InterventionsForRumination { + param($Interventions, [DateTime] $Now) + + return @( + @(Convert-ToValueArray -Value $Interventions) | ForEach-Object { + @{ + interventionId = Get-ObjectPropertyString -Value $_ -Name "interventionId" + kind = Get-ObjectPropertyString -Value $_ -Name "kind" + status = Get-ObjectPropertyString -Value $_ -Name "status" + target = Get-ObjectPropertyValue -Value $_ -Name "target" + summary = Get-ObjectPropertyString -Value $_ -Name "summary" + draft = Get-ObjectPropertyString -Value $_ -Name "draft" + priority = Get-ObjectPropertyValue -Value $_ -Name "priority" + mustEventuallyShare = Get-ObjectPropertyValue -Value $_ -Name "mustEventuallyShare" + created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now + updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now + spoken = Project-RelativeTimestamp -Value $_ -Name "spokenAt" -Now $Now + retired = Project-RelativeTimestamp -Value $_ -Name "retiredAt" -Now $Now + tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags")) + } + } + ) +} + +function Project-ScheduledRuntimeForRumination { + param($Runtime, [DateTime] $Now) + + $sleepCycle = Get-ObjectPropertyValue -Value $Runtime -Name "sleepCycle" + $speakingPressure = Get-ObjectPropertyValue -Value $Runtime -Name "speakingPressure" + $lastRuns = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Runtime -Name "lastRuns")) + + return @{ + sleepCycle = @{ + isNapping = [bool](Get-ObjectPropertyValue -Value $sleepCycle -Name "isNapping") + currentNapStarted = Project-RelativeTimestamp -Value $sleepCycle -Name "currentNapStartedAt" -Now $Now + currentNapEnds = Project-RelativeTimestamp -Value $sleepCycle -Name "currentNapEndsAt" -Now $Now + nextNapStarts = Project-RelativeTimestamp -Value $sleepCycle -Name "nextNapStartsAt" -Now $Now + activeDreamThemes = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $sleepCycle -Name "activeDreamThemes")) + } + speakingPressure = @{ + needToSpeak = Get-ObjectPropertyValue -Value $speakingPressure -Name "needToSpeak" + confessionPressure = Get-ObjectPropertyValue -Value $speakingPressure -Name "confessionPressure" + noveltyPressure = Get-ObjectPropertyValue -Value $speakingPressure -Name "noveltyPressure" + recentSpeechDamping = Get-ObjectPropertyValue -Value $speakingPressure -Name "recentSpeechDamping" + lastSpoke = Project-RelativeTimestamp -Value $speakingPressure -Name "lastSpokeAt" -Now $Now + lastHerald = Project-RelativeTimestamp -Value $speakingPressure -Name "lastHeraldAt" -Now $Now + } + lastRuns = @( + $lastRuns | ForEach-Object { + @{ + runner = Get-ObjectPropertyString -Value $_ -Name "runner" + ran = Project-RelativeTimestamp -Value $_ -Name "ranAt" -Now $Now + summary = Get-ObjectPropertyString -Value $_ -Name "summary" + } + } + ) + } +} + +function Project-CursorForRumination { + param($Cursor, [DateTime] $Now) + + return @{ + lastReviewedMessageId = Get-ObjectPropertyString -Value $Cursor -Name "lastReviewedMessageId" + lastReviewed = Project-RelativeTimestamp -Value $Cursor -Name "lastReviewedTimestamp" -Now $Now + } +} + +function Project-RepoCommitForRumination { + param($Commit, [DateTime] $Now) + + if ($null -eq $Commit) { + return $null + } + + $hash = Get-ObjectPropertyString -Value $Commit -Name "hash" + return @{ + hash = if ($hash -and $hash.Length -gt 12) { $hash.Substring(0, 12) } else { $hash } + committed = Project-RelativeTimestamp -Value $Commit -Name "committedAt" -Now $Now + author = Get-ObjectPropertyString -Value $Commit -Name "author" + subject = Get-ObjectPropertyString -Value $Commit -Name "subject" + diffstat = Get-ObjectPropertyValue -Value $Commit -Name "diffstat" + changedPaths = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Commit -Name "changedPaths")) + } +} + +function Select-RuminationRepoActivity { + param($RepoActivity, [DateTime] $Now) + + if ($null -eq $RepoActivity) { + return $null + } + + $status = Get-ObjectPropertyString -Value $RepoActivity -Name "status" + if ($status -eq "failed") { + return @{ + status = "failed" + error = Get-ObjectPropertyString -Value $RepoActivity -Name "error" + } + } + + $repos = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $RepoActivity -Name "repos")) + $freshRepos = @( + $repos | + Where-Object { + $recent = Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount" + $repoStatus = Get-ObjectPropertyString -Value $_ -Name "status" + $null -ne $recent -and $repoStatus -eq "ok" -and [int]$recent -gt 0 + } | + Select-Object -First 8 + ) + $projectedRepos = @( + $freshRepos | ForEach-Object { + @{ + repoName = Get-ObjectPropertyString -Value $_ -Name "repoName" + branch = Get-ObjectPropertyString -Value $_ -Name "branch" + status = Get-ObjectPropertyString -Value $_ -Name "status" + recentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount" + windowRecentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "windowRecentCommitCount" + suppressedRecentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "suppressedRecentCommitCount" + latestCommit = Project-RepoCommitForRumination -Commit (Get-ObjectPropertyValue -Value $_ -Name "latestCommit") -Now $Now + commits = @( + @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "commits")) | ForEach-Object { + Project-RepoCommitForRumination -Commit $_ -Now $Now + } + ) + } + } + ) + + return @{ + generated = Project-RelativeTimestamp -Value $RepoActivity -Name "generatedAt" -Now $Now + cursorMode = Get-ObjectPropertyString -Value $RepoActivity -Name "cursorMode" + summary = if ($projectedRepos.Count -gt 0) { "New tracked repo commits crossed the saved repo-activity cursor." } else { "No new tracked repo commits crossed the saved repo-activity cursor." } + freshRepos = $projectedRepos + } +} diff --git a/scripts/run-void-moderator-rumination.ps1 b/scripts/run-void-moderator-rumination.ps1 index 9791ef1..72cb7a9 100644 --- a/scripts/run-void-moderator-rumination.ps1 +++ b/scripts/run-void-moderator-rumination.ps1 @@ -21,11 +21,14 @@ $operationOutputPath = Join-Path $statusDir "moderation-rumination-operations.js $contextPath = Join-Path $statusDir "moderation-rumination-context.json" $lockPath = Join-Path $statusDir "moderation-rumination.lock" $promptTemplatePath = Join-Path $repoRoot "prompts\void-moderator-rumination.md" +$contextProjectionScriptPath = Join-Path $repoRoot "scripts\lib\void-rumination-context-projection.ps1" $recentHistoryScriptPath = Join-Path $repoRoot "scripts\export-recent-discord-history.mjs" $repoActivityScriptPath = Join-Path $repoRoot "scripts\export-recent-repo-activity.mjs" $selfStateScriptPath = Join-Path $repoRoot "scripts\void-self-state.mjs" $startedAtUtc = [DateTime]::UtcNow +. $contextProjectionScriptPath + function Write-JsonFile { param( [Parameter(Mandatory = $true)] @@ -268,29 +271,6 @@ function Convert-ToOperationArray { return @($Value) } -function Get-ObjectPropertyString { - param( - $Value, - [Parameter(Mandatory = $true)] - [string] $Name - ) - - if ($null -eq $Value) { - return $null - } - - $property = $Value.PSObject.Properties[$Name] - if ($null -eq $property -or $null -eq $property.Value) { - return $null - } - - $text = [string]$property.Value - if ([string]::IsNullOrWhiteSpace($text)) { - return $null - } - return $text -} - function Convert-LastSpeechToReceiptOperation { param($Speech) @@ -339,33 +319,6 @@ function Convert-LastSpeechToReceiptOperation { } } -function Select-RuminationRepoActivity { - param($RepoActivity) - - if ($null -eq $RepoActivity) { - return $null - } - - $reposProperty = $RepoActivity.PSObject.Properties["repos"] - $repos = if ($null -ne $reposProperty -and $null -ne $reposProperty.Value) { @($reposProperty.Value) } else { @() } - $freshRepos = @( - $repos | - Where-Object { - $recent = $_.PSObject.Properties["recentCommitCount"] - $status = $_.PSObject.Properties["status"] - $null -ne $recent -and $null -ne $status -and [string]$status.Value -eq "ok" -and [int]$recent.Value -gt 0 - } | - Select-Object -First 8 - ) - - return @{ - generatedAt = Get-ObjectPropertyString -Value $RepoActivity -Name "generatedAt" - cursorMode = Get-ObjectPropertyString -Value $RepoActivity -Name "cursorMode" - digest = Get-ObjectPropertyString -Value $RepoActivity -Name "digest" - freshRepos = $freshRepos - } -} - trap { $finishedAtUtc = [DateTime]::UtcNow $failureMessage = $_.Exception.Message @@ -413,7 +366,7 @@ if (Test-Path $operationOutputPath) { Remove-Item -LiteralPath $operationOutputPath -Force } -foreach ($requiredPath in @($recentHistoryScriptPath, $repoActivityScriptPath, $selfStateScriptPath)) { +foreach ($requiredPath in @($contextProjectionScriptPath, $recentHistoryScriptPath, $repoActivityScriptPath, $selfStateScriptPath)) { if (-not (Test-Path $requiredPath)) { throw "Missing required helper at $requiredPath" } @@ -473,26 +426,21 @@ try { } Write-JsonFile -Path $contextPath -Data @{ - generatedAt = [DateTime]::UtcNow.ToString("o") + generated = "now" stateFile = $stateFilePath noPost = [bool]$NoPost selfStateSummary = $typedContext.summary - openCases = $typedState.moderationCursor.openCases - speechReceipts = $typedState.speechReceipts.recentReceipts - memories = $typedState.thoughtMemory.memories - incubation = $typedState.thoughtMemory.incubation - candidateInterventions = $typedState.candidateInterventions.interventions - scheduledRuntime = $typedState.scheduledRuntime - priorCursor = @{ - lastReviewedMessageId = Get-ObjectPropertyString -Value $priorCursor -Name "lastReviewedMessageId" - lastReviewedTimestamp = Get-ObjectPropertyString -Value $priorCursor -Name "lastReviewedTimestamp" - } - observedCursor = @{ - lastReviewedMessageId = Get-ObjectPropertyString -Value $observedCursor -Name "lastReviewedMessageId" - lastReviewedTimestamp = Get-ObjectPropertyString -Value $observedCursor -Name "lastReviewedTimestamp" - } - recentHistory = $history - repoActivity = Select-RuminationRepoActivity -RepoActivity $repoActivity + chronology = "Times in this prompt-facing context are relative phrases. Exact timestamps stay parent-owned for typed state and cursor bookkeeping." + openCases = @(Project-OpenCasesForRumination -Cases $typedState.moderationCursor.openCases -Now $startedAtUtc) + speechReceipts = @(Project-SpeechReceiptsForRumination -Receipts $typedState.speechReceipts.recentReceipts -Now $startedAtUtc) + memories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.memories -Now $startedAtUtc) + incubation = @(Project-IncubationForRumination -Threads $typedState.thoughtMemory.incubation -Now $startedAtUtc) + candidateInterventions = @(Project-InterventionsForRumination -Interventions $typedState.candidateInterventions.interventions -Now $startedAtUtc) + scheduledRuntime = Project-ScheduledRuntimeForRumination -Runtime $typedState.scheduledRuntime -Now $startedAtUtc + priorCursor = Project-CursorForRumination -Cursor $priorCursor -Now $startedAtUtc + observedCursor = Project-CursorForRumination -Cursor $observedCursor -Now $startedAtUtc + recentHistory = Project-RecentHistoryForRumination -History $history -Now $startedAtUtc + repoActivity = Select-RuminationRepoActivity -RepoActivity $repoActivity -Now $startedAtUtc } $stateWriteBefore = if (Test-Path $stateFilePath) { (Get-Item $stateFilePath).LastWriteTimeUtc } else { [DateTime]::MinValue } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 2f3c998..1816d12 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -107,3 +107,4 @@ {"ts":"2026-05-16T18:12:51Z","type":"implementation","status":"ok","note":"Restored scheduled moderation as typed rumination instead of cursor-only bookkeeping. The runner now loads prompts/void-moderator-rumination.md, builds a bounded typed context packet from .voidbot/private/void-self-state.cc, accepts only typed operation output, and leaves cursor/receipt writes parent-owned."} {"ts":"2026-05-17T07:31:53Z","type":"implementation","status":"ok","note":"Cleaned up the typed self-state read projection boundary: packages/core/src/void-self-state-projection.ts now owns typed document to agent-facing summary/projection rendering, while void-self-state-loader.ts is a thin .cc loader/delegator. Projection smoke confirmed nap mode and transient room context render from a temp .cc store."} {"ts":"2026-05-17T07:36:19Z","type":"implementation","status":"ok","note":"Reduced template-language leakage in Void's active memory surfaces: the typed self-state projection now renders memories and speaking pressure as natural-language thought surfaces instead of claim=/tension=/next= or needToSpeak labels, and the rumination prompt now frames the schema as a storage contract rather than Void's voice. Also removed obvious stock phrase generators from old memory-organ helpers."} +{"ts":"2026-05-17T07:51:34Z","type":"implementation","status":"ok","note":"Moderation rumination now projects prompt-facing chronology through scripts/lib/void-rumination-context-projection.ps1: recent Discord, repo activity, receipts, memory, cursor, and runtime times render as relative phrases while exact timestamps stay in typed state/status/parent bookkeeping."} diff --git a/state/map.yaml b/state/map.yaml index 83607eb..7c7fcbc 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -85,6 +85,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 memory organ should queue 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 moderation state as `moderation_runtime.recent_delivery_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 ` 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`. - 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 `moderation_runtime.repo_activity_cursor` inside the live moderation state 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. - Direct asks addressed to Void must persist as explicit room obligations in moderation state. The reviewed cursor is not the same thing as closure, and optional repo-weather should not preempt an unanswered human. @@ -129,7 +130,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 old `Void Moderator Rumination` and `Void Mood Drift` scheduled tasks are disabled. Their replacement entry scripts are typed-only: moderation now builds a bounded context packet, runs the reviewed rumination prompt from `prompts/void-moderator-rumination.md`, and applies typed operation payloads; mood updates typed sleep/speaking pressure in `.voidbot/private/void-self-state.cc`. + - The old `Void Moderator Rumination` and `Void Mood Drift` scheduled tasks are disabled. Their replacement entry scripts are typed-only: moderation now builds a bounded context packet with prompt-facing relative chronology, runs the reviewed rumination prompt from `prompts/void-moderator-rumination.md`, and applies typed operation payloads; mood updates typed sleep/speaking pressure in `.voidbot/private/void-self-state.cc`. - The active self-state service no longer registers, reads, or writes the legacy moderation document during routine operations. - The old Codex rumination, memory-organ cleanup, repo-weather, vector incubation, legacy mirror repair, and JSON projection paths are offline until rebuilt against the typed state boundary. - Memory cleanup now has to be meaningfully destructive. Sleep-side consolidation should translate old thought fossils into the live claim/question/fascination surface, drop templated slogan-slop, trim repo/archive/runtime residue down to a small recent evidence surface, and keep the freshest surviving records instead of accidentally embalming the oldest ones after a sort.