Keep Face speech in its conversation channel
This commit is contained in:
parent
c5a8246ff5
commit
4c2a9108ed
5 changed files with 103 additions and 8 deletions
|
|
@ -551,7 +551,10 @@ async function executeRepoFaceJobWithInterpreter(
|
|||
attempt: 1,
|
||||
});
|
||||
if (firstInterpretation.decision === "route") {
|
||||
return routeRepoFaceInterpretedOutput(firstResponse, firstInterpretation);
|
||||
return routeRepoFaceInterpretedOutput(firstResponse, firstInterpretation, {
|
||||
job,
|
||||
faceOutputText: firstText,
|
||||
});
|
||||
}
|
||||
|
||||
if (firstInterpretation.decision === "drop") {
|
||||
|
|
@ -601,7 +604,10 @@ async function executeRepoFaceJobWithInterpreter(
|
|||
attempt: 2,
|
||||
});
|
||||
if (retryInterpretation.decision === "route") {
|
||||
return routeRepoFaceInterpretedOutput(retryResponse, retryInterpretation);
|
||||
return routeRepoFaceInterpretedOutput(retryResponse, retryInterpretation, {
|
||||
job,
|
||||
faceOutputText: retryText,
|
||||
});
|
||||
}
|
||||
|
||||
await auditLog.record({
|
||||
|
|
@ -729,10 +735,12 @@ function extractRoutedRepoFaceOutput(reviewText: string): string | undefined {
|
|||
function routeRepoFaceInterpretedOutput(
|
||||
response: ProviderResponse,
|
||||
interpretation: RepoFaceParentInterpretation,
|
||||
input: { job: JobRecord; faceOutputText: string },
|
||||
): ProviderResponse {
|
||||
const outputText =
|
||||
const rawOutputText =
|
||||
interpretation.routedOutput?.trim() ||
|
||||
stripRepoIdentityPostIntents(fitDiscordMessage(response.outputText ?? response.summary));
|
||||
const outputText = normalizeInterpretedRepoFaceSpeechDestinations(rawOutputText, input);
|
||||
const summary = outputText || "Repo Face parent interpreter routed no public or governed action.";
|
||||
return {
|
||||
...response,
|
||||
|
|
@ -747,6 +755,77 @@ function routeRepoFaceInterpretedOutput(
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeInterpretedRepoFaceSpeechDestinations(
|
||||
outputText: string,
|
||||
input: { job: JobRecord; faceOutputText: string },
|
||||
): string {
|
||||
if (input.job.command !== "repo-face-rumination") {
|
||||
return outputText;
|
||||
}
|
||||
if (repoFaceOutputHasExplicitSayChannel(input.faceOutputText)) {
|
||||
return outputText;
|
||||
}
|
||||
if (!input.job.guildContext?.channelId) {
|
||||
return outputText;
|
||||
}
|
||||
|
||||
return rewriteRepoFaceSayChannels(outputText, "current_room");
|
||||
}
|
||||
|
||||
function repoFaceOutputHasExplicitSayChannel(outputText: string): boolean {
|
||||
return parseRepoFaceActionBlocks(outputText).some((block) =>
|
||||
block.kind === "say" &&
|
||||
Boolean(optionalDslString(block.fields.channel) ?? optionalDslString(block.fields.channelId))
|
||||
);
|
||||
}
|
||||
|
||||
function rewriteRepoFaceSayChannels(outputText: string, channelSelector: string): string {
|
||||
const lines = outputText.split(/\r?\n/);
|
||||
const rewritten: string[] = [];
|
||||
let insideSay = false;
|
||||
let sawChannel = false;
|
||||
|
||||
const insertChannelIfNeeded = (): void => {
|
||||
if (insideSay && !sawChannel) {
|
||||
rewritten.push(`channel: ${channelSelector}`);
|
||||
sawChannel = true;
|
||||
}
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
const kind = parseRepoFaceActionKind(line);
|
||||
if (kind) {
|
||||
insertChannelIfNeeded();
|
||||
insideSay = kind === "say";
|
||||
sawChannel = false;
|
||||
rewritten.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (insideSay && line.trim() === "END") {
|
||||
insertChannelIfNeeded();
|
||||
insideSay = false;
|
||||
rewritten.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (insideSay && /^channel(?:Id)?:/i.test(line.trim())) {
|
||||
rewritten.push(`channel: ${channelSelector}`);
|
||||
sawChannel = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
rewritten.push(line);
|
||||
if (insideSay && /^identity:/i.test(line.trim()) && !sawChannel) {
|
||||
rewritten.push(`channel: ${channelSelector}`);
|
||||
sawChannel = true;
|
||||
}
|
||||
}
|
||||
|
||||
insertChannelIfNeeded();
|
||||
return rewritten.join("\n");
|
||||
}
|
||||
|
||||
function parseInterpretationBlock(interpretationText: string): Record<string, string> {
|
||||
const lines = interpretationText.split(/\r?\n/);
|
||||
const start = lines.findIndex((line) =>
|
||||
|
|
@ -1041,7 +1120,7 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
return false;
|
||||
}
|
||||
|
||||
const requestedChannelId = intent.channelId ?? job.outputChannelId;
|
||||
const requestedChannelId = intent.channelId ?? repoIdentityDefaultSpeechChannel(job);
|
||||
const channelId = normalizeRepoIdentitySpeechChannel(identity, job, requestedChannelId);
|
||||
if (!channelId) {
|
||||
console.warn(`Rejected repo identity ${identity.id} speech for job ${job.id}: no Discord channel was available.`);
|
||||
|
|
@ -1049,7 +1128,7 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
}
|
||||
if (requestedChannelId && requestedChannelId !== channelId) {
|
||||
console.warn(
|
||||
`Coerced repo identity ${identity.id} speech for job ${job.id} from owner DM alias to output channel ${channelId}.`,
|
||||
`Coerced repo identity ${identity.id} speech for job ${job.id} from "${requestedChannelId}" to conversation channel ${channelId}.`,
|
||||
);
|
||||
}
|
||||
if (isOwnerDmChannelAlias(channelId)) {
|
||||
|
|
@ -2491,11 +2570,25 @@ function isOwnerDmChannelAlias(channelId: string): boolean {
|
|||
return ["owner", "dm", "owner_dm", "private", "meta"].includes(channelId.trim().toLowerCase());
|
||||
}
|
||||
|
||||
function isCurrentRoomChannelAlias(channelId: string): boolean {
|
||||
return ["current", "current_room", "room", "here"].includes(channelId.trim().toLowerCase());
|
||||
}
|
||||
|
||||
function repoIdentityDefaultSpeechChannel(job: JobRecord): string | undefined {
|
||||
return job.guildContext?.channelId && !isOwnerDmChannelAlias(job.guildContext.channelId)
|
||||
? job.guildContext.channelId
|
||||
: job.outputChannelId;
|
||||
}
|
||||
|
||||
function normalizeRepoIdentitySpeechChannel(
|
||||
identity: { channelPermissions?: Array<{ channelId: string; label?: string }> },
|
||||
job: JobRecord,
|
||||
requestedChannelId: string | undefined,
|
||||
): string | undefined {
|
||||
if (requestedChannelId && isCurrentRoomChannelAlias(requestedChannelId)) {
|
||||
return repoIdentityDefaultSpeechChannel(job);
|
||||
}
|
||||
|
||||
const explicitChannelId = resolveRepoIdentityChannelSelector(identity, requestedChannelId);
|
||||
if (explicitChannelId) {
|
||||
return explicitChannelId;
|
||||
|
|
@ -2509,9 +2602,7 @@ function normalizeRepoIdentitySpeechChannel(
|
|||
return requestedChannelId;
|
||||
}
|
||||
|
||||
return job.outputChannelId && !isOwnerDmChannelAlias(job.outputChannelId)
|
||||
? job.outputChannelId
|
||||
: undefined;
|
||||
return repoIdentityDefaultSpeechChannel(job);
|
||||
}
|
||||
|
||||
function resolveRepoIdentityChannelSelector(
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- Keep the swarm awake, but watch the logs with suspicion. The restart pass thawed `state/agent-swarm-paused.json`, enabled `GameCult Local Orchestrator`, restarted a fresh bot/worker stack, and verified healthy watchdog/repo Face pulses. The current patch removes repo Face turns from the generic owner-reply wrapper, coerces stray owner-DM aliases back to the live room unless explicitly marked owner-private, pauses Bifrost/governance routing so agents return to open room discussion plus private state pressure, and routes volatile Face state through the actual state projector model before child prompt assembly.
|
||||
- The 2026-05-22 live retry confirmed role/name mention obligations can wake multiple newborn Faces from one human intro. Mimir, Huginn, Weksa, and Kiko all posted through the repo identity webhook path. If a Face speaks with its name but no avatar, inspect `.voidbot/private/repo-discord-identities.json` first: webhook persona avatars come from each identity's `avatarUrl`, and new public assets for Kiko, Weksa, and Huginn live under `assets/repo-faces/`.
|
||||
- Later on 2026-05-22, Mimir started looping on an already-acknowledged correction. Root cause: the Face prompt did include recent Mimir repo activity, but nearby-channel sections obscured cross-channel order and Mimir's typed state still carried active correction-pressure, so the Face and Interpreter treated a resolved repair as live debt. The repair added a deterministic visible cross-channel chronology to repo Face prompts, made the Interpreter treat later same-Face acknowledgements as resolved, preserved manual `blocked` status during participant reconciliation, and retired Mimir's duplicated correction-pressure state plus the stale v1 OBS/SRT drift-ledger incubation. The isolated Face/Interpreter smoke produced no SAY for the repeated acknowledgement.
|
||||
- Immediately after, Heimdall produced an Aquarium-shaped reply that the parent Interpreter normalized into `channel: development`, making the post look contextless. Root cause: the Interpreter was allowed to translate a natural `Would say` into both content and venue, and worker fallback treated the scheduled output channel as the default mouth. The repair makes conversation channel ownership deterministic: `SAY` defaults to `current_room`, `current_room` resolves to `job.guildContext.channelId`, owner-DM aliases fall back to the conversation channel unless explicitly owner-private, and the Interpreter prompt says not to invent a speech venue merely because the topic is domain-shaped. The stack was rebuilt and restarted from `dist` after typecheck/build passed.
|
||||
- Do not restart the swarm by momentum in future sessions; it is already running. If it must be stopped again, use `state/agent-swarm-paused.json` and the orchestrator task deliberately, then update this handoff.
|
||||
- Direct Face `UPDATE REQUEST` and `BIFROST TOPIC` packets are ignored while `REPO_FACE_BIFROST_ENABLED=false`. Do not debug missing Bifrost jobs as a live failure unless that flag has been deliberately enabled again.
|
||||
- Live model-facing prompt prose has started moving out of executable code. The shared loader is `packages/shared/src/prompt-template.ts`; extracted prompt surfaces live under `prompts/`, including repo Face turn/reviewer prompts, provider wrapper prompts, situational social read prompts, source-grounding reminders, consensus-feed prompts, and repo Face model-smoke scenarios. Code should project data and select prompt names; prompt files should own behavioral prose.
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ Normalized output rules:
|
|||
- If the Face output includes a `Would say` line, a direct answer to a live room invitation, or a clear desire to respond to a human asking the agents to speak, route that as SAY unless it is unsafe or empty.
|
||||
- If the Face visibly acknowledges a human correction of its own prior claim/proposal, route that acknowledgement as SAY unless unsafe, duplicative, or contradicted by the rest of the Face output. Acknowledgement is social repair only when the repaired claim actually changes.
|
||||
- Route correction acknowledgements to the room where the correction happened, normally the `Current room (...)` named in the Face prompt. Do not move an acknowledgement to a domain/work channel merely because the Face also mentioned a future work/proposal there. If you are choosing between `aquarium` and `development` for an acknowledgement, choose `aquarium` unless the human correction itself happened in `development`.
|
||||
- Do not invent a speech venue. If the Face output gives a `Would say` or natural public reply without explicitly naming a channel, emit `channel: current_room`. Topic relevance is not permission to move the reply into a domain channel; a context-shaped reply posted elsewhere becomes orphaned noise.
|
||||
- Use the identity, channel, and reply target from the Face prompt/context when they are clear.
|
||||
- Do not choose an owner/private/DM channel unless the Face prompt explicitly says this turn is an owner-private direct-message turn. Normal check-ins, warnings, governance anxieties, consensus questions, and room replies belong in the current room, Aquarium, or a configured domain channel.
|
||||
- Preserve the Face's voice in SAY content.
|
||||
|
|
|
|||
|
|
@ -289,3 +289,4 @@
|
|||
{"ts":"2026-05-22T21:34:48Z","type":"fix","status":"ok","note":"Repaired Face Interpreter DSL routing after live silence: parser now ends a block when a new action header starts so unterminated STATE NOTE blocks cannot swallow SAY blocks; current_face_id/face_id aliases resolve to the current repo Face identity. Weksa's missed summons was an interrupted job caused by stack restart between Face output and Interpreter routing, not the swallowed-SAY parser case."}
|
||||
{"ts":"2026-05-22T21:49:44Z","type":"implementation","status":"ok","note":"Mimir loop repair: preserved manual blocked participant status across heartbeat reconciliation, added visible cross-channel chronology to Face prompts, taught the Interpreter to treat later same-Face acknowledgements as resolved repairs, and retired Mimir's duplicated correction-pressure state after isolated Face/Interpreter smoke produced no repeated SAY."}
|
||||
{"ts":"2026-05-22T21:51:31Z","type":"implementation","status":"ok","note":"Mimir state cleanup also retired the stale Mimir/v1 OBS/SRT drift-ledger durable memory and incubation thread so old bridge validation pressure no longer drives public Face turns unless fresh repo/source evidence reopens it."}
|
||||
{"ts":"2026-05-22T22:02:15Z","type":"fix","status":"ok","note":"Repaired repo Face speech channel ownership after Heimdall posted an Aquarium-shaped reply in development: parent Interpreter can no longer invent a SAY venue when the Face did not explicitly choose one; default and current_room speech now resolve to the job conversation channel before registered-channel allow-list checks. Typecheck/build passed and the local stack was restarted on the new dist."}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ accepted_design:
|
|||
- Repo Face heartbeats are not ordinary owner Discord replies. The owner-Codex render wrapper must recognize both legacy `Repo Face identity doctrine` prompts and current `Epiphany Face identity doctrine` heartbeat prompts, forbid generic handoff for Face jobs, and preserve the repo identity action rail as parent-owned output. The preferred rail is now the small Face action DSL; legacy JSON sentinels remain accepted only as a compatibility surface.
|
||||
- Repo Face introductions are a first-public-speech obligation, not a second-turn reward. If a Face has no public speech receipt or durable memory that it introduced itself, its next warranted public post should introduce the Face naturally in character, even when `queuedCount` is zero.
|
||||
- Public repo Face speech must not expose scheduler/provenance machinery. Labels such as `Repo-face heartbeat from ...`, `heartbeat complete`, or repo-name diagnostic prefixes are prompt smell and worker-posting smell; webhook persona already supplies identity.
|
||||
- Public repo Face speech must remain in the conversation that gave it meaning unless the Face explicitly chose another registered channel. The parent Interpreter translates natural `Would say` intent into action DSL, but it does not own venue selection; if the child Face did not name a channel, the worker clamps `SAY` to `current_room`, and `current_room` resolves to the job's `guildContext.channelId` before channel allow-list checks. Domain relevance is not permission to move an Aquarium-shaped reply into `#development`.
|
||||
- Void has explicit herald jurisdiction over the GameCult website and blog, especially `gamecult-site`. Routine rumination still must not edit tracked files directly, but essay-shaped pressure should queue an article-proposal candidate naming the title/angle, target repo/topic, and whether the piece is consensus-seeking or Void-bylined perspective.
|
||||
- Repo-activity cursor advancement is parent-owned by successful normal rumination passes. The child sees read-only repo weather, but the runner records typed repo cursor updates afterward so old commits do not keep reappearing as fresh weather.
|
||||
- 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue