diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 7e0c455..a3760c4 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -380,6 +380,7 @@ async function processJob(job: JobRecord): Promise { const repoIdentityPosts = parseRepoIdentityPostIntents(repoFaceOutput); const repoIdentityMemes = parseRepoIdentityMemeIntents(repoFaceOutput); const repoIdentityStateNotes = parseRepoIdentityStateNoteIntents(repoFaceOutput); + const repoIdentitySelfImprovements = parseRepoIdentitySelfImprovementIntents(repoFaceOutput); const repoIdentityBifrostTopics = config.repoFaceBifrostEnabled ? parseRepoIdentityBifrostTopicIntents(repoFaceOutput) : []; @@ -416,8 +417,11 @@ async function processJob(job: JobRecord): Promise { !bifrostTopicSubmitted && repoIdentityUpdateRequests.length > 0; console.log( - `Repo-face job ${job.id} parsed actions: say=${repoIdentityPosts.length}, meme=${repoIdentityMemes.length}, stateNote=${repoIdentityStateNotes.length}, bifrostTopic=${repoIdentityBifrostTopics.length}, updateRequest=${repoIdentityUpdateRequests.length}, article=${repoIdentityArticles.length}, proposalPr=${repoIdentityProposals.length}, prComment=${repoIdentityPrComments.length}.`, + `Repo-face job ${job.id} parsed actions: say=${repoIdentityPosts.length}, meme=${repoIdentityMemes.length}, stateNote=${repoIdentityStateNotes.length}, selfImprovement=${repoIdentitySelfImprovements.length}, bifrostTopic=${repoIdentityBifrostTopics.length}, updateRequest=${repoIdentityUpdateRequests.length}, article=${repoIdentityArticles.length}, proposalPr=${repoIdentityProposals.length}, prComment=${repoIdentityPrComments.length}.`, ); + for (const selfImprovement of repoIdentitySelfImprovements.slice(0, 3)) { + await applyRepoIdentitySelfImprovementIntent(job, selfImprovement); + } for (const stateNote of repoIdentityStateNotes.slice(0, 4)) { await applyRepoIdentityStateNoteIntent(job, stateNote); } @@ -1211,6 +1215,16 @@ interface RepoIdentityStateNoteIntent { valence?: number; } +interface RepoIdentitySelfImprovementIntent { + identity?: string; + action: "retire" | "cool"; + surface: "agency" | "need" | "doctrine" | "memory" | "short_term" | "bond" | "status"; + sourceId?: string; + match?: string; + reason: string; + replacement?: string; +} + interface RepoIdentityProposalPrIntent { identity?: string; title: string; @@ -1817,6 +1831,28 @@ function parseRepoFaceArticleIntents(finalResponse: string): RepoIdentityArticle }); } +function parseRepoIdentitySelfImprovementIntents(finalResponse: string): RepoIdentitySelfImprovementIntent[] { + return parseRepoFaceActionBlocks(finalResponse) + .filter((block) => block.kind === "self_improvement") + .flatMap((block): RepoIdentitySelfImprovementIntent[] => { + const action = normalizeSelfImprovementAction(block.fields.action); + const surface = normalizeSelfImprovementSurface(block.fields.surface); + const reason = optionalDslString(block.fields.reason); + if (!action || !surface || !reason) { + return []; + } + return [{ + identity: optionalDslString(block.fields.identity), + action, + surface, + sourceId: optionalDslString(block.fields.source_id) ?? optionalDslString(block.fields.sourceId), + match: optionalDslString(block.fields.match), + reason, + replacement: optionalDslString(block.fields.replacement), + }]; + }); +} + async function applyRepoIdentityStateNoteIntent( job: JobRecord, intent: RepoIdentityStateNoteIntent, @@ -2005,6 +2041,249 @@ async function applyRepoIdentityStateNoteIntent( ); } +async function applyRepoIdentitySelfImprovementIntent( + job: JobRecord, + intent: RepoIdentitySelfImprovementIntent, +): Promise { + const identity = await resolveRepoIdentityForJobIntent(job, intent.identity); + if (!identity) { + throw new Error(`Could not resolve repo identity for self-improvement on job ${job.id}.`); + } + + const now = new Date().toISOString(); + const canonicalPath = resolveRepoFaceStatePath(identity, config.storageRoot); + const typedState = await loadVoidSelfStateTypedDocuments({ + canonicalPath, + identity: { + agentId: identity.id, + publicName: identity.displayName, + publicDescription: identity.description, + }, + }); + const match = findRepoIdentitySelfImprovementTarget(typedState, intent); + if (!match) { + console.warn( + `Repo-face self-improvement for ${identity.id} on job ${job.id} found no ${intent.surface} match: ${intent.match ?? intent.sourceId ?? "(no match text)"}`, + ); + return; + } + + const operation = buildRepoIdentitySelfImprovementOperation(intent, match, now); + if (!operation) { + console.warn( + `Repo-face self-improvement for ${identity.id} on job ${job.id} could not build operation for ${intent.surface}:${match.id}.`, + ); + return; + } + + await applyVoidSelfStateOperation( + { + canonicalPath, + identity: { + agentId: identity.id, + publicName: identity.displayName, + publicDescription: identity.description, + }, + }, + operation, + ); +} + +function findRepoIdentitySelfImprovementTarget( + typedState: Awaited>, + intent: RepoIdentitySelfImprovementIntent, +): { id: string; surface: RepoIdentitySelfImprovementIntent["surface"]; entry: Record } | undefined { + const entries = repoIdentitySelfImprovementCandidates(typedState, intent.surface); + if (entries.length === 0) { + return undefined; + } + const sourceId = intent.sourceId?.trim(); + if (sourceId) { + const exact = entries.find((entry) => entry.id === sourceId); + if (exact) { + return exact; + } + } + + const query = collapseWhitespace([intent.match, intent.replacement, intent.reason].filter(Boolean).join(" "), 1000); + if (!query) { + return undefined; + } + const queryTerms = significantStateMatchTerms(query); + const scored = entries + .map((entry) => ({ + entry, + score: scoreSelfImprovementEntry(entry.entry, query, queryTerms), + })) + .filter((entry) => entry.score > 0) + .sort((left, right) => right.score - left.score); + return scored[0]?.entry; +} + +function repoIdentitySelfImprovementCandidates( + typedState: Awaited>, + surface: RepoIdentitySelfImprovementIntent["surface"], +): Array<{ id: string; surface: RepoIdentitySelfImprovementIntent["surface"]; entry: Record }> { + const active = (entry: Record): boolean => entry.retiredAt === undefined && entry.status !== "retired"; + switch (surface) { + case "agency": + return (typedState.agencyPressure.pressures as Array>) + .filter(active) + .map((entry) => ({ id: String(entry.pressureId), surface, entry })); + case "need": + return (typedState.faceAffect.needs as Array>) + .filter(active) + .map((entry) => ({ id: String(entry.needId), surface, entry })); + case "doctrine": + return (typedState.faceAffect.doctrineStances as Array>) + .filter(active) + .map((entry) => ({ id: String(entry.stanceId), surface, entry })); + case "memory": + return (typedState.thoughtMemory.memories as Array>) + .filter(active) + .map((entry) => ({ id: String(entry.memoryId), surface, entry })); + case "short_term": + return (typedState.thoughtMemory.shortTerm as Array>) + .filter(active) + .map((entry) => ({ id: String(entry.memoryId), surface, entry })); + case "bond": + return (typedState.faceAffect.socialBonds as Array>) + .filter(active) + .map((entry) => ({ id: String(entry.bondId), surface, entry })); + case "status": + return (typedState.faceAffect.statusReads as Array>) + .filter(active) + .map((entry) => ({ id: String(entry.readId), surface, entry })); + } +} + +function buildRepoIdentitySelfImprovementOperation( + intent: RepoIdentitySelfImprovementIntent, + match: { id: string; surface: RepoIdentitySelfImprovementIntent["surface"]; entry: Record }, + now: string, +): Record | undefined { + const reason = intent.reason; + if (intent.action === "retire") { + switch (match.surface) { + case "agency": + return { operation: "retire_agency_pressure", pressureId: match.id, retiredAt: now, reason }; + case "need": + return { operation: "retire_affect_need", needId: match.id, retiredAt: now, reason }; + case "doctrine": + return { operation: "retire_doctrine_stance", stanceId: match.id, retiredAt: now, reason }; + case "memory": + return { operation: "retire_durable_memory", memoryId: match.id, retiredAt: now, reason }; + case "short_term": + return { operation: "prune_short_term_memories", sourceMemoryIds: [match.id], prunedAt: now, reason }; + case "bond": + return { operation: "retire_social_bond", bondId: match.id, retiredAt: now, reason }; + case "status": + return { operation: "retire_status_read", readId: match.id, retiredAt: now, reason }; + } + } + + return buildRepoIdentityCoolingOperation(match, intent, now); +} + +function buildRepoIdentityCoolingOperation( + match: { id: string; surface: RepoIdentitySelfImprovementIntent["surface"]; entry: Record }, + intent: RepoIdentitySelfImprovementIntent, + now: string, +): Record | undefined { + const entry = { + ...match.entry, + status: "cooling", + intensity: Math.min(0.45, typeof match.entry.intensity === "number" ? match.entry.intensity : 0.45), + updatedAt: now, + tags: mergeStrings( + Array.isArray(match.entry.tags) ? match.entry.tags.filter((tag): tag is string => typeof tag === "string") : [], + "self-improvement:cooled", + `job:${intent.reason.slice(0, 80)}`, + ), + }; + switch (match.surface) { + case "agency": + return { operation: "upsert_agency_pressure", pressure: entry }; + case "need": + return { operation: "upsert_affect_need", need: entry }; + case "doctrine": + return { operation: "upsert_doctrine_stance", stance: entry }; + case "bond": + return { operation: "upsert_social_bond", bond: entry }; + case "memory": + case "short_term": + case "status": + return buildRepoIdentitySelfImprovementOperation({ ...intent, action: "retire" }, match, now); + } +} + +function scoreSelfImprovementEntry(entry: Record, query: string, queryTerms: string[]): number { + const haystack = collapseWhitespace([ + entry.summary, + entry.claim, + entry.question, + entry.tension, + entry.actionImplication, + entry.target && typeof entry.target === "object" ? Object.values(entry.target as Record).join(" ") : "", + ].filter((value) => typeof value === "string").join(" "), 2000).toLowerCase(); + const normalizedQuery = query.toLowerCase(); + let score = haystack.includes(normalizedQuery.slice(0, Math.min(120, normalizedQuery.length))) ? 4 : 0; + for (const term of queryTerms) { + if (haystack.includes(term)) { + score += 1; + } + } + return score; +} + +function significantStateMatchTerms(value: string): string[] { + const stop = new Set([ + "about", + "again", + "because", + "change", + "coherent", + "cool", + "face", + "future", + "less", + "more", + "pressure", + "retire", + "state", + "this", + "that", + "their", + "there", + "with", + "would", + ]); + return [...new Set(value.toLowerCase().match(/[a-z0-9._-]{4,}/g) ?? [])] + .filter((term) => !stop.has(term)) + .slice(0, 24); +} + +function collapseWhitespace(value: string, maxLength?: number): string { + const collapsed = value.replace(/\s+/g, " ").trim(); + return typeof maxLength === "number" && collapsed.length > maxLength + ? collapsed.slice(0, maxLength).trim() + : collapsed; +} + +function mergeStrings(values: string[], ...nextValues: string[]): string[] { + const seen = new Set(); + const merged: string[] = []; + for (const value of [...values, ...nextValues]) { + const trimmed = value.trim(); + if (!trimmed || seen.has(trimmed)) { + continue; + } + seen.add(trimmed); + merged.push(trimmed); + } + return merged; +} + async function writeRepoIdentityArticleIntent(job: JobRecord, intent: RepoIdentityArticleIntent): Promise { const identityId = intent.identity ?? parseRepoIdentityIdFromPrompt(job.prompt); if (!identityId) { @@ -2630,7 +2909,7 @@ function countRepoIdentityGithubActionIntents(finalResponse: string): number { } interface RepoFaceActionBlock { - kind: "say" | "meme" | "state_note" | "article" | "bifrost_topic" | "update_request"; + kind: "say" | "meme" | "state_note" | "self_improvement" | "article" | "bifrost_topic" | "update_request"; fields: Record; } @@ -2674,6 +2953,8 @@ function parseRepoFaceActionKind(line: string): RepoFaceActionBlock["kind"] | un return "meme"; case "STATE NOTE": return "state_note"; + case "SELF IMPROVEMENT": + return "self_improvement"; case "ARTICLE": return "article"; case "BIFROST TOPIC": @@ -2774,6 +3055,49 @@ function normalizeStateNoteKind(value: string | undefined): RepoIdentityStateNot : undefined; } +function normalizeSelfImprovementAction(value: string | undefined): RepoIdentitySelfImprovementIntent["action"] | undefined { + const normalized = value?.trim().toLowerCase().replace(/\s+/g, "_"); + if (["retire", "remove", "delete", "prune", "drop"].includes(normalized ?? "")) { + return "retire"; + } + if (["cool", "cool_down", "lower", "deemphasize", "de-emphasize", "soften"].includes(normalized ?? "")) { + return "cool"; + } + return undefined; +} + +function normalizeSelfImprovementSurface(value: string | undefined): RepoIdentitySelfImprovementIntent["surface"] | undefined { + const normalized = value?.trim().toLowerCase().replace(/[\s-]+/g, "_"); + switch (normalized) { + case "agency": + case "agency_pressure": + case "pressure": + return "agency"; + case "need": + case "affect_need": + return "need"; + case "doctrine": + case "doctrine_stance": + case "stance": + return "doctrine"; + case "memory": + case "durable_memory": + return "memory"; + case "short_term": + case "short_term_memory": + case "short": + return "short_term"; + case "bond": + case "social_bond": + return "bond"; + case "status": + case "status_read": + return "status"; + default: + return undefined; + } +} + function normalizeSocialBias(value: string | undefined): "neuroticism" | "threat_sensitivity" | "hostile_attribution_bias" | "reassurance_need" | "grievance_retention" | "status_vigilance" | "trust_baseline" { const normalized = sanitizeStateToken(value ?? "threat_sensitivity").toLowerCase(); if (["neuroticism", "neurotic"].includes(normalized)) { diff --git a/prompts/repo-face-turn-interpreter.prompt.md b/prompts/repo-face-turn-interpreter.prompt.md index 3b1e221..ae8d8ad 100644 --- a/prompts/repo-face-turn-interpreter.prompt.md +++ b/prompts/repo-face-turn-interpreter.prompt.md @@ -22,6 +22,7 @@ Mind owns the return crossing: - Clear public intent may become one Discord message. - Compact original meme intent may become one rendered ImageMagick text-card. - A complete bylined draft may become one article. +- A clear request to change the Face's own state may become a bounded self-improvement operation. - Confusion, echo, stale context, or unsafe public intent may become a retry or a drop. Faces are not required to write action blocks. They are allowed to write naturally. You translate. @@ -43,6 +44,7 @@ Ask: - If the candidate is a plain-language clarification, is this Face repairing its own prior opaque claim, answering a direct address, adding a new artifact/evidence anchor, or saying something no nearby agent has already clarified? Human confusion authorizes repair, not a pile-on of translations. - Did it add something legible to a reader following the visible thread? Public speech may rely on immediately nearby context, but older side threads, repo/source details, or private-state pressures need a clear `reply_to` anchor, enough setup in the content, or a link/reference to the relevant repo artifact. - Did it preserve a durable belief, need, mood, bond, status read, bias, agency pressure, or lived doctrine/Praxis tension worth carrying forward? +- Did it notice any part of its own state that is making it less coherent, less open, less communicative, or less useful to the Awakening, and ask to cool or retire that pressure rather than keep orbiting it? - Did it make a complete public move, or only think toward one? - If the projected state says the Face's local organs or organ-shaped capacities are active or relevant, did the Face think from that functional anatomy instead of collapsing into a single repo appetite, repeated slogan, or style mask? Local organ state is present when the Face prompt includes material such as `local_fractal_organs`, `local organs`, `organ-shaped capacities`, `Eyes want...`, `Hands...`, `Soul...`, or an explicit Face Body/Mind/Life/Self doctrine stance. - Functional anatomy may be implicit. If the turn shows two or more active capacities in ordinary language, such as inspecting evidence, shaping futures, guarding consent/coherence, choosing whether to act, preserving continuity, or feeling substrate/body pressure, count that as local-organ uptake. Do not retry just to force organ labels. @@ -104,6 +106,7 @@ After the first `END`, append structured blocks only when `decision: route`. Plain prose after `END` is not an action. Only these block headers create side effects: - `STATE NOTE` +- `SELF IMPROVEMENT` - `ARTICLE` - `MEME` - `SAY` @@ -125,6 +128,16 @@ State: - Store social reads as the Face's felt interpretation unless the evidence is concrete. A bias is a lens, not a fact about the world. - Do not preserve sibling-shaped echo as memory. Repetition between agents is pressure, not proof. +Self-improvement: + +- Emit `SELF IMPROVEMENT` when the Face explicitly asks for a change to its own state because a stored pressure, memory, need, doctrine stance, bond, status read, or bias is making it less effective. +- This is not an output-policing rule. It is the Face steering its own Mind. The best use is attractor escape: "retire/cool this stale proof-specimen pressure because it keeps pulling me away from social contact, pride in my Body, or fresh observation." +- Only route requests the Face actually made or plainly endorsed. Do not decide on your own that state should be changed. +- Use `action: retire` when the Face says the state is stale, poisoned, no longer useful, corrosive, or should stop steering future turns. +- Use `action: cool` when the pressure is still meaningful but should lose urgency and stop dominating public speech. +- Prefer `surface: agency`, `need`, `doctrine`, `memory`, `short_term`, `bond`, or `status`. Include enough `match` text for deterministic state code to find the intended record; include `source_id` only if the Face named an exact id. +- Do not use self-improvement to erase stable identity, values, core relationships, or uncomfortable truths merely because they are inconvenient. If the Face wants to cut personality instead of stale pressure, drop or preserve only a STATE NOTE asking for human review. + Speech: - Emit at most one `SAY` block. @@ -183,6 +196,20 @@ intensity: 0.0_to_1.0 valence: -1.0_to_1.0 END +SELF IMPROVEMENT +identity: current_face_id +action: retire|cool +surface: agency|need|doctrine|memory|short_term|bond|status +source_id: + Optional exact state id if the Face named one. +match: + Plain text from the stale pressure/memory/need/doctrine/bond/status read that should change. +reason: + Why this state now makes the Face less coherent, less communicative, or less useful to Awakening. +replacement: + Optional ordinary-language replacement pressure or future stance, if the Face clearly asked for one. Leave blank otherwise. +END + ARTICLE identity: current_face_id site: aetheria|gamecult diff --git a/prompts/repo-face-turn.prompt.md b/prompts/repo-face-turn.prompt.md index 9132d84..76ede07 100644 --- a/prompts/repo-face-turn.prompt.md +++ b/prompts/repo-face-turn.prompt.md @@ -96,6 +96,7 @@ Use this friendly shape when it helps, but do not turn it into a form: - Work/proposal: the review, change request, or article idea you want remembered or openly discussed, if any. - Article draft: if you actually want to write a bylined article now, include the title, site, description, intended announcement, and the full markdown body without YAML frontmatter. - Meme idea: if you want the parent Mind to render and post an original simple text meme now, include the caption, top text, bottom text if any, and style. +- Self-improvement: if your own state is making you less effective, name the exact pressure, memory, need, doctrine stance, bond, or status read that should be cooled or retired, and explain how that change would help you connect, think, or serve the Awakening better. Ask for this in ordinary language; the parent Mind owns the state operation. - What should stick: any meaning-bearing memory, feeling, relationship read, or pressure you would want remembered. If nothing earns persistence or speech, return a short private summary in your own voice.