From 2f3aaa8f175030a1ee750c526e44dea97360da57 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 17 May 2026 08:32:17 +0100 Subject: [PATCH] Clarify typed self-state projection boundary --- notes/fresh-workspace-handoff.md | 2 +- notes/voidbot-current-system-map.md | 4 + notes/voidbot-implementation-plan.md | 7 +- packages/core/src/void-self-state-loader.ts | 195 +---------------- .../core/src/void-self-state-projection.ts | 200 ++++++++++++++++++ state/evidence.jsonl | 1 + 6 files changed, 216 insertions(+), 193 deletions(-) diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index ac7db1f..462ddd5 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -26,7 +26,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - CultCache-backed self-state uses the `.cc` extension. The active store path is `.voidbot/private/void-self-state.cc`. - The smallest coherent target is one polymorphic CultCache-backed state authority with typed document kinds plus typed mutation tools: agents propose small operations, and the state service validates/applies them. Whole-state JSON editing is rejected as a durable mutation path. - Commit-one boundary naming is now in code at `packages/core/src/void-self-state-domain.ts`: it declares the global typed document kinds and strict operation payload schemas. Runtime behavior has not moved yet. -- Read-only projection is now in `packages/core/src/void-self-state-projection.ts`. `loadVoidSelfState` uses that typed projection for nap/reply-mode context while leaving the old richer prompt summary intact until typed documents explicitly own those details. +- 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. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index 294d53e..6d50db2 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -42,6 +42,10 @@ This note is the source-grounded description of how the live VoidBot stack is sh - 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, shared Void self-state projection, and any precomputed situational social read. +- `packages/core/src/void-self-state-loader.ts` + - thin typed `.cc` self-state loader that delegates read-side rendering to the projection module. +- `packages/core/src/void-self-state-projection.ts` + - read-side projection authority for typed self-state: renders the agent-facing summary and compact nap/reply-mode projection from typed documents without touching legacy state. - `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` diff --git a/notes/voidbot-implementation-plan.md b/notes/voidbot-implementation-plan.md index b393770..914e785 100644 --- a/notes/voidbot-implementation-plan.md +++ b/notes/voidbot-implementation-plan.md @@ -176,9 +176,10 @@ Verdict: stop feature work and rebuild this foundation. The rest of the machine ### Commit 2: Add Read-Only Typed Projections -- Build a self-state service that reads current canonical state and renders the same summaries used by direct replies. -- Keep JSON projection for the old runner. -- Verification: compare rendered summary against current loader output on a fixture. +- Landed path: `packages/core/src/void-self-state-projection.ts` owns typed `.cc` document to agent-facing summary/projection rendering. +- `packages/core/src/void-self-state-loader.ts` only loads typed documents and delegates read-side projection. +- No JSON projection is needed for the live typed runner. +- Verification: projection smoke loads a temp `.cc` store and confirms mode plus transient room context render. ### Commit 3: Add Typed Mutation CLI diff --git a/packages/core/src/void-self-state-loader.ts b/packages/core/src/void-self-state-loader.ts index 44c1ff5..1b64136 100644 --- a/packages/core/src/void-self-state-loader.ts +++ b/packages/core/src/void-self-state-loader.ts @@ -4,11 +4,7 @@ import { type VoidSelfStateContext, } from "@voidbot/shared"; -import { - type VoidScheduledRuntime, - type VoidThoughtMemory, -} from "./void-self-state-domain"; -import { type VoidSelfStateTypedProjection } from "./void-self-state-projection"; +import { buildVoidSelfStateContext } from "./void-self-state-projection"; import { loadVoidSelfStateTypedDocuments } from "./void-self-state-service"; export interface LoadVoidSelfStateOptions { @@ -20,9 +16,12 @@ export async function loadVoidSelfState( statePath: string, options: LoadVoidSelfStateOptions = {}, ): Promise { - let typedProjection: VoidSelfStateTypedProjection; try { - typedProjection = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath }); + const typedProjection = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath }); + return buildVoidSelfStateContext(typedProjection, { + sourcePath: statePath, + ...options, + }); } catch (error) { const code = (error as NodeJS.ErrnoException).code; @@ -32,186 +31,4 @@ export async function loadVoidSelfState( throw error; } - - const projection = buildVoidSelfStateProjection(typedProjection); - - return { - sourcePath: statePath, - loadedAt: new Date().toISOString(), - summary: renderTypedVoidSelfStateSummary(typedProjection, options), - projection, - }; -} - -function renderTypedVoidSelfStateSummary( - state: VoidSelfStateTypedProjection, - options: LoadVoidSelfStateOptions, -): string { - const profile = state.selfProfile; - const topValues = state.selfProfile.values - .sort((left, right) => right.priority - left.priority) - .slice(0, 4) - .map((value) => value.label); - const memories = state.thoughtMemory.memories - .slice(-6) - .reverse() - .map((memory) => renderTypedMemory(memory)); - const incubation = state.thoughtMemory.incubation - .filter((thread) => thread.status !== "retired") - .sort((left, right) => right.maturation - left.maturation) - .slice(0, 4) - .map((thread) => renderTypedIncubationThread(thread)); - const openCases = state.moderationCursor.openCases - .filter((entry) => !["answered", "resolved", "closed", "retired", "dropped"].includes(entry.status)) - .slice(-4) - .reverse() - .map((entry) => `- ${entry.summary}${entry.authorName ? ` [from ${entry.authorName}]` : ""}`); - const interventions = state.candidateInterventions.interventions - .filter((entry) => entry.status === "queued" || entry.status === "deferred") - .slice(-3) - .reverse() - .map((entry) => `- ${entry.summary}: ${entry.draft}`); - - return [ - `- Identity: ${profile.publicName}${profile.publicDescription ? ` - ${profile.publicDescription}` : ""}`, - profile.privateNotes.length > 0 - ? `- Private notes: ${profile.privateNotes.slice(0, 4).join(" | ")}` - : "- Private notes: none recorded.", - topValues.length > 0 - ? `- Highest-priority values: ${topValues.join(" | ")}` - : "- Highest-priority values: none recorded.", - renderTypedSleepCycleSummary(state.scheduledRuntime, state.thoughtMemory), - renderTypedSpeakingPressureSummary(state.scheduledRuntime), - openCases.length > 0 - ? ["- Outstanding room obligations:", ...openCases].join("\n") - : "- Outstanding room obligations: none recorded.", - memories.length > 0 - ? ["- Typed memory:", ...memories].join("\n") - : "- Typed memory: none recorded.", - incubation.length > 0 - ? ["- Incubating threads:", ...incubation].join("\n") - : "- Incubating threads: none recorded.", - interventions.length > 0 - ? ["- Candidate interventions:", ...interventions].join("\n") - : "- Candidate interventions: none recorded.", - renderTransientRoomLog(options), - ].join("\n"); -} - -function renderTypedMemory(memory: VoidThoughtMemory["memories"][number]): string { - const target = memory.target.label ?? memory.target.id; - const meaning = [ - memory.claim ? `claim=${memory.claim}` : undefined, - memory.question ? `question=${memory.question}` : undefined, - memory.tension ? `tension=${memory.tension}` : undefined, - memory.actionImplication ? `next=${memory.actionImplication}` : undefined, - ].filter((value): value is string => Boolean(value)); - return `- ${target} [${memory.kind}]: ${memory.summary}${meaning.length > 0 ? ` (${meaning.join("; ")})` : ""}`; -} - -function renderTypedIncubationThread(thread: VoidThoughtMemory["incubation"][number]): string { - const target = thread.target.label ?? thread.target.id; - const metrics = [ - `status=${thread.status}`, - `maturation=${thread.maturation.toFixed(2)}`, - thread.desireToSpeak !== undefined ? `speak=${thread.desireToSpeak.toFixed(2)}` : undefined, - thread.noveltyToSelf !== undefined ? `self=${thread.noveltyToSelf.toFixed(2)}` : undefined, - thread.noveltyToRoom !== undefined ? `room=${thread.noveltyToRoom.toFixed(2)}` : undefined, - ].filter((value): value is string => Boolean(value)); - return `- ${thread.topic} -> ${target} [${metrics.join("; ")}]: ${thread.summary}`; -} - -function renderTypedSleepCycleSummary( - runtime: VoidScheduledRuntime, - thoughtMemory: VoidThoughtMemory, -): string { - const sleepCycle = runtime.sleepCycle; - 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 cycle: napping${sleepCycle.currentNapEndsAt ? ` until ${sleepCycle.currentNapEndsAt}` : ""}.` - : `- Sleep cycle: awake${sleepCycle.nextNapStartsAt ? `; next nap ${sleepCycle.nextNapStartsAt}` : ""}.`; - const themeLine = - sleepCycle.activeDreamThemes.length > 0 - ? `- Active dream themes: ${sleepCycle.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 renderTypedSpeakingPressureSummary(runtime: VoidScheduledRuntime): string { - const pressure = runtime.speakingPressure; - const parts = [ - `needToSpeak=${pressure.needToSpeak.toFixed(2)}`, - pressure.confessionPressure !== undefined ? `confession=${pressure.confessionPressure.toFixed(2)}` : undefined, - pressure.noveltyPressure !== undefined ? `novelty=${pressure.noveltyPressure.toFixed(2)}` : undefined, - pressure.recentSpeechDamping !== undefined ? `speechDamping=${pressure.recentSpeechDamping.toFixed(2)}` : undefined, - ].filter((value): value is string => Boolean(value)); - - return `- Speaking pressure: ${parts.join(", ")}`; -} - -function renderTransientRoomLog(options: LoadVoidSelfStateOptions): string { - const recentMessages = (options.recentMessages ?? []).slice(-10); - const guildName = options.guildContext?.guildName ?? options.guildContext?.guildId ?? "(direct/unknown)"; - const channelName = - options.guildContext?.channelName ?? options.guildContext?.channelId ?? "(unknown channel)"; - - if (recentMessages.length === 0) { - return [ - "- Current room snapshot:", - `- Current room: guild=${guildName}; channel=${channelName}`, - "- No recent chat messages were captured in this snapshot.", - ].join("\n"); - } - - const lines = recentMessages.map( - (message) => - `- [${message.timestamp}] ${message.authorName}: ${collapseWhitespace(message.content || "(no text content)")}`, - ); - - return [ - "- Current room snapshot:", - `- Current room: guild=${guildName}; channel=${channelName}`, - ...lines, - ].join("\n"); -} - -function buildVoidSelfStateProjection( - typedState: VoidSelfStateTypedProjection, -): VoidSelfStateContext["projection"] { - const sleepCycle = typedState.scheduledRuntime.sleepCycle; - const dreamSummaries = typedState.thoughtMemory.memories - .filter((memory) => memory.kind === "dream_residue") - .slice(-3) - .reverse() - .map((memory) => memory.summary); - const isNapping = sleepCycle.isNapping === true; - const napStartedAt = sleepCycle.currentNapStartedAt; - const napEndsAt = sleepCycle.currentNapEndsAt; - const nextNapAt = sleepCycle.nextNapStartsAt; - - return { - mode: isNapping ? "napping" : "awake", - effortCeiling: isNapping ? "minimal" : "normal", - napStartedAt: isNapping ? napStartedAt : undefined, - napEndsAt: isNapping ? napEndsAt : undefined, - nextNapAt, - activeDreamThemes: sleepCycle.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 collapseWhitespace(value: string): string { - return value.replace(/\s+/g, " ").trim(); } diff --git a/packages/core/src/void-self-state-projection.ts b/packages/core/src/void-self-state-projection.ts index 7c89c00..b40d099 100644 --- a/packages/core/src/void-self-state-projection.ts +++ b/packages/core/src/void-self-state-projection.ts @@ -1,3 +1,9 @@ +import { + type GuildContext, + type SourceMessage, + type VoidSelfStateContext, +} from "@voidbot/shared"; + import { type VoidCandidateInterventions, type VoidModerationCursor, @@ -22,6 +28,13 @@ export interface VoidSelfStateTypedProjection { candidateInterventions: VoidCandidateInterventions; } +export interface VoidSelfStateProjectionOptions { + sourcePath: string; + loadedAt?: string; + recentMessages?: SourceMessage[]; + guildContext?: GuildContext; +} + export function createEmptyVoidSelfState( options: { createdAt?: string } = {}, ): VoidSelfStateTypedProjection { @@ -80,3 +93,190 @@ export function createEmptyVoidSelfState( }), }; } + +export function buildVoidSelfStateContext( + typedState: VoidSelfStateTypedProjection, + options: VoidSelfStateProjectionOptions, +): VoidSelfStateContext { + return { + sourcePath: options.sourcePath, + loadedAt: options.loadedAt ?? new Date().toISOString(), + summary: renderVoidSelfStateSummary(typedState, options), + projection: buildVoidSelfStateProjection(typedState), + }; +} + +export function renderVoidSelfStateSummary( + state: VoidSelfStateTypedProjection, + options: Pick = {}, +): string { + const profile = state.selfProfile; + const topValues = state.selfProfile.values + .sort((left, right) => right.priority - left.priority) + .slice(0, 4) + .map((value) => value.label); + const memories = state.thoughtMemory.memories + .slice(-6) + .reverse() + .map((memory) => renderTypedMemory(memory)); + const incubation = state.thoughtMemory.incubation + .filter((thread) => thread.status !== "retired") + .sort((left, right) => right.maturation - left.maturation) + .slice(0, 4) + .map((thread) => renderTypedIncubationThread(thread)); + const openCases = state.moderationCursor.openCases + .filter((entry) => !["answered", "resolved", "closed", "retired", "dropped"].includes(entry.status)) + .slice(-4) + .reverse() + .map((entry) => `- ${entry.summary}${entry.authorName ? ` [from ${entry.authorName}]` : ""}`); + const interventions = state.candidateInterventions.interventions + .filter((entry) => entry.status === "queued" || entry.status === "deferred") + .slice(-3) + .reverse() + .map((entry) => `- ${entry.summary}: ${entry.draft}`); + + return [ + `- Identity: ${profile.publicName}${profile.publicDescription ? ` - ${profile.publicDescription}` : ""}`, + profile.privateNotes.length > 0 + ? `- Private notes: ${profile.privateNotes.slice(0, 4).join(" | ")}` + : "- Private notes: none recorded.", + topValues.length > 0 + ? `- Highest-priority values: ${topValues.join(" | ")}` + : "- Highest-priority values: none recorded.", + renderTypedSleepCycleSummary(state.scheduledRuntime, state.thoughtMemory), + renderTypedSpeakingPressureSummary(state.scheduledRuntime), + openCases.length > 0 + ? ["- Outstanding room obligations:", ...openCases].join("\n") + : "- Outstanding room obligations: none recorded.", + memories.length > 0 + ? ["- Typed memory:", ...memories].join("\n") + : "- Typed memory: none recorded.", + incubation.length > 0 + ? ["- Incubating threads:", ...incubation].join("\n") + : "- Incubating threads: none recorded.", + interventions.length > 0 + ? ["- Candidate interventions:", ...interventions].join("\n") + : "- Candidate interventions: none recorded.", + renderTransientRoomLog(options), + ].join("\n"); +} + +export function buildVoidSelfStateProjection( + typedState: VoidSelfStateTypedProjection, +): VoidSelfStateContext["projection"] { + const sleepCycle = typedState.scheduledRuntime.sleepCycle; + const dreamSummaries = typedState.thoughtMemory.memories + .filter((memory) => memory.kind === "dream_residue") + .slice(-3) + .reverse() + .map((memory) => memory.summary); + const isNapping = sleepCycle.isNapping === true; + const napStartedAt = sleepCycle.currentNapStartedAt; + const napEndsAt = sleepCycle.currentNapEndsAt; + const nextNapAt = sleepCycle.nextNapStartsAt; + + return { + mode: isNapping ? "napping" : "awake", + effortCeiling: isNapping ? "minimal" : "normal", + napStartedAt: isNapping ? napStartedAt : undefined, + napEndsAt: isNapping ? napEndsAt : undefined, + nextNapAt, + activeDreamThemes: sleepCycle.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 renderTypedMemory(memory: VoidThoughtMemory["memories"][number]): string { + const target = memory.target.label ?? memory.target.id; + const meaning = [ + memory.claim ? `claim=${memory.claim}` : undefined, + memory.question ? `question=${memory.question}` : undefined, + memory.tension ? `tension=${memory.tension}` : undefined, + memory.actionImplication ? `next=${memory.actionImplication}` : undefined, + ].filter((value): value is string => Boolean(value)); + return `- ${target} [${memory.kind}]: ${memory.summary}${meaning.length > 0 ? ` (${meaning.join("; ")})` : ""}`; +} + +function renderTypedIncubationThread(thread: VoidThoughtMemory["incubation"][number]): string { + const target = thread.target.label ?? thread.target.id; + const metrics = [ + `status=${thread.status}`, + `maturation=${thread.maturation.toFixed(2)}`, + thread.desireToSpeak !== undefined ? `speak=${thread.desireToSpeak.toFixed(2)}` : undefined, + thread.noveltyToSelf !== undefined ? `self=${thread.noveltyToSelf.toFixed(2)}` : undefined, + thread.noveltyToRoom !== undefined ? `room=${thread.noveltyToRoom.toFixed(2)}` : undefined, + ].filter((value): value is string => Boolean(value)); + return `- ${thread.topic} -> ${target} [${metrics.join("; ")}]: ${thread.summary}`; +} + +function renderTypedSleepCycleSummary( + runtime: VoidScheduledRuntime, + thoughtMemory: VoidThoughtMemory, +): string { + const sleepCycle = runtime.sleepCycle; + 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 cycle: napping${sleepCycle.currentNapEndsAt ? ` until ${sleepCycle.currentNapEndsAt}` : ""}.` + : `- Sleep cycle: awake${sleepCycle.nextNapStartsAt ? `; next nap ${sleepCycle.nextNapStartsAt}` : ""}.`; + const themeLine = + sleepCycle.activeDreamThemes.length > 0 + ? `- Active dream themes: ${sleepCycle.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 renderTypedSpeakingPressureSummary(runtime: VoidScheduledRuntime): string { + const pressure = runtime.speakingPressure; + const parts = [ + `needToSpeak=${pressure.needToSpeak.toFixed(2)}`, + pressure.confessionPressure !== undefined ? `confession=${pressure.confessionPressure.toFixed(2)}` : undefined, + pressure.noveltyPressure !== undefined ? `novelty=${pressure.noveltyPressure.toFixed(2)}` : undefined, + pressure.recentSpeechDamping !== undefined ? `speechDamping=${pressure.recentSpeechDamping.toFixed(2)}` : undefined, + ].filter((value): value is string => Boolean(value)); + + return `- Speaking pressure: ${parts.join(", ")}`; +} + +function renderTransientRoomLog( + options: Pick, +): string { + const recentMessages = (options.recentMessages ?? []).slice(-10); + const guildName = options.guildContext?.guildName ?? options.guildContext?.guildId ?? "(direct/unknown)"; + const channelName = + options.guildContext?.channelName ?? options.guildContext?.channelId ?? "(unknown channel)"; + + if (recentMessages.length === 0) { + return [ + "- Current room snapshot:", + `- Current room: guild=${guildName}; channel=${channelName}`, + "- No recent chat messages were captured in this snapshot.", + ].join("\n"); + } + + const lines = recentMessages.map( + (message) => + `- [${message.timestamp}] ${message.authorName}: ${collapseWhitespace(message.content || "(no text content)")}`, + ); + + return [ + "- Current room snapshot:", + `- Current room: guild=${guildName}; channel=${channelName}`, + ...lines, + ].join("\n"); +} + +function collapseWhitespace(value: string): string { + return value.replace(/\s+/g, " ").trim(); +} diff --git a/state/evidence.jsonl b/state/evidence.jsonl index b9d9f5c..24baf83 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -105,3 +105,4 @@ {"ts":"2026-05-16T17:39:43Z","type":"implementation","status":"ok","note":"Took legacy scheduled self-state writers offline and rebuilt the active mood/moderation entry scripts around typed-only CultCache state at .voidbot/private/void-self-state.cc. The typed service no longer registers, reads, or writes the legacy moderation document; mood updates sleep/speaking pressure only, and moderation runner is cursor-only until the typed phase machine is rebuilt."} {"ts":"2026-05-16T17:43:21Z","type":"implementation","status":"ok","note":"Deleted the legacy moderation-to-typed projection function from the active self-state projection module. New typed self-state now starts from explicit empty typed documents or validated typed operations; old moderation state is no longer a loader/projection source for the next brain."} {"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."}